diff --git a/.github/assets/IzzyOnDroid.png b/.github/assets/IzzyOnDroid.png new file mode 100644 index 00000000..59ba94bf Binary files /dev/null and b/.github/assets/IzzyOnDroid.png differ diff --git a/assets/banner_dark.svg b/.github/assets/banner_dark.svg similarity index 100% rename from assets/banner_dark.svg rename to .github/assets/banner_dark.svg diff --git a/assets/banner_light.svg b/.github/assets/banner_light.svg similarity index 100% rename from assets/banner_light.svg rename to .github/assets/banner_light.svg diff --git a/.github/assets/get_it_on_downloader.png b/.github/assets/get_it_on_downloader.png new file mode 100644 index 00000000..fd6967fa Binary files /dev/null and b/.github/assets/get_it_on_downloader.png differ diff --git a/.github/assets/get_it_on_github.png b/.github/assets/get_it_on_github.png new file mode 100644 index 00000000..343eee44 Binary files /dev/null and b/.github/assets/get_it_on_github.png differ diff --git a/.github/badges/coverage.svg b/.github/badges/coverage.svg new file mode 100644 index 00000000..48c7774a --- /dev/null +++ b/.github/badges/coverage.svg @@ -0,0 +1,20 @@ + + Coverage: 46.7% + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/badges/downloads.svg b/.github/badges/downloads.svg new file mode 100644 index 00000000..2a62ae84 --- /dev/null +++ b/.github/badges/downloads.svg @@ -0,0 +1,20 @@ + + Downloads: 15.5k + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/badges/prerelease.png b/.github/badges/prerelease.png new file mode 100644 index 00000000..ff533f6c Binary files /dev/null and b/.github/badges/prerelease.png differ diff --git a/.github/badges/stars.svg b/.github/badges/stars.svg new file mode 100644 index 00000000..a50f8431 --- /dev/null +++ b/.github/badges/stars.svg @@ -0,0 +1,20 @@ + + Stars: 249 + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/badges/tests.svg b/.github/badges/tests.svg new file mode 100644 index 00000000..7fc3e18a --- /dev/null +++ b/.github/badges/tests.svg @@ -0,0 +1,20 @@ + + Tests: 198/199 passed + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/badges/version.svg b/.github/badges/version.svg new file mode 100644 index 00000000..0334fb2b --- /dev/null +++ b/.github/badges/version.svg @@ -0,0 +1,20 @@ + + Version: v2026.07.10 + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/scripts/calculate_coverage.py b/.github/scripts/calculate_coverage.py new file mode 100644 index 00000000..3fb18340 --- /dev/null +++ b/.github/scripts/calculate_coverage.py @@ -0,0 +1,32 @@ +import os + +if os.path.exists('coverage/lcov.info'): + lf = 0 + lh = 0 + with open('coverage/lcov.info') as f: + for line in f: + if line.startswith('LF:'): + lf += int(line.split(':')[1]) + elif line.startswith('LH:'): + lh += int(line.split(':')[1]) + percentage = (lh / lf) * 100 if lf > 0 else 0.0 +else: + percentage = 0.0 + +percentage_str = f"{percentage:.1f}" + +# Color logic +if percentage >= 90: + color = 'green' +elif percentage >= 75: + color = 'yellowgreen' +elif percentage >= 50: + color = 'yellow' +else: + color = 'red' + +github_output = os.environ.get('GITHUB_OUTPUT') +if github_output: + with open(github_output, 'a') as f: + f.write(f'percent={percentage_str}\n') + f.write(f'color={color}\n') diff --git a/.github/scripts/calculate_test_stats.py b/.github/scripts/calculate_test_stats.py new file mode 100644 index 00000000..23d2200b --- /dev/null +++ b/.github/scripts/calculate_test_stats.py @@ -0,0 +1,36 @@ +import json +import os + +if os.path.exists('test-reports/results.json'): + success = 0 + failed = 0 + with open('test-reports/results.json') as f: + for line in f: + try: + data = json.loads(line) + if data.get('type') == 'testDone': + if data.get('hidden', False): + continue + res = data.get('result') + if res == 'success': + success += 1 + elif res in ('failure', 'error'): + failed += 1 + except Exception: + pass + total = success + failed + if failed > 0: + summary = f'{success}/{total} passed' + color = 'red' + else: + summary = f'{success} passed' + color = 'green' +else: + summary = 'no tests' + color = 'grey' + +github_output = os.environ.get('GITHUB_OUTPUT') +if github_output: + with open(github_output, 'a') as f: + f.write(f'summary={summary}\n') + f.write(f'color={color}\n') diff --git a/.github/scripts/fetch_stats.py b/.github/scripts/fetch_stats.py new file mode 100644 index 00000000..98beb8c5 --- /dev/null +++ b/.github/scripts/fetch_stats.py @@ -0,0 +1,47 @@ +import urllib.request +import json +import os + +headers = {'User-Agent': 'Mozilla/5.0'} + +# Fetch Stars +try: + req_repo = urllib.request.Request('https://api.github.com/repos/LeanBitLab/LtvLauncher', headers=headers) + with urllib.request.urlopen(req_repo) as response: + repo_data = json.loads(response.read().decode()) + stars = repo_data.get('stargazers_count', 0) +except Exception as e: + print(f'Error fetching stars: {e}') + stars = 0 + +# Fetch Releases & Downloads +try: + req_releases = urllib.request.Request('https://api.github.com/repos/LeanBitLab/LtvLauncher/releases', headers=headers) + with urllib.request.urlopen(req_releases) as response: + releases_data = json.loads(response.read().decode()) + version = releases_data[0].get('tag_name', 'unknown') if releases_data else 'unknown' + downloads = 0 + for release in releases_data: + for asset in release.get('assets', []): + downloads += asset.get('download_count', 0) +except Exception as e: + print(f'Error fetching releases: {e}') + version = 'unknown' + downloads = 0 + +def format_num(n): + if n >= 1000000: + return f'{n/1000000:.1f}M' + if n >= 1000: + return f'{n/1000:.1f}k' + return str(n) + +stars_str = format_num(stars) +downloads_str = format_num(downloads) + +github_output = os.environ.get('GITHUB_OUTPUT') +if github_output: + with open(github_output, 'a') as f: + f.write(f'stars={stars_str}\n') + f.write(f'version={version}\n') + f.write(f'downloads={downloads_str}\n') diff --git a/.github/scripts/generate_release_notes.py b/.github/scripts/generate_release_notes.py new file mode 100644 index 00000000..3c038db5 --- /dev/null +++ b/.github/scripts/generate_release_notes.py @@ -0,0 +1,117 @@ +import os +import subprocess + +def get_version_code(): + try: + with open("pubspec.yaml", "r") as f: + for line in f: + if line.strip().startswith("version:"): + parts = line.strip().split(":") + version_str = parts[1].strip() + if "+" in version_str: + return version_str.split("+")[1].strip() + except Exception: + pass + return None + +def get_fastlane_changelog(version_code): + if not version_code: + return None + changelog_path = f"fastlane/metadata/android/en-US/changelogs/{version_code}.txt" + if os.path.exists(changelog_path): + try: + with open(changelog_path, "r") as f: + return f.read().strip() + except Exception: + pass + return None + +def get_git_changelog(current_tag): + # Check if the tag exists in git + tag_exists = False + try: + subprocess.check_call(['git', 'rev-parse', current_tag], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + tag_exists = True + except Exception: + tag_exists = False + + # If tag exists, compare. If not, use HEAD as current commit. + current_ref = current_tag if tag_exists else "HEAD" + + try: + # Find the previous tag before the current tag + prev_tag = subprocess.check_output( + ['git', 'describe', '--tags', '--abbrev=0', f'{current_ref}^'], + stderr=subprocess.DEVNULL + ).decode().strip() + except Exception: + prev_tag = "" + + if prev_tag: + git_log_range = f"{prev_tag}..{current_ref}" + else: + git_log_range = current_ref + + try: + log_output = subprocess.check_output( + ['git', 'log', '--pretty=format:- %s', git_log_range] + ).decode().strip() + + changelog_lines = [] + if log_output: + for line in log_output.split('\n'): + # Exclude merge commits or chore commits + if "Merge" in line or "chore: " in line: + continue + changelog_lines.append(line) + return "\n".join(changelog_lines) if changelog_lines else "- General improvements and bug fixes" + except Exception as e: + return f"- New changes in release {current_tag}" + +def get_file_size(path): + if os.path.exists(path): + size_bytes = os.path.getsize(path) + return f"{size_bytes / (1024 * 1024):.1f} MB" + return "0.0 MB" + +def main(): + tag_name = os.environ.get('TAG_NAME', '') + if not tag_name: + try: + tag_name = subprocess.check_output( + ['git', 'describe', '--tags', '--always'] + ).decode().strip() + except Exception: + tag_name = "v-dev" + + # Try loading Fastlane changelog first + version_code = get_version_code() + changelog = get_fastlane_changelog(version_code) + + # Fallback to git changelog if Fastlane is missing/empty + if not changelog: + changelog = get_git_changelog(tag_name) + + size_universal = get_file_size("build/app/outputs/flutter-apk/app-release.apk") + size_armv7 = get_file_size("build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk") + size_arm64 = get_file_size("build/app/outputs/flutter-apk/app-arm64-v8a-release.apk") + + release_notes = f"""### ❤️ Support the Project +If you love using LTvLauncher (ad-free, lightweight, and open source), please consider supporting us! Your contributions help buy testing hardware, cover bills, and keep development active. +👉 **[Sponsor LeanBitLab on GitHub](https://github.com/sponsors/LeanBitLab)** + +### Whats New +{changelog} + +#### 📦 Artifacts +* **LTvLauncher-universal-release.apk**: Universal build containing all architectures. +* **LTvLauncher-armeabi-v7a-release.apk**: Optimized build for ARMv7 architectures (older Android TVs & Fire TV sticks). +* **LTvLauncher-arm64-v8a-release.apk**: Optimized build for ARM64 architectures (newer Android TVs & NVIDIA Shield). +""" + + with open("release_notes.md", "w") as f: + f.write(release_notes) + print("Successfully generated release_notes.md") + +if __name__ == "__main__": + main() diff --git a/.github/workflows/badges.yml b/.github/workflows/badges.yml new file mode 100644 index 00000000..b21b93a0 --- /dev/null +++ b/.github/workflows/badges.yml @@ -0,0 +1,113 @@ +name: Update Badges + +on: + schedule: + - cron: '0 0 * * *' # Run daily at midnight + workflow_dispatch: + +permissions: + contents: write + +jobs: + update-badges: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Install SQLite + run: | + sudo apt-get update + sudo apt-get install -y sqlite3 libsqlite3-dev + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.24.5' + + - name: Install Dependencies + run: flutter pub get + + - name: Generate Mocks + run: flutter pub run build_runner build --delete-conflicting-outputs + + - name: Generate Database Migrations Schema + run: flutter pub run drift_dev schema generate drift_schemas/ test/generated_migrations/ + + - name: Run Tests with Coverage and Reports + run: | + mkdir -p test-reports + flutter test --coverage --file-reporter json:test-reports/results.json > test-reports/test_run.log || true + + - name: Calculate Coverage Percentage + id: coverage + run: python3 .github/scripts/calculate_coverage.py + + - name: Calculate Test Statistics + id: test_stats + run: python3 .github/scripts/calculate_test_stats.py + + - name: Create Coverage Badge + uses: emibcn/badge-action@v2.0.2 + with: + label: 'Coverage' + status: '${{ steps.coverage.outputs.percent }}%' + color: '${{ steps.coverage.outputs.color }}' + path: '.github/badges/coverage.svg' + + - name: Create Tests Badge + uses: emibcn/badge-action@v2.0.2 + with: + label: 'Tests' + status: '${{ steps.test_stats.outputs.summary }}' + color: '${{ steps.test_stats.outputs.color }}' + path: '.github/badges/tests.svg' + + - name: Fetch GitHub Stats + id: stats + run: python3 .github/scripts/fetch_stats.py + + - name: Create Version Badge + uses: emibcn/badge-action@v2.0.2 + with: + label: 'Version' + status: '${{ steps.stats.outputs.version }}' + color: '7C4DFF' + path: '.github/badges/version.svg' + + - name: Create Downloads Badge + uses: emibcn/badge-action@v2.0.2 + with: + label: 'Downloads' + status: '${{ steps.stats.outputs.downloads }}' + color: '7C4DFF' + path: '.github/badges/downloads.svg' + + - name: Create Stars Badge + uses: emibcn/badge-action@v2.0.2 + with: + label: 'Stars' + status: '${{ steps.stats.outputs.stars }}' + color: '7C4DFF' + path: '.github/badges/stars.svg' + + - name: Pull latest changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git stash || true + git pull --rebase origin master + git stash pop || true + + - name: Commit and Push Badges + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: auto-update badges" + file_pattern: ".github/badges/coverage.svg .github/badges/tests.svg .github/badges/version.svg .github/badges/downloads.svg .github/badges/stars.svg" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..70768ae7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: Release APK + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag_name: + description: 'Tag name for the release (e.g. v1.0.0)' + required: true + default: 'v-dev' + +permissions: + contents: write + +jobs: + build-and-release: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '21' + + - name: Install SQLite + run: | + sudo apt-get update + sudo apt-get install -y sqlite3 libsqlite3-dev + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.24.5' + + - name: Install Dependencies + run: flutter pub get + + - name: Generate Mocks + run: flutter pub run build_runner build --delete-conflicting-outputs + + - name: Generate Database Migrations Schema + run: flutter pub run drift_dev schema generate drift_schemas/ test/generated_migrations/ + + - name: Configure Keystore Signing + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} + if: ${{ env.KEYSTORE_BASE64 != '' }} + run: | + python3 -c "import os, base64; s = os.environ['KEYSTORE_BASE64']; s = ''.join(s.split()); m = len(s) % 4; s += '=' * (4 - m) if m else ''; open('android/my-release-key.jks', 'wb').write(base64.b64decode(s))" + + - name: Build Release APKs + env: + KEYSTORE_FILE: "../my-release-key.jks" + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + run: | + # Build universal APK first + flutter build apk --release --target-platform=android-arm,android-arm64 + # Rename/save it temporarily + mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/app-release.apk.temp + # Build split APKs + flutter build apk --release --split-per-abi --target-platform=android-arm,android-arm64 + # Rename to final LTvLauncher-* names + mv build/app/outputs/flutter-apk/app-release.apk.temp build/app/outputs/flutter-apk/LTvLauncher-universal-release.apk + mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk build/app/outputs/flutter-apk/LTvLauncher-armeabi-v7a-release.apk + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk build/app/outputs/flutter-apk/LTvLauncher-arm64-v8a-release.apk + + - name: Generate Release Notes + env: + TAG_NAME: ${{ github.event.inputs.tag_name || github.ref_name }} + run: python3 .github/scripts/generate_release_notes.py + + - name: Create GitHub Release and Upload APKs + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.tag_name || github.ref_name }} + body_path: release_notes.md + files: | + build/app/outputs/flutter-apk/LTvLauncher-universal-release.apk + build/app/outputs/flutter-apk/LTvLauncher-armeabi-v7a-release.apk + build/app/outputs/flutter-apk/LTvLauncher-arm64-v8a-release.apk + draft: false + prerelease: ${{ contains(github.event.inputs.tag_name || github.ref_name, '-') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 144f6a80..1572e4eb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,24 +3,27 @@ *.log *.pyc *.swp +*~ +*.bak +*.tmp +*.temp +Thumbs.db +ehthumbs.db +Desktop.ini .DS_Store .atom/ .buildlog/ .history .svn/ -# IntelliJ related +# IDEs and Editors *.iml *.ipr *.iws .idea/ +.vscode/ -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related +# Flutter / Dart / Pub **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ @@ -30,65 +33,94 @@ .pub-cache/ .pub/ /build/ +pubspec-overrides.yaml -# Web related +# Web / Platforms (Unused Flutter Modules) lib/generated_plugin_registrant.dart +.android/ +.ios/ -# Symbolication related -app.*.symbols +# Swift Package Manager +.swiftpm/ +.build/ -# Obfuscation related +# Symbols & Obfuscation +app.*.symbols app.*.map.json -# Android Studio will place build artifacts here +# Android Build & Secrets /android/app/debug /android/app/profile /android/app/release /android/app/build /android/build - android/app/google-services.json android/app/upload-keystore.jks -coverage/ - -# Secrets +android/keystore.properties *.jks *.keystore keystore.properties key.properties -# Logs +# Coverage, Logs, and Test Reports +coverage/ build_log.txt release_build_log.txt - -# Gemini -.geminiignore -GEMINI.md -GEMINI.original.md - -# Qwen -.qwenignore -QWEN.md -.qwen/ - -# Local Project Docs & Tools +test-reports/ pdocs/ Pdoc/ -build_log.txt -release_build_log.txt +scratch/ +test/app_card_long_press_test.dart -# Android Secrets -android/keystore.properties +# Drift Generated Schemas & Mockito Mocks +drift_schemas/ +test/generated_migrations/ +test/mocks.mocks.dart +# Local Tools, Libs & Scripts +*.py +!.github/scripts/**/*.py +sqlite_lib/ +com.leanbitlab.ltvL.yml +*.db +*.db-journal +*.db-shm +*.db-wal +*.sqlite +*.sqlite-journal +*.sqlite-shm +*.sqlite-wal + +# Environment & Dev Tooling +.env +.gitlab-ci.yml +commitlint.config.js +node_modules/ +.npm/ + +# AI Agent Configs & Documentation .aider* .antigravityignore +.antigravitycli/ +.geminiignore .gemini/ - -# Swift Package Manager (Context7 recommendation) -.swiftpm/ -.build/ - -# Flutter module (Context7 recommendation) -.android/ -.ios/ -*.py +GEMINI.md +GEMINI.original.md +.qwenignore +.qwen/ +QWEN.md +.agents/ +AGENTS.md +AGENTS.original.md +.pi/ +.claude/ +CLAUDE.md +CLAUDE.original.md +.cursor/ +.cursorrules +.continue/ +.roo/ +.supermaven/ +.windsurf/ +.cody/ +.copilot/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 512937a5..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,154 +0,0 @@ -variables: - FLUTTER_DOCKER_IMAGE: cirrusci/flutter:3.7.5 - -stages: - - precheck - - analyze - - test - - build - - transform - - upload - - deploy - - promote - -commitlint: - image: - name: gtramontina/commitlint:8.3.5 - entrypoint: [ "" ] - stage: precheck - needs: [ ] - script: - - commitlint --from origin/master --verbose - except: - - master - -stylelint: - image: $FLUTTER_DOCKER_IMAGE - stage: precheck - needs: [ ] - script: - - flutter format --line-length 120 --dry-run --set-exit-if-changed . - -analyze: - image: $FLUTTER_DOCKER_IMAGE - stage: analyze - needs: [ ] - script: - - flutter analyze - -test: - image: $FLUTTER_DOCKER_IMAGE - stage: test - needs: [ ] - before_script: - - apt-get update - - apt-get install -y libsqlite3-dev - script: - - flutter test --coverage - after_script: - - lcov --remove coverage/lcov.info lib/database.drift.dart --output-file coverage/lcov.info - - genhtml --output ./coverage coverage/lcov.info - coverage: '/^\s+lines\.+:\s+([\d.]+\%)\s+/' - -build:appbundle:debug: - image: $FLUTTER_DOCKER_IMAGE - stage: build - needs: [ ] - except: - - master - - tags - script: - - flutter build appbundle --debug - -build:appbundle:release: - image: $FLUTTER_DOCKER_IMAGE - stage: build - needs: [ ] - only: - - master - - tags - before_script: - - echo "$GOOGLE_SERVICES_JSON" > android/app/google-services.json - - echo -n "$SIGNING_JKS_FILE_BASE64" | base64 -d > android/app/upload-keystore.jks - - if [ -n "$CI_COMMIT_TAG" ]; then BUILD_NAME=$CI_COMMIT_TAG; else BUILD_NAME=$CI_COMMIT_SHORT_SHA; fi - script: - - flutter build appbundle --release --build-number=$CI_PIPELINE_IID --build-name=$BUILD_NAME - artifacts: - paths: - - build/app/outputs/bundle/release/app-release.aab - -build:apk: - image: openjdk:16-buster - stage: transform - needs: [ "build:appbundle:release" ] - only: - - tags - before_script: - - echo -n "$SIGNING_JKS_FILE_BASE64" | base64 -d > android/app/upload-keystore.jks - - wget https://github.com/google/bundletool/releases/download/1.6.1/bundletool-all-1.6.1.jar -O bundletool.jar - script: - - java -jar bundletool.jar build-apks --mode=universal --bundle=build/app/outputs/bundle/release/app-release.aab --output=app-release.apks --ks=android/app/upload-keystore.jks --ks-pass=pass:$SIGNING_KEYSTORE_PASSWORD --ks-key-alias=$SIGNING_KEY_ALIAS --key-pass=pass:$SIGNING_KEY_PASSWORD - - unzip app-release.apks - - mv universal.apk app-release.apk - artifacts: - paths: - - app-release.apk - -deploy:gitlab-release: - image: curlimages/curl:latest - stage: deploy - needs: [ "commitlint", "stylelint", "analyze", "test", "build:apk" ] - only: - - tags - script: - - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file app-release.apk "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/flauncher/$CI_COMMIT_TAG/flauncher-$CI_COMMIT_TAG.apk"' - -deploy:play-store: - stage: deploy - needs: [ "commitlint", "stylelint", "analyze", "test", "build:appbundle:release" ] - image: ruby:2.6.3 - only: - - tags - before_script: - - echo "$GOOGLE_PLAY_API_KEY" > android/fastlane/google_play_api_key.json - - export LC_ALL=en_US.UTF-8 - - export LANG=en_US.UTF-8 - - gem install fastlane -NV - - cd android - script: - - fastlane deploy - -promote:play-store: - stage: promote - needs: [ "deploy:play-store" ] - image: ruby:2.6.3 - when: manual - only: - - tags - before_script: - - echo "$GOOGLE_PLAY_API_KEY" > android/fastlane/google_play_api_key.json - - export LC_ALL=en_US.UTF-8 - - export LANG=en_US.UTF-8 - - gem install fastlane -NV - - cd android - script: - - fastlane promote - -promote:gitlab-release: - stage: promote - needs: [ "deploy:gitlab-release" ] - image: registry.gitlab.com/gitlab-org/release-cli:latest - when: manual - only: - - tags - script: - - echo 'running promote:gitlab-release' - release: - name: $CI_COMMIT_TAG - description: FLauncher v$CI_COMMIT_TAG - tag_name: $CI_COMMIT_TAG - assets: - links: - - name: flauncher-$CI_COMMIT_TAG.apk - url: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/flauncher/$CI_COMMIT_TAG/flauncher-$CI_COMMIT_TAG.apk - filepath: /flauncher-$CI_COMMIT_TAG.apk diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e18d1062..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "automatic", - "java.compile.nullAnalysis.mode": "automatic", - "editor.fontSize": 16, - "editor.minimap.sectionHeaderFontSize": 11, - "debug.console.fontSize": 16, - "markdown.preview.fontSize": 16 -} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 9998886d..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,76 +0,0 @@ -# LTvLauncher Agent Guidelines - -This document provides important guidelines, architectural decisions, and context for AI agents (like Jules) working on the LTvLauncher project. Read these instructions carefully before making modifications to ensure consistency, performance, and stability. - -## Project Overview - -- **Name:** LTvLauncher (a fork of FLauncher) -- **Language:** Dart / Flutter -- **Platform:** Android TV -- **Package Names:** `com.leanbitlab.ltvL` (and legacy `me.efesser.flauncher`) - -## Architecture & State Management - -- **State Management:** The project uses the `provider` package (`Selector`, `ChangeNotifierProvider`, `context.read`) for state management and dependency injection. -- **Service Logic:** Complex logic involving joining categories and applications, and visibility filtering, is handled in `AppsService._refreshState` rather than the database layer. Tests for these behaviors reside in `test/providers/apps_service_test.dart`. -- **Race Conditions:** To prevent race conditions in asynchronous `ChangeNotifier` methods that update state, use a sequence counter (e.g., `_callCount`) incremented at method entry and compare it to a local snapshot before calling `notifyListeners()`. -- **Localization:** Managed with `flutter gen-l10n` via `.arb` files in `lib/l10n/`. The generated `AppLocalizations` classes must be imported via `package:flutter_gen/gen_l10n/app_localizations.dart` (do not use relative paths). -- **Settings Service Optimization:** Cache SharedPreferences values in local fields initialized during construction. Setters must update both the local cache and SharedPreferences to avoid N+1 synchronous read overhead. Use a `Key` suffix for SharedPreferences string constants (e.g., `_themesKey`). -- **Backward Compatibility:** When renaming or refactoring settings features, preserve the underlying SharedPreferences string keys (e.g., `'wifi_usage_period'`) to maintain compatibility for existing users. -- **Cache Invalidation:** In `AppsService`, ensure cache invalidation methods are called not only when the primary collection (`_categoriesById`) changes, but also when mutable properties of individual items (e.g., `category.name`) are updated. - -## Testing & Mocking - -- **Test Framework:** Use `mockito` and `flutter_test`. Run the test suite with `flutter test` (do not use `dart test`). -- **Mock Generation:** Mocks are defined in `test/mocks.dart` using `@GenerateMocks`. To generate new mocks, append the target class to the array and run `flutter pub run build_runner build --delete-conflicting-outputs`. -- **Model Tests:** Unit tests for data models should be organized under `test/models/` mirroring `lib/models/`. -- **Database Testing:** - - Instantiate the database with `FLauncherDatabase.inMemory()` to avoid side effects. - - When testing Drift database operations with foreign keys, ensure parent records are inserted first (e.g., `database.persistApps()`). -- **Provider / UI Widget Testing:** - - Wrap the test widget in a `MultiProvider` and `Directionality` (e.g., `textDirection: TextDirection.ltr`). - - Provide default stubs for visual preference getters in mocked `SettingsService` (e.g., `accentColorHex`, `theme`, `appHighlightAnimationEnabled`) to prevent null/type errors. - - For `intl` date formatting (like `DateTimeWidget`), call `await initializeDateFormatting('en_US');` in `setUpAll`. -- **FocusNode Geometry:** When testing FocusNode geometry or traversal (critical for Android TV), use `WidgetTester` with `Stack` and `Positioned` to explicitly lay out nodes, as they need to be attached to a rendered widget tree to have dimensions. -- **AppsService Testing:** - - Wait for the service to signal completion by polling the `initialized` property (`while (!appsService.initialized) { await Future.delayed(...); }`). - - Ensure `MockFLauncherChannel` is stubbed for `getApplicationIcon` and `getApplicationBanner` to avoid `MissingStubError`. -- **Asynchronous Interactions:** Use `await Future.delayed(Duration.zero);` to allow the microtask queue to clear before making assertions on unawaited asynchronous calls. -- **SharedPreferences & Channels:** Initialize test environments using `SharedPreferencesStorePlatform.instance = InMemorySharedPreferencesStore.empty();` and mock channel calls via `TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler`. -- **Test File Naming:** Avoid versioned suffixes like `_v2` in filenames; use descriptive names (e.g., `apps_service_batch_test.dart`). - -## Performance & Optimization - -- **File I/O:** Avoid synchronous file I/O (e.g., `File.existsSync`) on the main UI thread. Prefer `File.exists` and `await` the result to prevent jank. -- **Map Lookups:** Prefer a single lookup with a null check (`final val = map[key]; if (val != null)`) over `containsKey` followed by a forced unwrap (`map[key]!`). -- **Concurrency:** Use `package:pool` to manage concurrency limits for unawaited background tasks, particularly for intensive operations like fetching app icons and banners. -- **Database Operations (Drift):** Use the `batch` API for multiple insertions, or rely on existing custom batch methods (like `insertAppsCategories`) for efficient single-transaction operations. Bulk operations (e.g., `addAllToCategory`) prevent N+1 query patterns. -- **Logging:** Prefer `log` from `dart:developer` over `print`. Use the `name` parameter to provide class context (e.g., `name: 'NetworkService'`) and pass exceptions to the `error` parameter. -- **Image Rebuilding:** To force an `Image` widget to rebuild when a `FileImage` changes but its path remains the same, append a version counter or timestamp to its `Key` (e.g., `Key("background_$version")`). - -## UI & Interactions - -- **Painting Order:** In `ListView` or `GridView`, items are painted sequentially. To prevent scaled-up focused items (like app banners) from being visually cropped by siblings, cap the maximum scale factor relative to the item's `maxWidth` using `LayoutBuilder` so expansion stays within spacing gaps. -- **Keyboard Events:** When migrating from `onKey` to `onKeyEvent`, ensure `KeyRepeatEvent` is explicitly handled alongside `KeyDownEvent` to preserve continuous/long-press interactions. - -## Platform / Android Specifics - -- **Java Compatibility:** The Android project is configured with Java 21 compatibility (source and target). -- **URI Injection Prevention:** Validate `packageName` strings received from Flutter MethodChannels (e.g., regex `^[a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)+$`) before using them in `Uri.fromParts` to construct Intents. -- **NetworkStatsManager:** When querying `querySummaryForDevice`, ensure the `subscriberId` parameter is passed as `null` rather than an empty string `""`. -- **Backups:** Android backups are explicitly disabled via `android:allowBackup="false"` and removing `android:fullBackupContent` in `AndroidManifest.xml` to prevent unauthorized data extraction via adb backup. -- **Multi-Manifest:** The project follows a multi-manifest structure with source sets for `main`, `debug`, and `profile` located under `android/app/src/`. - -## Build, Environment, and Tooling - -- **Git & Environment Files:** **Never edit the `.gitignore` file.** -- **Dependency Issues (Environment Constraints):** - - Commands like `flutter pub get` or `flutter test` might unintentionally update `pubspec.lock` with newer/beta SDK constraints. Restore `pubspec.lock` (e.g., `git restore pubspec.lock`) before committing. - - Constant evaluation errors (`FontWeight`) might occur when `google_fonts` >= 6.3.0. Pinning `google_fonts` to a lower version like `6.2.1` in `pubspec.yaml` can bypass this during test compilation. - - The execution environment may encounter timeouts (>400s) on network-heavy commands (`flutter pub get`, `flutter test`, `flutter analyze`). Missing `.dart_tool/` triggers `pub get` automatically. - - Use `dart analyze | grep ""` to quickly verify specific static analysis fixes without resolving all environment dependencies. - - Standard Git network operations (e.g., `git fetch`) might timeout. Use `git restore` and manual re-application if necessary for merge conflicts. -- **Android Gradle:** To execute Android Gradle commands (`gradle test`), the `android/local.properties` file must define `sdk.dir` and `flutter.sdk` (`/opt/flutter` in this environment). -- **Releases & Fastlane:** - - When updating `versionCode` in `pubspec.yaml`, rename the corresponding Fastlane changelog file in `fastlane/metadata/android/en-US/changelogs/` to match (e.g., `6101.txt`). - - For Reproducible Builds (RB) on F-Droid / IzzyOnDroid, ensure release tags exactly match the commit where the `pubspec.yaml` version aligns with the published APK (e.g., `version: 3.2.2+6101`). diff --git a/README.md b/README.md index 2b90f12a..acaa0e1e 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,62 @@ # LTvLauncher - - - LTvLauncher Banner + + + LTvLauncher Banner -[![Version](https://img.shields.io/github/v/release/LeanBitLab/LtvLauncher?style=for-the-badge&color=7C4DFF&label=Version)](https://github.com/LeanBitLab/LtvLauncher/releases/latest) [![Downloads](https://img.shields.io/github/downloads/LeanBitLab/LtvLauncher/total?style=for-the-badge&color=7C4DFF&label=Downloads)](https://github.com/LeanBitLab/LtvLauncher/releases) [![Stars](https://img.shields.io/github/stars/LeanBitLab/LtvLauncher?style=for-the-badge&color=7C4DFF)](https://github.com/LeanBitLab/LtvLauncher/stargazers) +[![Version](https://raw.githubusercontent.com/LeanBitLab/LtvLauncher/master/.github/badges/version.svg)](https://github.com/LeanBitLab/LtvLauncher/releases/latest) [![Downloads](https://raw.githubusercontent.com/LeanBitLab/LtvLauncher/master/.github/badges/downloads.svg)](https://github.com/LeanBitLab/LtvLauncher/releases) [![Stars](https://raw.githubusercontent.com/LeanBitLab/LtvLauncher/master/.github/badges/stars.svg)](https://github.com/LeanBitLab/LtvLauncher/stargazers) [![Tests](https://raw.githubusercontent.com/LeanBitLab/LtvLauncher/master/.github/badges/tests.svg)](https://github.com/LeanBitLab/LtvLauncher/actions/workflows/badges.yml) [![Coverage](https://raw.githubusercontent.com/LeanBitLab/LtvLauncher/master/.github/badges/coverage.svg)](https://github.com/LeanBitLab/LtvLauncher/actions/workflows/badges.yml) + + **LTvLauncher** is a fork of [FLauncher](https://github.com/osrosal/flauncher) (originally by [etienn01](https://gitlab.com/flauncher/flauncher)) - an open-source alternative launcher for Android TV. This customized version introduces usability enhancements and some UX improvements by [LeanBitLab](https://github.com/LeanBitLab). - Get it on GitHub + Get it on GitHub - Get it on IzzyOnDroid + Get it on IzzyOnDroid + + + Downloader Code: 7259827 + + Pre-release + + + + + +## Screenshots + + + + + + + + + + + + + + + + +
Home ScreenSettings 1Settings 2Settings 3Screensaver
Home ScreenSettings 1Settings 2Settings 3Screensaver
+ ## Key Features & Enhancements +- **Multiple Backups & Restore Dialog** - Export backups with unique timestamps and restore them via a D-pad friendly in-app backup selection dialog. +- **System-wide Notification Overlays** - Display notification alerts globally over other apps. +- **Status Bar Inputs Toggle** - Easily toggle the visibility of the inputs widget in the status bar. +- **TV Input Selector** - Directly switch between input sources (HDMI, AV, etc.) from the launcher. +- **Accessibility Settings Page** - Manage launcher-specific accessibility settings. - **Data Usage Widget** - Track daily Internet consumption (WiFi, Ethernet, Mobile) directly from the status bar. - **Inbuilt OLED Screensaver** - Minimal screensaver with 30s clock position shifting to prevent burn-in. - **Easy WiFi Access** - Network indicator doubles as a shortcut to system WiFi settings. @@ -59,32 +95,23 @@ This customized version introduces usability enhancements and some UX improvemen - [x] Navigation sound feedback - [x] Official support for `armeabi-v7a`, `arm64-v8a`, and `x86_64` devices. -## Screenshots - - - - - - - - - - - - - - - -
Home ScreenSettings 1Settings 2Settings 3Screensaver
Home ScreenSettings 1Settings 2Settings 3Screensaver
+## Set LTvLauncher as default launcher +### Method 1: Via Built-in Settings (Recommended) +This is the easiest and native way. Go to **Settings -> Accessibility -> Set as default launcher**. This will open the system home picker or default apps settings directly where you can select **LTvLauncher** as your default home app. -## Set LTvLauncher as default launcher +### Method 2: Home Button Fix (Google TV / Fire TV) +If your device blocks changing the default launcher (common on Google TV and newer Fire TV updates), you can use our built-in Home Button Fix: +1. Open **Settings -> Accessibility**. +2. Tap **Home Button Fix (Google TV)**. +3. Turn on the accessibility service for **LTvLauncher** in the system settings. +Once enabled, the launcher will intercept Home button presses to automatically return to **LTvLauncher**. -### Method 1: Remap the Home button -This is the "safer" and easiest way. Use [key Mapper](https://github.com/keymapperorg/KeyMapper) to remap the Home button of the remote to launch LTvLauncher. +### Method 3: Remap the Home button +This is another fallback if the built-in options don't suit your setup. Use [Key Mapper](https://github.com/keymapperorg/KeyMapper) to remap the Home button of the remote to launch LTvLauncher. -### Method 2: Disable the default launcher +### Method 4: Disable the default launcher via ADB **:warning: Disclaimer :warning:** **You are doing this at your own risk, and you'll be responsible in any case of malfunction on your device.** diff --git a/android/app/build.gradle b/android/app/build.gradle index 0e2e4d4a..a1ce4189 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -21,6 +21,18 @@ plugins { id "dev.flutter.flutter-gradle-plugin" } +// Force Flutter to only build for ARMv7 and ARMv8a by default +if (!project.hasProperty('target-platform')) { + project.ext.set('target-platform', 'android-arm,android-arm64') +} else { + def platforms = project.property('target-platform').split(',') + def filtered = platforms.findAll { it == 'android-arm' || it == 'android-arm64' }.join(',') + if (filtered) { + project.ext.set('target-platform', filtered) + } +} + + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -44,6 +56,9 @@ secretProperties.setProperty("signing_keystore_password", "${System.getenv('FLAU secretProperties.setProperty("signing_key_password", "${System.getenv('FLAUNCHER_SIGNING_KEY_PASSWORD')}") secretProperties.setProperty("signing_key_alias", "${System.getenv('FLAUNCHER_SIGNING_KEY_ALIAS')}") +def keystoreFilePath = System.getenv("KEYSTORE_FILE") ?: localProperties['storeFile'] +def hasKeystore = keystoreFilePath != null && file(keystoreFilePath).exists() + android { dependenciesInfo { // Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid) @@ -59,16 +74,19 @@ android { targetSdk 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a' + } } signingConfigs { debug {} release { - if (localProperties['storeFile'] != null) { - storeFile file(localProperties['storeFile']) - storePassword localProperties['storePassword'] - keyAlias localProperties['keyAlias'] - keyPassword localProperties['keyPassword'] + if (hasKeystore) { + storeFile file(keystoreFilePath) + storePassword System.getenv("KEYSTORE_PASSWORD") ?: localProperties['storePassword'] + keyAlias System.getenv("KEY_ALIAS") ?: localProperties['keyAlias'] + keyPassword System.getenv("KEY_PASSWORD") ?: localProperties['keyPassword'] } } } @@ -85,14 +103,21 @@ android { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.release + if (hasKeystore) { + signingConfig signingConfigs.release + } + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a' + } } } splits { abi { // https://stackoverflow.com/a/39950584/21037183 - enable gradle.startParameter.taskNames.any { it.contains("assembleRelease") } + enable gradle.startParameter.taskNames.any { it.contains("assembleRelease") } && System.getenv("UNIVERSAL_ONLY") != "true" + reset() + include "armeabi-v7a", "arm64-v8a" universalApk true } } @@ -106,6 +131,7 @@ android { applicationVariants.all { variant -> variant.outputs.all { output -> + output.versionCodeOverride = variant.versionCode def abi = output.getFilter(com.android.build.OutputFile.ABI) if (abi != null) { outputFileName = "LTvLauncher-${abi}-${variant.buildType.name}.apk" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ed896d1f..6c3a222d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,12 +30,16 @@ + + + + @@ -89,6 +93,29 @@ android:resource="@xml/screensaver" /> + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/leanbitlab/ltvL/LauncherAccessibilityService.java b/android/app/src/main/java/com/leanbitlab/ltvL/LauncherAccessibilityService.java new file mode 100644 index 00000000..ca6738ae --- /dev/null +++ b/android/app/src/main/java/com/leanbitlab/ltvL/LauncherAccessibilityService.java @@ -0,0 +1,30 @@ +package com.leanbitlab.ltvL; + +import android.accessibilityservice.AccessibilityService; +import android.content.Intent; +import android.view.KeyEvent; + +public class LauncherAccessibilityService extends AccessibilityService { + @Override + public void onAccessibilityEvent(android.view.accessibility.AccessibilityEvent event) { + // Not used + } + + @Override + public void onInterrupt() { + // Not used + } + + @Override + protected boolean onKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_HOME) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + return true; // Consume Home key event + } + return super.onKeyEvent(event); + } +} diff --git a/android/app/src/main/java/com/leanbitlab/ltvL/LauncherNotificationListenerService.java b/android/app/src/main/java/com/leanbitlab/ltvL/LauncherNotificationListenerService.java new file mode 100644 index 00000000..dc68c8c9 --- /dev/null +++ b/android/app/src/main/java/com/leanbitlab/ltvL/LauncherNotificationListenerService.java @@ -0,0 +1,254 @@ +package com.leanbitlab.ltvL; + +import android.app.Notification; +import android.content.pm.PackageManager; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Gravity; +import android.view.WindowManager; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import java.util.ArrayList; +import java.util.List; + +public class LauncherNotificationListenerService extends NotificationListenerService { + public interface NotificationListener { + void onNotificationChanged(); + } + + private static final List listeners = new ArrayList<>(); + private static LauncherNotificationListenerService instance = null; + + public static void registerListener(NotificationListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + public static void unregisterListener(NotificationListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + public static LauncherNotificationListenerService getInstance() { + return instance; + } + + @Override + public void onCreate() { + super.onCreate(); + instance = this; + notifyListeners(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (instance == this) { + instance = null; + } + notifyListeners(); + } + + @Override + public void onNotificationPosted(StatusBarNotification sbn) { + notifyListeners(); + showNotificationPopup(sbn); + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn) { + notifyListeners(); + } + + private void notifyListeners() { + synchronized (listeners) { + for (NotificationListener listener : listeners) { + listener.onNotificationChanged(); + } + } + } + + private boolean canShowPopup() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return android.provider.Settings.canDrawOverlays(this); + } + return true; + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } + + private void showNotificationPopup(StatusBarNotification sbn) { + if (sbn == null || sbn.isOngoing()) { + return; + } + + android.content.SharedPreferences prefs = getSharedPreferences("FlutterSharedPreferences", MODE_PRIVATE); + boolean enabled = prefs.getBoolean("flutter.system_notifications_popup", false); + if (!enabled) { + return; + } + + if (!canShowPopup()) { + return; + } + + android.app.Notification notification = sbn.getNotification(); + if (notification == null) return; + + // Filter out service and media transport notifications + String category = notification.category; + if (android.app.Notification.CATEGORY_SERVICE.equals(category) || + android.app.Notification.CATEGORY_TRANSPORT.equals(category)) { + return; + } + + Bundle extras = notification.extras; + if (extras == null) return; + + // Filter out media sessions (playback) + if (extras.containsKey(android.app.Notification.EXTRA_MEDIA_SESSION)) { + return; + } + + CharSequence titleChar = extras.getCharSequence(Notification.EXTRA_TITLE); + CharSequence textChar = extras.getCharSequence(Notification.EXTRA_TEXT); + + final String title = titleChar != null ? titleChar.toString().trim() : ""; + final String text = textChar != null ? textChar.toString().trim() : ""; + + if (title.isEmpty() && text.isEmpty()) { + return; + } + + final String packageName = sbn.getPackageName(); + final PackageManager pm = getPackageManager(); + String appLabel = packageName; + Drawable appIcon = null; + try { + android.content.pm.ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0); + appLabel = pm.getApplicationLabel(appInfo).toString().trim(); + appIcon = pm.getApplicationIcon(appInfo); + } catch (Exception e) { + e.printStackTrace(); + } + + // Filter out generic app running notifications where title/text are just the app label + if (title.equalsIgnoreCase(appLabel) && (text.isEmpty() || text.equalsIgnoreCase(appLabel))) { + return; + } + if (title.equalsIgnoreCase(packageName) && (text.isEmpty() || text.equalsIgnoreCase(packageName))) { + return; + } + + final String finalAppLabel = appLabel; + final Drawable finalAppIcon = appIcon; + + final WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + if (windowManager == null) return; + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + try { + android.widget.LinearLayout container = new android.widget.LinearLayout(LauncherNotificationListenerService.this); + container.setOrientation(android.widget.LinearLayout.HORIZONTAL); + container.setGravity(Gravity.CENTER_VERTICAL); + int pad = dpToPx(16); + container.setPadding(pad, pad, pad, pad); + + android.graphics.drawable.GradientDrawable background = new android.graphics.drawable.GradientDrawable(); + background.setColor(android.graphics.Color.parseColor("#E01E1E1E")); + background.setCornerRadius(dpToPx(12)); + background.setStroke(dpToPx(1), android.graphics.Color.parseColor("#44FFFFFF")); + container.setBackground(background); + + android.widget.ImageView iconView = new android.widget.ImageView(LauncherNotificationListenerService.this); + if (finalAppIcon != null) { + iconView.setImageDrawable(finalAppIcon); + } + android.widget.LinearLayout.LayoutParams iconParams = new android.widget.LinearLayout.LayoutParams(dpToPx(40), dpToPx(40)); + iconParams.rightMargin = dpToPx(12); + iconView.setLayoutParams(iconParams); + container.addView(iconView); + + android.widget.LinearLayout textContainer = new android.widget.LinearLayout(LauncherNotificationListenerService.this); + textContainer.setOrientation(android.widget.LinearLayout.VERTICAL); + android.widget.LinearLayout.LayoutParams textContainerParams = new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT + ); + textContainer.setLayoutParams(textContainerParams); + + android.widget.TextView titleView = new android.widget.TextView(LauncherNotificationListenerService.this); + String headerText = finalAppLabel; + if (title != null && !title.isEmpty()) { + headerText += " • " + title; + } + titleView.setText(headerText); + titleView.setTextColor(android.graphics.Color.WHITE); + titleView.setTextSize(14); + titleView.setTypeface(android.graphics.Typeface.DEFAULT_BOLD); + textContainer.addView(titleView); + + if (text != null && !text.isEmpty()) { + android.widget.TextView bodyView = new android.widget.TextView(LauncherNotificationListenerService.this); + bodyView.setText(text); + bodyView.setTextColor(android.graphics.Color.parseColor("#CCCCCC")); + bodyView.setTextSize(13); + android.widget.LinearLayout.LayoutParams bodyParams = new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT + ); + bodyParams.topMargin = dpToPx(4); + bodyView.setLayoutParams(bodyParams); + textContainer.addView(bodyView); + } + + container.addView(textContainer); + + int layoutFlag; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + layoutFlag = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + layoutFlag = WindowManager.LayoutParams.TYPE_PHONE; + } + + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + layoutFlag, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, + PixelFormat.TRANSLUCENT + ); + + params.gravity = Gravity.TOP | Gravity.END; + params.x = dpToPx(24); + params.y = dpToPx(24); + + windowManager.addView(container, params); + + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + try { + windowManager.removeView(container); + } catch (Exception e) { + } + } + }, 4000); + + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } +} diff --git a/android/app/src/main/java/com/leanbitlab/ltvL/MainActivity.java b/android/app/src/main/java/com/leanbitlab/ltvL/MainActivity.java index fafc38f6..e13f0a44 100644 --- a/android/app/src/main/java/com/leanbitlab/ltvL/MainActivity.java +++ b/android/app/src/main/java/com/leanbitlab/ltvL/MainActivity.java @@ -31,6 +31,9 @@ import android.os.Build; import android.provider.Settings; import android.util.Pair; +import android.media.tv.TvInputManager; +import android.media.tv.TvInputInfo; +import android.media.tv.TvContract; import androidx.annotation.NonNull; @@ -63,10 +66,15 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; +import android.service.notification.StatusBarNotification; +import android.content.ComponentName; + public class MainActivity extends FlutterActivity { private final String METHOD_CHANNEL = "me.efesser.flauncher/method"; private final String APPS_EVENT_CHANNEL = "me.efesser.flauncher/event_apps"; private final String NETWORK_EVENT_CHANNEL = "me.efesser.flauncher/event_network"; + private final String NOTIFICATIONS_EVENT_CHANNEL = "me.efesser.flauncher/event_notifications"; + private MethodChannel.Result pendingPermissionResult; @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { @@ -119,15 +127,57 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { result.success(null); } case "checkWriteSettingsPermission" -> result.success(checkWriteSettingsPermission()); - case "requestWriteSettingsPermission" -> { - requestWriteSettingsPermission(); - result.success(null); - } + case "requestWriteSettingsPermission" -> result.success(requestWriteSettingsPermission()); case "setSystemBrightness" -> { int brightness = call.argument("brightness"); result.success(setSystemBrightness(brightness)); } + case "openDefaultLauncherSettings" -> result.success(openDefaultLauncherSettings()); case "openWifiSettings" -> result.success(openWifiSettings()); + case "openVpnSettings" -> result.success(openVpnSettings()); + case "getTvInputs" -> result.success(getTvInputs()); + case "launchTvInput" -> result.success(launchTvInput(call.arguments())); + case "checkNotificationListenerPermission" -> result.success(checkNotificationListenerPermission()); + case "requestNotificationListenerPermission" -> result.success(requestNotificationListenerPermission()); + case "getActiveNotifications" -> result.success(getActiveNotifications()); + case "dismissNotification" -> { + String key = call.argument("key"); + result.success(dismissNotification(key)); + } + case "dismissAllNotifications" -> result.success(dismissAllNotifications()); + case "checkOverlayPermission" -> result.success(checkOverlayPermission()); + case "requestOverlayPermission" -> result.success(requestOverlayPermission()); + case "checkAccessibilityPermission" -> result.success(isAccessibilityServiceEnabled()); + case "requestAccessibilityPermission" -> result.success(openAccessibilitySettings()); + case "checkWatchNextPermission" -> result.success(checkWatchNextPermission()); + case "requestWatchNextPermission" -> { + if (checkWatchNextPermission()) { + result.success(true); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (pendingPermissionResult != null) { + result.error("ALREADY_REQUESTING", "A permission request is already in progress", null); + } else { + pendingPermissionResult = result; + requestPermissions(new String[]{"android.permission.READ_TV_LISTINGS"}, 1002); + } + } else { + result.success(true); + } + } + case "getWatchNextPrograms" -> result.success(getWatchNextPrograms()); + case "getWatchNextPoster" -> { + String posterArtUri = call.argument("posterArtUri"); + result.success(getWatchNextPoster(posterArtUri)); + } + case "launchWatchNextProgram" -> { + String intentUri = call.argument("intentUri"); + result.success(launchWatchNextProgram(intentUri)); + } + case "getPackageName" -> result.success(getPackageName()); + case "playClickSound" -> { + getWindow().getDecorView().playSoundEffect(android.view.SoundEffectConstants.CLICK); + result.success(null); + } default -> throw new IllegalArgumentException(); } }); @@ -137,6 +187,36 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { new EventChannel(messenger, NETWORK_EVENT_CHANNEL).setStreamHandler( new NetworkEventStreamHandler(this)); + + new EventChannel(messenger, NOTIFICATIONS_EVENT_CHANNEL).setStreamHandler( + new EventChannel.StreamHandler() { + private LauncherNotificationListenerService.NotificationListener listener; + + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + listener = () -> { + runOnUiThread(() -> { + try { + events.success(getActiveNotifications()); + } catch (Exception e) { + e.printStackTrace(); + } + }); + }; + LauncherNotificationListenerService.registerListener(listener); + // Send current state immediately + listener.onNotificationChanged(); + } + + @Override + public void onCancel(Object arguments) { + if (listener != null) { + LauncherNotificationListenerService.unregisterListener(listener); + listener = null; + } + } + } + ); } private List> getApplications() { @@ -650,9 +730,35 @@ private long getMonthlyDataUsage() { private boolean checkUsageStatsPermission() { AppOpsManager appOps = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); - int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, - android.os.Process.myUid(), getPackageName()); - return mode == AppOpsManager.MODE_ALLOWED; + int mode; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + mode = appOps.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, + android.os.Process.myUid(), getPackageName()); + } else { + mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, + android.os.Process.myUid(), getPackageName()); + } + + if (mode == AppOpsManager.MODE_ALLOWED) { + return true; + } + + if (mode == AppOpsManager.MODE_DEFAULT) { + try { + NetworkStatsManager networkStatsManager = (NetworkStatsManager) getSystemService(Context.NETWORK_STATS_SERVICE); + if (networkStatsManager != null) { + long now = System.currentTimeMillis(); + networkStatsManager.querySummaryForDevice(ConnectivityManager.TYPE_WIFI, null, now - 1, now); + return true; + } + } catch (SecurityException e) { + return false; + } catch (Exception e) { + return true; + } + } + + return false; } private void requestUsageStatsPermission() { @@ -667,12 +773,31 @@ private boolean checkWriteSettingsPermission() { return true; } - private void requestWriteSettingsPermission() { + private boolean requestWriteSettingsPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS); - intent.setData(Uri.parse("package:" + getPackageName())); - tryStartActivity(intent); + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, + Uri.parse("package:" + getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (tryStartActivity(intent)) { + return true; + } + } catch (Exception ignored) {} + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (tryStartActivity(intent)) { + return true; + } + } catch (Exception ignored) {} + try { + Intent intent = new Intent(Settings.ACTION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return tryStartActivity(intent); + } catch (Exception ignored) {} + return false; } + return true; } private boolean setSystemBrightness(int brightness) { @@ -696,6 +821,23 @@ private boolean setSystemBrightness(int brightness) { return false; } + private boolean openDefaultLauncherSettings() { + // 1. Try Android TV home settings + Intent homeIntent = new Intent(Settings.ACTION_HOME_SETTINGS); + if (tryStartActivity(homeIntent)) { + return true; + } + + // 2. Try manage default apps settings + Intent defaultAppsIntent = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS); + if (tryStartActivity(defaultAppsIntent)) { + return true; + } + + // 3. Fallback to main settings + return launchActivityFromAction(Settings.ACTION_SETTINGS); + } + private boolean openWifiSettings() { // 1. Try Android Q+ WiFi panel if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -721,6 +863,23 @@ private boolean openWifiSettings() { return launchActivityFromAction(Settings.ACTION_SETTINGS); } + private boolean openVpnSettings() { + // 1. Try standard VPN settings + Intent vpnIntent = new Intent(Settings.ACTION_VPN_SETTINGS); + if (tryStartActivity(vpnIntent)) { + return true; + } + + // 2. Fallback to general wireless settings + Intent wirelessIntent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); + if (tryStartActivity(wirelessIntent)) { + return true; + } + + // 3. Final fallback - open main settings + return launchActivityFromAction(Settings.ACTION_SETTINGS); + } + private boolean openScreensaverSettings() { // 1. Try Android TV specific screensaver settings (DaydreamActivity - from // Aerial Views) @@ -748,4 +907,328 @@ private boolean openScreensaverSettings() { return launchActivityFromAction(Settings.ACTION_SETTINGS); } + private List> getTvInputs() { + List> result = new ArrayList<>(); + try { + TvInputManager manager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); + if (manager != null) { + List inputs = manager.getTvInputList(); + for (TvInputInfo input : inputs) { + if (input.isPassthroughInput()) { + Map map = new HashMap<>(); + map.put("id", input.getId()); + CharSequence label = input.loadLabel(this); + map.put("label", label != null ? label.toString() : input.getId()); + map.put("type", input.getType()); + result.add(map); + } + } + } + } catch (Exception e) { + // TIF might not be supported or initialized on emulator + } + return result; + } + + private boolean launchTvInput(String inputId) { + try { + Uri uri = TvContract.buildChannelUriForPassthroughInput(inputId); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(uri); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return tryStartActivity(intent); + } catch (Exception e) { + return false; + } + } + + private boolean checkNotificationListenerPermission() { + String packageName = getPackageName(); + String flat = Settings.Secure.getString(getContentResolver(), "enabled_notification_listeners"); + if (flat != null) { + String[] names = flat.split(":"); + for (String name : names) { + ComponentName cn = ComponentName.unflattenFromString(name); + if (cn != null && cn.getPackageName().equals(packageName)) { + return true; + } + } + } + return false; + } + + private boolean requestNotificationListenerPermission() { + try { + Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } catch (Exception e) { + e.printStackTrace(); + try { + Intent intent = new Intent(Settings.ACTION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } catch (Exception ex) { + ex.printStackTrace(); + return false; + } + } + } + + private List> getActiveNotifications() { + List> list = new ArrayList<>(); + LauncherNotificationListenerService service = LauncherNotificationListenerService.getInstance(); + if (service == null) { + return list; + } + try { + StatusBarNotification[] sbns = service.getActiveNotifications(); + if (sbns != null) { + for (StatusBarNotification sbn : sbns) { + Map map = new HashMap<>(); + map.put("key", sbn.getKey()); + map.put("packageName", sbn.getPackageName()); + + android.app.Notification notification = sbn.getNotification(); + String title = ""; + String text = ""; + if (notification != null && notification.extras != null) { + CharSequence titleChar = notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE); + CharSequence textChar = notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT); + if (titleChar != null) title = titleChar.toString(); + if (textChar != null) text = textChar.toString(); + } + map.put("title", title); + map.put("text", text); + map.put("isClearable", sbn.isClearable()); + list.add(map); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return list; + } + + private boolean dismissNotification(String key) { + LauncherNotificationListenerService service = LauncherNotificationListenerService.getInstance(); + if (service == null || key == null) { + return false; + } + try { + service.cancelNotification(key); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private boolean dismissAllNotifications() { + LauncherNotificationListenerService service = LauncherNotificationListenerService.getInstance(); + if (service == null) { + return false; + } + try { + service.cancelAllNotifications(); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private boolean checkOverlayPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return Settings.canDrawOverlays(this); + } + return true; + } + + private boolean requestOverlayPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:" + getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } catch (Exception e) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } catch (Exception ex) { + ex.printStackTrace(); + return false; + } + } + } + return true; + } + + private static String cursorStringOrEmpty(android.database.Cursor cursor, String column) { + String val = cursor.getString(cursor.getColumnIndexOrThrow(column)); + return val != null ? val : ""; + } + + private List> getWatchNextPrograms() { + List> list = new ArrayList<>(); + try { + String[] projection = { + TvContract.WatchNextPrograms._ID, + TvContract.WatchNextPrograms.COLUMN_PACKAGE_NAME, + TvContract.WatchNextPrograms.COLUMN_TITLE, + TvContract.WatchNextPrograms.COLUMN_SHORT_DESCRIPTION, + TvContract.WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE, + TvContract.WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS, + TvContract.WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS, + TvContract.WatchNextPrograms.COLUMN_DURATION_MILLIS, + TvContract.WatchNextPrograms.COLUMN_INTENT_URI, + TvContract.WatchNextPrograms.COLUMN_POSTER_ART_URI + }; + + android.database.Cursor cursor = getContentResolver().query( + TvContract.WatchNextPrograms.CONTENT_URI, + projection, + null, + null, + TvContract.WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS + " DESC LIMIT 20" + ); + + if (cursor != null) { + while (cursor.moveToNext()) { + Map map = new HashMap<>(); + map.put("id", cursor.getLong(cursor.getColumnIndexOrThrow(TvContract.WatchNextPrograms._ID))); + map.put("packageName", cursorStringOrEmpty(cursor, TvContract.WatchNextPrograms.COLUMN_PACKAGE_NAME)); + map.put("title", cursorStringOrEmpty(cursor, TvContract.WatchNextPrograms.COLUMN_TITLE)); + map.put("description", cursorStringOrEmpty(cursor, TvContract.WatchNextPrograms.COLUMN_SHORT_DESCRIPTION)); + map.put("watchNextType", cursor.getInt(cursor.getColumnIndexOrThrow(TvContract.WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE))); + map.put("lastEngagementTime", cursor.getLong(cursor.getColumnIndexOrThrow(TvContract.WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS))); + map.put("playbackPosition", cursor.getLong(cursor.getColumnIndexOrThrow(TvContract.WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS))); + map.put("duration", cursor.getLong(cursor.getColumnIndexOrThrow(TvContract.WatchNextPrograms.COLUMN_DURATION_MILLIS))); + map.put("intentUri", cursorStringOrEmpty(cursor, TvContract.WatchNextPrograms.COLUMN_INTENT_URI)); + map.put("posterArtUri", cursorStringOrEmpty(cursor, TvContract.WatchNextPrograms.COLUMN_POSTER_ART_URI)); + list.add(map); + } + cursor.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return list; + } + + private byte[] getWatchNextPoster(String posterArtUri) { + if (posterArtUri == null || posterArtUri.isEmpty()) { + return null; + } + try { + Uri uri = Uri.parse(posterArtUri); + try (java.io.InputStream inputStream = getContentResolver().openInputStream(uri)) { + if (inputStream != null) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + return outputStream.toByteArray(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private boolean launchWatchNextProgram(String intentUri) { + if (intentUri == null || intentUri.isEmpty()) { + return false; + } + // Security: only allow safe URI schemes + String lower = intentUri.toLowerCase(); + if (!lower.startsWith("intent://") && !lower.startsWith("https://") && + !lower.startsWith("http://") && !lower.startsWith("content://")) { + return false; + } + try { + Intent intent = Intent.parseUri(intentUri, Intent.URI_INTENT_SCHEME); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private boolean isAccessibilityServiceEnabled() { + String service = getPackageName() + "/" + LauncherAccessibilityService.class.getName(); + int accessibilityEnabled = 0; + try { + accessibilityEnabled = Settings.Secure.getInt( + getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED + ); + } catch (Settings.SettingNotFoundException e) { + e.printStackTrace(); + } + + if (accessibilityEnabled == 1) { + String settingValue = Settings.Secure.getString( + getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES + ); + if (settingValue != null) { + String[] services = settingValue.split(":"); + for (String s : services) { + if (s.equalsIgnoreCase(service)) { + return true; + } + } + } + } + return false; + } + + private boolean openAccessibilitySettings() { + try { + Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (tryStartActivity(intent)) { + return true; + } + } catch (Exception ignored) {} + try { + Intent intent = new Intent(Settings.ACTION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return tryStartActivity(intent); + } catch (Exception ignored) {} + return false; + } + + private boolean checkWatchNextPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return checkSelfPermission("android.permission.READ_TV_LISTINGS") == PackageManager.PERMISSION_GRANTED; + } + return true; + } + + + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == 1002) { + if (pendingPermissionResult != null) { + boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; + pendingPermissionResult.success(granted); + pendingPermissionResult = null; + } + } + } } diff --git a/android/app/src/main/java/com/leanbitlab/ltvL/NetworkUtils.java b/android/app/src/main/java/com/leanbitlab/ltvL/NetworkUtils.java index 903e31e7..3d531cab 100644 --- a/android/app/src/main/java/com/leanbitlab/ltvL/NetworkUtils.java +++ b/android/app/src/main/java/com/leanbitlab/ltvL/NetworkUtils.java @@ -30,6 +30,7 @@ public class NetworkUtils public static final String KEY_NETWORK_ACCESS = "networkAccess"; public static final String KEY_NETWORK_TYPE = "networkType"; public static final String KEY_WIRELESS_SIGNAL_LEVEL = "wirelessSignalLevel"; + public static final String KEY_VPN_ACTIVE = "vpnActive"; public static Map getNetworkCapabilitiesInformation(Context context, NetworkCapabilities capabilities) { @@ -45,7 +46,10 @@ public static Map getNetworkCapabilitiesInformation(Context cont hasInternetAccess = hasNetworkAccess; } - if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + networkType = NETWORK_TYPE_VPN; + } + else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { networkType = NETWORK_TYPE_CELLULAR; } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { @@ -73,9 +77,6 @@ else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { networkType = NETWORK_TYPE_WIFI; } - else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { - networkType = NETWORK_TYPE_VPN; - } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { networkType = NETWORK_TYPE_WIRED; } @@ -90,28 +91,55 @@ else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { public static Map getNetworkInformation(Context context, Network network) { - Map map = null; - int wirelessNetworkSignalLevel = 0; + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + boolean isVpnActive = false; + NetworkCapabilities physicalCaps = null; if (network != null) { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); + NetworkCapabilities activeCaps = connectivityManager.getNetworkCapabilities(network); + if (activeCaps != null) { + if (activeCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + isVpnActive = true; + } else { + physicalCaps = activeCaps; + } + } + } - if (capabilities != null) { - map = getNetworkCapabilitiesInformation(context, capabilities); + if (isVpnActive || physicalCaps == null) { + for (Network net : connectivityManager.getAllNetworks()) { + NetworkCapabilities caps = connectivityManager.getNetworkCapabilities(net); + if (caps != null) { + if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + isVpnActive = true; + } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) || + caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + if (physicalCaps == null) { + physicalCaps = caps; + } + } + } + } + } - if (Objects.equals(map.get(KEY_NETWORK_TYPE), NETWORK_TYPE_WIFI)) { - try { - WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - if (wifiManager != null) { - WifiInfo wifiInfo = wifiManager.getConnectionInfo(); - if (wifiInfo != null) { - wirelessNetworkSignalLevel = getWifiSignalLevel(wifiInfo); - } + Map map = null; + int wirelessNetworkSignalLevel = 0; + + if (physicalCaps != null) { + map = getNetworkCapabilitiesInformation(context, physicalCaps); + + if (Objects.equals(map.get(KEY_NETWORK_TYPE), NETWORK_TYPE_WIFI)) { + try { + WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + if (wifiManager != null) { + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo != null) { + wirelessNetworkSignalLevel = getWifiSignalLevel(wifiInfo); } - } catch (Exception e) { - e.printStackTrace(); } + } catch (Exception e) { + e.printStackTrace(); } } } @@ -127,6 +155,7 @@ public static Map getNetworkInformation(Context context, Network } map.put(KEY_WIRELESS_SIGNAL_LEVEL, wirelessNetworkSignalLevel); + map.put(KEY_VPN_ACTIVE, isVpnActive); return map; } @@ -135,6 +164,16 @@ public static Map getNetworkInformation(Context context, @Nullab { boolean hasNetworkAccess = false; int networkType = NETWORK_TYPE_UNKNOWN, networkInfoType, wirelessSignalLevel = 0; + boolean isVpnActive = false; + + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager != null) { + // noinspection deprecation + NetworkInfo vpnInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_VPN); + if (vpnInfo != null && vpnInfo.isConnected()) { + isVpnActive = true; + } + } if (networkInfo != null) { hasNetworkAccess = networkInfo.isConnected(); @@ -143,7 +182,7 @@ public static Map getNetworkInformation(Context context, @Nullab if (networkInfoType == ConnectivityManager.TYPE_MOBILE) { networkType = NETWORK_TYPE_CELLULAR; } - if (networkInfoType == ConnectivityManager.TYPE_WIFI) { + else if (networkInfoType == ConnectivityManager.TYPE_WIFI) { WifiManager wifiManager = (WifiManager) context .getApplicationContext().getSystemService(Context.WIFI_SERVICE); @@ -160,7 +199,32 @@ public static Map getNetworkInformation(Context context, @Nullab } } else if (networkInfoType == ConnectivityManager.TYPE_VPN) { - networkType = NETWORK_TYPE_VPN; + isVpnActive = true; + // noinspection deprecation + NetworkInfo wifiInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + // noinspection deprecation + NetworkInfo mobileInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); + // noinspection deprecation + NetworkInfo ethernetInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_ETHERNET); + + if (wifiInfo != null && wifiInfo.isConnected()) { + networkType = NETWORK_TYPE_WIFI; + WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + try { + if (wifiManager != null) { + WifiInfo wInfo = wifiManager.getConnectionInfo(); + if (wInfo != null) { + wirelessSignalLevel = getWifiSignalLevel(wInfo); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } else if (ethernetInfo != null && ethernetInfo.isConnected()) { + networkType = NETWORK_TYPE_WIRED; + } else if (mobileInfo != null && mobileInfo.isConnected()) { + networkType = NETWORK_TYPE_CELLULAR; + } } else if (networkInfoType == ConnectivityManager.TYPE_ETHERNET) { networkType = NETWORK_TYPE_WIRED; @@ -172,6 +236,7 @@ else if (networkInfoType == ConnectivityManager.TYPE_ETHERNET) { mapOut.put(KEY_NETWORK_ACCESS, hasNetworkAccess); mapOut.put(KEY_INTERNET_ACCESS, hasNetworkAccess); mapOut.put(KEY_WIRELESS_SIGNAL_LEVEL, wirelessSignalLevel); + mapOut.put(KEY_VPN_ACTIVE, isVpnActive); return mapOut; } diff --git a/android/app/src/main/java/me/efesser/flauncher/LauncherAccessibilityService.java b/android/app/src/main/java/me/efesser/flauncher/LauncherAccessibilityService.java new file mode 100644 index 00000000..13d6239d --- /dev/null +++ b/android/app/src/main/java/me/efesser/flauncher/LauncherAccessibilityService.java @@ -0,0 +1,30 @@ +package me.efesser.flauncher; + +import android.accessibilityservice.AccessibilityService; +import android.content.Intent; +import android.view.KeyEvent; + +public class LauncherAccessibilityService extends AccessibilityService { + @Override + public void onAccessibilityEvent(android.view.accessibility.AccessibilityEvent event) { + // Not used + } + + @Override + public void onInterrupt() { + // Not used + } + + @Override + protected boolean onKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_HOME) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + return true; // Consume Home key event + } + return super.onKeyEvent(event); + } +} diff --git a/android/app/src/main/java/me/efesser/flauncher/LauncherNotificationListenerService.java b/android/app/src/main/java/me/efesser/flauncher/LauncherNotificationListenerService.java new file mode 100644 index 00000000..b14faa7f --- /dev/null +++ b/android/app/src/main/java/me/efesser/flauncher/LauncherNotificationListenerService.java @@ -0,0 +1,254 @@ +package me.efesser.flauncher; + +import android.app.Notification; +import android.content.pm.PackageManager; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Gravity; +import android.view.WindowManager; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import java.util.ArrayList; +import java.util.List; + +public class LauncherNotificationListenerService extends NotificationListenerService { + public interface NotificationListener { + void onNotificationChanged(); + } + + private static final List listeners = new ArrayList<>(); + private static LauncherNotificationListenerService instance = null; + + public static void registerListener(NotificationListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + public static void unregisterListener(NotificationListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + public static LauncherNotificationListenerService getInstance() { + return instance; + } + + @Override + public void onCreate() { + super.onCreate(); + instance = this; + notifyListeners(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (instance == this) { + instance = null; + } + notifyListeners(); + } + + @Override + public void onNotificationPosted(StatusBarNotification sbn) { + notifyListeners(); + showNotificationPopup(sbn); + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn) { + notifyListeners(); + } + + private void notifyListeners() { + synchronized (listeners) { + for (NotificationListener listener : listeners) { + listener.onNotificationChanged(); + } + } + } + + private boolean canShowPopup() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return android.provider.Settings.canDrawOverlays(this); + } + return true; + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } + + private void showNotificationPopup(StatusBarNotification sbn) { + if (sbn == null || sbn.isOngoing()) { + return; + } + + android.content.SharedPreferences prefs = getSharedPreferences("FlutterSharedPreferences", MODE_PRIVATE); + boolean enabled = prefs.getBoolean("flutter.system_notifications_popup", false); + if (!enabled) { + return; + } + + if (!canShowPopup()) { + return; + } + + android.app.Notification notification = sbn.getNotification(); + if (notification == null) return; + + // Filter out service and media transport notifications + String category = notification.category; + if (android.app.Notification.CATEGORY_SERVICE.equals(category) || + android.app.Notification.CATEGORY_TRANSPORT.equals(category)) { + return; + } + + Bundle extras = notification.extras; + if (extras == null) return; + + // Filter out media sessions (playback) + if (extras.containsKey(android.app.Notification.EXTRA_MEDIA_SESSION)) { + return; + } + + CharSequence titleChar = extras.getCharSequence(Notification.EXTRA_TITLE); + CharSequence textChar = extras.getCharSequence(Notification.EXTRA_TEXT); + + final String title = titleChar != null ? titleChar.toString().trim() : ""; + final String text = textChar != null ? textChar.toString().trim() : ""; + + if (title.isEmpty() && text.isEmpty()) { + return; + } + + final String packageName = sbn.getPackageName(); + final PackageManager pm = getPackageManager(); + String appLabel = packageName; + Drawable appIcon = null; + try { + android.content.pm.ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0); + appLabel = pm.getApplicationLabel(appInfo).toString().trim(); + appIcon = pm.getApplicationIcon(appInfo); + } catch (Exception e) { + e.printStackTrace(); + } + + // Filter out generic app running notifications where title/text are just the app label + if (title.equalsIgnoreCase(appLabel) && (text.isEmpty() || text.equalsIgnoreCase(appLabel))) { + return; + } + if (title.equalsIgnoreCase(packageName) && (text.isEmpty() || text.equalsIgnoreCase(packageName))) { + return; + } + + final String finalAppLabel = appLabel; + final Drawable finalAppIcon = appIcon; + + final WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + if (windowManager == null) return; + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + try { + android.widget.LinearLayout container = new android.widget.LinearLayout(LauncherNotificationListenerService.this); + container.setOrientation(android.widget.LinearLayout.HORIZONTAL); + container.setGravity(Gravity.CENTER_VERTICAL); + int pad = dpToPx(16); + container.setPadding(pad, pad, pad, pad); + + android.graphics.drawable.GradientDrawable background = new android.graphics.drawable.GradientDrawable(); + background.setColor(android.graphics.Color.parseColor("#E01E1E1E")); + background.setCornerRadius(dpToPx(12)); + background.setStroke(dpToPx(1), android.graphics.Color.parseColor("#44FFFFFF")); + container.setBackground(background); + + android.widget.ImageView iconView = new android.widget.ImageView(LauncherNotificationListenerService.this); + if (finalAppIcon != null) { + iconView.setImageDrawable(finalAppIcon); + } + android.widget.LinearLayout.LayoutParams iconParams = new android.widget.LinearLayout.LayoutParams(dpToPx(40), dpToPx(40)); + iconParams.rightMargin = dpToPx(12); + iconView.setLayoutParams(iconParams); + container.addView(iconView); + + android.widget.LinearLayout textContainer = new android.widget.LinearLayout(LauncherNotificationListenerService.this); + textContainer.setOrientation(android.widget.LinearLayout.VERTICAL); + android.widget.LinearLayout.LayoutParams textContainerParams = new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT + ); + textContainer.setLayoutParams(textContainerParams); + + android.widget.TextView titleView = new android.widget.TextView(LauncherNotificationListenerService.this); + String headerText = finalAppLabel; + if (title != null && !title.isEmpty()) { + headerText += " • " + title; + } + titleView.setText(headerText); + titleView.setTextColor(android.graphics.Color.WHITE); + titleView.setTextSize(14); + titleView.setTypeface(android.graphics.Typeface.DEFAULT_BOLD); + textContainer.addView(titleView); + + if (text != null && !text.isEmpty()) { + android.widget.TextView bodyView = new android.widget.TextView(LauncherNotificationListenerService.this); + bodyView.setText(text); + bodyView.setTextColor(android.graphics.Color.parseColor("#CCCCCC")); + bodyView.setTextSize(13); + android.widget.LinearLayout.LayoutParams bodyParams = new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT + ); + bodyParams.topMargin = dpToPx(4); + bodyView.setLayoutParams(bodyParams); + textContainer.addView(bodyView); + } + + container.addView(textContainer); + + int layoutFlag; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + layoutFlag = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + layoutFlag = WindowManager.LayoutParams.TYPE_PHONE; + } + + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + layoutFlag, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, + PixelFormat.TRANSLUCENT + ); + + params.gravity = Gravity.TOP | Gravity.END; + params.x = dpToPx(24); + params.y = dpToPx(24); + + windowManager.addView(container, params); + + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + try { + windowManager.removeView(container); + } catch (Exception e) { + } + } + }, 4000); + + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } +} diff --git a/android/app/src/main/java/me/efesser/flauncher/MainActivity.java b/android/app/src/main/java/me/efesser/flauncher/MainActivity.java index 591b781d..78a00ee6 100644 --- a/android/app/src/main/java/me/efesser/flauncher/MainActivity.java +++ b/android/app/src/main/java/me/efesser/flauncher/MainActivity.java @@ -31,6 +31,9 @@ import android.os.Build; import android.provider.Settings; import android.util.Pair; +import android.media.tv.TvInputManager; +import android.media.tv.TvInputInfo; +import android.media.tv.TvContract; import androidx.annotation.NonNull; @@ -63,10 +66,15 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; +import android.service.notification.StatusBarNotification; +import android.content.ComponentName; + public class MainActivity extends FlutterActivity { private final String METHOD_CHANNEL = "me.efesser.flauncher/method"; private final String APPS_EVENT_CHANNEL = "me.efesser.flauncher/event_apps"; private final String NETWORK_EVENT_CHANNEL = "me.efesser.flauncher/event_network"; + private final String NOTIFICATIONS_EVENT_CHANNEL = "me.efesser.flauncher/event_notifications"; + private MethodChannel.Result pendingPermissionResult; @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { @@ -118,7 +126,58 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { requestUsageStatsPermission(); result.success(null); } + case "checkWriteSettingsPermission" -> result.success(checkWriteSettingsPermission()); + case "requestWriteSettingsPermission" -> result.success(requestWriteSettingsPermission()); + case "setSystemBrightness" -> { + int brightness = call.argument("brightness"); + result.success(setSystemBrightness(brightness)); + } + case "openDefaultLauncherSettings" -> result.success(openDefaultLauncherSettings()); case "openWifiSettings" -> result.success(openWifiSettings()); + case "openVpnSettings" -> result.success(openVpnSettings()); + case "getTvInputs" -> result.success(getTvInputs()); + case "launchTvInput" -> result.success(launchTvInput(call.arguments())); + case "checkNotificationListenerPermission" -> result.success(checkNotificationListenerPermission()); + case "requestNotificationListenerPermission" -> result.success(requestNotificationListenerPermission()); + case "getActiveNotifications" -> result.success(getActiveNotifications()); + case "dismissNotification" -> { + String key = call.argument("key"); + result.success(dismissNotification(key)); + } + case "dismissAllNotifications" -> result.success(dismissAllNotifications()); + case "checkOverlayPermission" -> result.success(checkOverlayPermission()); + case "requestOverlayPermission" -> result.success(requestOverlayPermission()); + case "checkAccessibilityPermission" -> result.success(isAccessibilityServiceEnabled()); + case "requestAccessibilityPermission" -> result.success(openAccessibilitySettings()); + case "checkWatchNextPermission" -> result.success(checkWatchNextPermission()); + case "requestWatchNextPermission" -> { + if (checkWatchNextPermission()) { + result.success(true); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (pendingPermissionResult != null) { + result.error("ALREADY_REQUESTING", "A permission request is already in progress", null); + } else { + pendingPermissionResult = result; + requestPermissions(new String[]{"android.permission.READ_TV_LISTINGS"}, 1002); + } + } else { + result.success(true); + } + } + case "getWatchNextPrograms" -> result.success(getWatchNextPrograms()); + case "getWatchNextPoster" -> { + String posterArtUri = call.argument("posterArtUri"); + result.success(getWatchNextPoster(posterArtUri)); + } + case "launchWatchNextProgram" -> { + String intentUri = call.argument("intentUri"); + result.success(launchWatchNextProgram(intentUri)); + } + case "getPackageName" -> result.success(getPackageName()); + case "playClickSound" -> { + getWindow().getDecorView().playSoundEffect(android.view.SoundEffectConstants.CLICK); + result.success(null); + } default -> throw new IllegalArgumentException(); } }); @@ -128,6 +187,36 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { new EventChannel(messenger, NETWORK_EVENT_CHANNEL).setStreamHandler( new NetworkEventStreamHandler(this)); + + new EventChannel(messenger, NOTIFICATIONS_EVENT_CHANNEL).setStreamHandler( + new EventChannel.StreamHandler() { + private LauncherNotificationListenerService.NotificationListener listener; + + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + listener = () -> { + runOnUiThread(() -> { + try { + events.success(getActiveNotifications()); + } catch (Exception e) { + e.printStackTrace(); + } + }); + }; + LauncherNotificationListenerService.registerListener(listener); + // Send current state immediately + listener.onNotificationChanged(); + } + + @Override + public void onCancel(Object arguments) { + if (listener != null) { + LauncherNotificationListenerService.unregisterListener(listener); + listener = null; + } + } + } + ); } private List> getApplications() { @@ -456,7 +545,7 @@ private long getDailyDataUsage() { try { NetworkStats.Bucket wifiBucket = networkStatsManager.querySummaryForDevice( ConnectivityManager.TYPE_WIFI, - "", + null, startTime, endTime); totalBytes += wifiBucket.getRxBytes() + wifiBucket.getTxBytes(); @@ -467,7 +556,7 @@ private long getDailyDataUsage() { try { NetworkStats.Bucket mobileBucket = networkStatsManager.querySummaryForDevice( ConnectivityManager.TYPE_MOBILE, - "", + null, startTime, endTime); totalBytes += mobileBucket.getRxBytes() + mobileBucket.getTxBytes(); @@ -478,7 +567,7 @@ private long getDailyDataUsage() { try { NetworkStats.Bucket ethernetBucket = networkStatsManager.querySummaryForDevice( ConnectivityManager.TYPE_ETHERNET, - "", + null, startTime, endTime); totalBytes += ethernetBucket.getRxBytes() + ethernetBucket.getTxBytes(); @@ -511,7 +600,7 @@ private long getWeeklyDataUsage() { try { NetworkStats.Bucket wifiBucket = networkStatsManager.querySummaryForDevice( ConnectivityManager.TYPE_WIFI, - "", + null, startTime, endTime); totalBytes += wifiBucket.getRxBytes() + wifiBucket.getTxBytes(); @@ -522,7 +611,7 @@ private long getWeeklyDataUsage() { try { NetworkStats.Bucket mobileBucket = networkStatsManager.querySummaryForDevice( ConnectivityManager.TYPE_MOBILE, - "", + null, startTime, endTime); totalBytes += mobileBucket.getRxBytes() + mobileBucket.getTxBytes(); @@ -533,7 +622,7 @@ private long getWeeklyDataUsage() { try { NetworkStats.Bucket ethernetBucket = networkStatsManager.querySummaryForDevice( ConnectivityManager.TYPE_ETHERNET, - "", + null, startTime, endTime); totalBytes += ethernetBucket.getRxBytes() + ethernetBucket.getTxBytes(); @@ -566,7 +655,7 @@ private long getMonthlyDataUsage() { try { NetworkStats.Bucket wifiBucket = networkStatsManager.querySummaryForDevice( ConnectivityManager.TYPE_WIFI, - "", + null, startTime, endTime); totalBytes += wifiBucket.getRxBytes() + wifiBucket.getTxBytes(); @@ -577,7 +666,7 @@ private long getMonthlyDataUsage() { try { NetworkStats.Bucket mobileBucket = networkStatsManager.querySummaryForDevice( ConnectivityManager.TYPE_MOBILE, - "", + null, startTime, endTime); totalBytes += mobileBucket.getRxBytes() + mobileBucket.getTxBytes(); @@ -588,7 +677,7 @@ private long getMonthlyDataUsage() { try { NetworkStats.Bucket ethernetBucket = networkStatsManager.querySummaryForDevice( ConnectivityManager.TYPE_ETHERNET, - "", + null, startTime, endTime); totalBytes += ethernetBucket.getRxBytes() + ethernetBucket.getTxBytes(); @@ -601,9 +690,35 @@ private long getMonthlyDataUsage() { private boolean checkUsageStatsPermission() { AppOpsManager appOps = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); - int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, - android.os.Process.myUid(), getPackageName()); - return mode == AppOpsManager.MODE_ALLOWED; + int mode; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + mode = appOps.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, + android.os.Process.myUid(), getPackageName()); + } else { + mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, + android.os.Process.myUid(), getPackageName()); + } + + if (mode == AppOpsManager.MODE_ALLOWED) { + return true; + } + + if (mode == AppOpsManager.MODE_DEFAULT) { + try { + NetworkStatsManager networkStatsManager = (NetworkStatsManager) getSystemService(Context.NETWORK_STATS_SERVICE); + if (networkStatsManager != null) { + long now = System.currentTimeMillis(); + networkStatsManager.querySummaryForDevice(ConnectivityManager.TYPE_WIFI, null, now - 1, now); + return true; + } + } catch (SecurityException e) { + return false; + } catch (Exception e) { + return true; + } + } + + return false; } private void requestUsageStatsPermission() { @@ -611,6 +726,78 @@ private void requestUsageStatsPermission() { tryStartActivity(intent); } + private boolean checkWriteSettingsPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return Settings.System.canWrite(this); + } + return true; + } + + private boolean requestWriteSettingsPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, + Uri.parse("package:" + getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (tryStartActivity(intent)) { + return true; + } + } catch (Exception ignored) {} + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (tryStartActivity(intent)) { + return true; + } + } catch (Exception ignored) {} + try { + Intent intent = new Intent(Settings.ACTION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return tryStartActivity(intent); + } catch (Exception ignored) {} + return false; + } + return true; + } + + private boolean setSystemBrightness(int brightness) { + if (checkWriteSettingsPermission()) { + try { + android.content.ContentResolver resolver = getContentResolver(); + // 1. Standard Android brightness + Settings.System.putInt(resolver, Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL); + Settings.System.putInt(resolver, Settings.System.SCREEN_BRIGHTNESS, brightness); + + // 2. Try common TV "Backlight" keys (Vendor specific) + Settings.System.putInt(resolver, "backlight", brightness); + Settings.System.putInt(resolver, "backlight_level", brightness); + + return true; + } catch (Exception e) { + // Ignore errors on specific keys as they may not exist + return true; + } + } + return false; + } + + private boolean openDefaultLauncherSettings() { + // 1. Try Android TV home settings + Intent homeIntent = new Intent(Settings.ACTION_HOME_SETTINGS); + if (tryStartActivity(homeIntent)) { + return true; + } + + // 2. Try manage default apps settings + Intent defaultAppsIntent = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS); + if (tryStartActivity(defaultAppsIntent)) { + return true; + } + + // 3. Fallback to main settings + return launchActivityFromAction(Settings.ACTION_SETTINGS); + } + private boolean openWifiSettings() { // 1. Try Android Q+ WiFi panel if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -636,6 +823,23 @@ private boolean openWifiSettings() { return launchActivityFromAction(Settings.ACTION_SETTINGS); } + private boolean openVpnSettings() { + // 1. Try standard VPN settings + Intent vpnIntent = new Intent(Settings.ACTION_VPN_SETTINGS); + if (tryStartActivity(vpnIntent)) { + return true; + } + + // 2. Fallback to general wireless settings + Intent wirelessIntent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); + if (tryStartActivity(wirelessIntent)) { + return true; + } + + // 3. Final fallback - open main settings + return launchActivityFromAction(Settings.ACTION_SETTINGS); + } + private boolean openScreensaverSettings() { // 1. Try Android TV specific screensaver settings (DaydreamActivity - from // Aerial Views) @@ -663,4 +867,328 @@ private boolean openScreensaverSettings() { return launchActivityFromAction(Settings.ACTION_SETTINGS); } + private List> getTvInputs() { + List> result = new ArrayList<>(); + try { + TvInputManager manager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); + if (manager != null) { + List inputs = manager.getTvInputList(); + for (TvInputInfo input : inputs) { + if (input.isPassthroughInput()) { + Map map = new HashMap<>(); + map.put("id", input.getId()); + CharSequence label = input.loadLabel(this); + map.put("label", label != null ? label.toString() : input.getId()); + map.put("type", input.getType()); + result.add(map); + } + } + } + } catch (Exception e) { + // TIF might not be supported or initialized on emulator + } + return result; + } + + private boolean launchTvInput(String inputId) { + try { + Uri uri = TvContract.buildChannelUriForPassthroughInput(inputId); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(uri); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return tryStartActivity(intent); + } catch (Exception e) { + return false; + } + } + + private boolean checkNotificationListenerPermission() { + String packageName = getPackageName(); + String flat = Settings.Secure.getString(getContentResolver(), "enabled_notification_listeners"); + if (flat != null) { + String[] names = flat.split(":"); + for (String name : names) { + ComponentName cn = ComponentName.unflattenFromString(name); + if (cn != null && cn.getPackageName().equals(packageName)) { + return true; + } + } + } + return false; + } + + private boolean requestNotificationListenerPermission() { + try { + Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } catch (Exception e) { + e.printStackTrace(); + try { + Intent intent = new Intent(Settings.ACTION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } catch (Exception ex) { + ex.printStackTrace(); + return false; + } + } + } + + private List> getActiveNotifications() { + List> list = new ArrayList<>(); + LauncherNotificationListenerService service = LauncherNotificationListenerService.getInstance(); + if (service == null) { + return list; + } + try { + StatusBarNotification[] sbns = service.getActiveNotifications(); + if (sbns != null) { + for (StatusBarNotification sbn : sbns) { + Map map = new HashMap<>(); + map.put("key", sbn.getKey()); + map.put("packageName", sbn.getPackageName()); + + android.app.Notification notification = sbn.getNotification(); + String title = ""; + String text = ""; + if (notification != null && notification.extras != null) { + CharSequence titleChar = notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE); + CharSequence textChar = notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT); + if (titleChar != null) title = titleChar.toString(); + if (textChar != null) text = textChar.toString(); + } + map.put("title", title); + map.put("text", text); + map.put("isClearable", sbn.isClearable()); + list.add(map); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return list; + } + + private boolean dismissNotification(String key) { + LauncherNotificationListenerService service = LauncherNotificationListenerService.getInstance(); + if (service == null || key == null) { + return false; + } + try { + service.cancelNotification(key); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private boolean dismissAllNotifications() { + LauncherNotificationListenerService service = LauncherNotificationListenerService.getInstance(); + if (service == null) { + return false; + } + try { + service.cancelAllNotifications(); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private boolean checkOverlayPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return Settings.canDrawOverlays(this); + } + return true; + } + + private boolean requestOverlayPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:" + getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } catch (Exception e) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } catch (Exception ex) { + ex.printStackTrace(); + return false; + } + } + } + return true; + } + + private static String cursorStringOrEmpty(android.database.Cursor cursor, String column) { + String val = cursor.getString(cursor.getColumnIndexOrThrow(column)); + return val != null ? val : ""; + } + + private List> getWatchNextPrograms() { + List> list = new ArrayList<>(); + try { + String[] projection = { + TvContract.WatchNextPrograms._ID, + TvContract.WatchNextPrograms.COLUMN_PACKAGE_NAME, + TvContract.WatchNextPrograms.COLUMN_TITLE, + TvContract.WatchNextPrograms.COLUMN_SHORT_DESCRIPTION, + TvContract.WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE, + TvContract.WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS, + TvContract.WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS, + TvContract.WatchNextPrograms.COLUMN_DURATION_MILLIS, + TvContract.WatchNextPrograms.COLUMN_INTENT_URI, + TvContract.WatchNextPrograms.COLUMN_POSTER_ART_URI + }; + + android.database.Cursor cursor = getContentResolver().query( + TvContract.WatchNextPrograms.CONTENT_URI, + projection, + null, + null, + TvContract.WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS + " DESC LIMIT 20" + ); + + if (cursor != null) { + while (cursor.moveToNext()) { + Map map = new HashMap<>(); + map.put("id", cursor.getLong(cursor.getColumnIndexOrThrow(TvContract.WatchNextPrograms._ID))); + map.put("packageName", cursorStringOrEmpty(cursor, TvContract.WatchNextPrograms.COLUMN_PACKAGE_NAME)); + map.put("title", cursorStringOrEmpty(cursor, TvContract.WatchNextPrograms.COLUMN_TITLE)); + map.put("description", cursorStringOrEmpty(cursor, TvContract.WatchNextPrograms.COLUMN_SHORT_DESCRIPTION)); + map.put("watchNextType", cursor.getInt(cursor.getColumnIndexOrThrow(TvContract.WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE))); + map.put("lastEngagementTime", cursor.getLong(cursor.getColumnIndexOrThrow(TvContract.WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS))); + map.put("playbackPosition", cursor.getLong(cursor.getColumnIndexOrThrow(TvContract.WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS))); + map.put("duration", cursor.getLong(cursor.getColumnIndexOrThrow(TvContract.WatchNextPrograms.COLUMN_DURATION_MILLIS))); + map.put("intentUri", cursorStringOrEmpty(cursor, TvContract.WatchNextPrograms.COLUMN_INTENT_URI)); + map.put("posterArtUri", cursorStringOrEmpty(cursor, TvContract.WatchNextPrograms.COLUMN_POSTER_ART_URI)); + list.add(map); + } + cursor.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return list; + } + + private byte[] getWatchNextPoster(String posterArtUri) { + if (posterArtUri == null || posterArtUri.isEmpty()) { + return null; + } + try { + Uri uri = Uri.parse(posterArtUri); + try (java.io.InputStream inputStream = getContentResolver().openInputStream(uri)) { + if (inputStream != null) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + return outputStream.toByteArray(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private boolean launchWatchNextProgram(String intentUri) { + if (intentUri == null || intentUri.isEmpty()) { + return false; + } + // Security: only allow safe URI schemes + String lower = intentUri.toLowerCase(); + if (!lower.startsWith("intent://") && !lower.startsWith("https://") && + !lower.startsWith("http://") && !lower.startsWith("content://")) { + return false; + } + try { + Intent intent = Intent.parseUri(intentUri, Intent.URI_INTENT_SCHEME); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private boolean isAccessibilityServiceEnabled() { + String service = getPackageName() + "/" + LauncherAccessibilityService.class.getName(); + int accessibilityEnabled = 0; + try { + accessibilityEnabled = Settings.Secure.getInt( + getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED + ); + } catch (Settings.SettingNotFoundException e) { + e.printStackTrace(); + } + + if (accessibilityEnabled == 1) { + String settingValue = Settings.Secure.getString( + getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES + ); + if (settingValue != null) { + String[] services = settingValue.split(":"); + for (String s : services) { + if (s.equalsIgnoreCase(service)) { + return true; + } + } + } + } + return false; + } + + private boolean openAccessibilitySettings() { + try { + Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (tryStartActivity(intent)) { + return true; + } + } catch (Exception ignored) {} + try { + Intent intent = new Intent(Settings.ACTION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return tryStartActivity(intent); + } catch (Exception ignored) {} + return false; + } + + private boolean checkWatchNextPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return checkSelfPermission("android.permission.READ_TV_LISTINGS") == PackageManager.PERMISSION_GRANTED; + } + return true; + } + + + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == 1002) { + if (pendingPermissionResult != null) { + boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; + pendingPermissionResult.success(granted); + pendingPermissionResult = null; + } + } + } } diff --git a/android/app/src/main/java/me/efesser/flauncher/NetworkUtils.java b/android/app/src/main/java/me/efesser/flauncher/NetworkUtils.java index e0dfc1b3..85f940f9 100644 --- a/android/app/src/main/java/me/efesser/flauncher/NetworkUtils.java +++ b/android/app/src/main/java/me/efesser/flauncher/NetworkUtils.java @@ -30,6 +30,7 @@ public class NetworkUtils public static final String KEY_NETWORK_ACCESS = "networkAccess"; public static final String KEY_NETWORK_TYPE = "networkType"; public static final String KEY_WIRELESS_SIGNAL_LEVEL = "wirelessSignalLevel"; + public static final String KEY_VPN_ACTIVE = "vpnActive"; public static Map getNetworkCapabilitiesInformation(Context context, NetworkCapabilities capabilities) { @@ -45,7 +46,10 @@ public static Map getNetworkCapabilitiesInformation(Context cont hasInternetAccess = hasNetworkAccess; } - if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + networkType = NETWORK_TYPE_VPN; + } + else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { networkType = NETWORK_TYPE_CELLULAR; } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { @@ -73,9 +77,6 @@ else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { networkType = NETWORK_TYPE_WIFI; } - else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { - networkType = NETWORK_TYPE_VPN; - } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { networkType = NETWORK_TYPE_WIRED; } @@ -90,28 +91,55 @@ else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { public static Map getNetworkInformation(Context context, Network network) { - Map map = null; - int wirelessNetworkSignalLevel = 0; + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + boolean isVpnActive = false; + NetworkCapabilities physicalCaps = null; if (network != null) { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); + NetworkCapabilities activeCaps = connectivityManager.getNetworkCapabilities(network); + if (activeCaps != null) { + if (activeCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + isVpnActive = true; + } else { + physicalCaps = activeCaps; + } + } + } + + if (isVpnActive || physicalCaps == null) { + for (Network net : connectivityManager.getAllNetworks()) { + NetworkCapabilities caps = connectivityManager.getNetworkCapabilities(net); + if (caps != null) { + if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + isVpnActive = true; + } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) || + caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + if (physicalCaps == null) { + physicalCaps = caps; + } + } + } + } + } - if (capabilities != null) { - map = getNetworkCapabilitiesInformation(context, capabilities); + Map map = null; + int wirelessNetworkSignalLevel = 0; - if (Objects.equals(map.get(KEY_NETWORK_TYPE), NETWORK_TYPE_WIFI)) { - try { - WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - if (wifiManager != null) { - WifiInfo wifiInfo = wifiManager.getConnectionInfo(); - if (wifiInfo != null) { - wirelessNetworkSignalLevel = getWifiSignalLevel(wifiInfo); - } + if (physicalCaps != null) { + map = getNetworkCapabilitiesInformation(context, physicalCaps); + + if (Objects.equals(map.get(KEY_NETWORK_TYPE), NETWORK_TYPE_WIFI)) { + try { + WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + if (wifiManager != null) { + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo != null) { + wirelessNetworkSignalLevel = getWifiSignalLevel(wifiInfo); } - } catch (Exception e) { - e.printStackTrace(); } + } catch (Exception e) { + e.printStackTrace(); } } } @@ -127,6 +155,7 @@ public static Map getNetworkInformation(Context context, Network } map.put(KEY_WIRELESS_SIGNAL_LEVEL, wirelessNetworkSignalLevel); + map.put(KEY_VPN_ACTIVE, isVpnActive); return map; } @@ -135,6 +164,16 @@ public static Map getNetworkInformation(Context context, @Nullab { boolean hasNetworkAccess = false; int networkType = NETWORK_TYPE_UNKNOWN, networkInfoType, wirelessSignalLevel = 0; + boolean isVpnActive = false; + + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager != null) { + // noinspection deprecation + NetworkInfo vpnInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_VPN); + if (vpnInfo != null && vpnInfo.isConnected()) { + isVpnActive = true; + } + } if (networkInfo != null) { hasNetworkAccess = networkInfo.isConnected(); @@ -143,7 +182,7 @@ public static Map getNetworkInformation(Context context, @Nullab if (networkInfoType == ConnectivityManager.TYPE_MOBILE) { networkType = NETWORK_TYPE_CELLULAR; } - if (networkInfoType == ConnectivityManager.TYPE_WIFI) { + else if (networkInfoType == ConnectivityManager.TYPE_WIFI) { WifiManager wifiManager = (WifiManager) context .getApplicationContext().getSystemService(Context.WIFI_SERVICE); @@ -160,19 +199,45 @@ public static Map getNetworkInformation(Context context, @Nullab } } else if (networkInfoType == ConnectivityManager.TYPE_VPN) { - networkType = NETWORK_TYPE_VPN; + isVpnActive = true; + // noinspection deprecation + NetworkInfo wifiInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + // noinspection deprecation + NetworkInfo mobileInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); + // noinspection deprecation + NetworkInfo ethernetInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_ETHERNET); + + if (wifiInfo != null && wifiInfo.isConnected()) { + networkType = NETWORK_TYPE_WIFI; + WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + try { + if (wifiManager != null) { + WifiInfo wInfo = wifiManager.getConnectionInfo(); + if (wInfo != null) { + wirelessSignalLevel = getWifiSignalLevel(wInfo); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } else if (ethernetInfo != null && ethernetInfo.isConnected()) { + networkType = NETWORK_TYPE_WIRED; + } else if (mobileInfo != null && mobileInfo.isConnected()) { + networkType = NETWORK_TYPE_CELLULAR; + } } else if (networkInfoType == ConnectivityManager.TYPE_ETHERNET) { networkType = NETWORK_TYPE_WIRED; } } - Map map = new java.util.HashMap<>(); - map.put(KEY_NETWORK_TYPE, networkType); - map.put(KEY_NETWORK_ACCESS, hasNetworkAccess); - map.put(KEY_INTERNET_ACCESS, hasNetworkAccess); - map.put(KEY_WIRELESS_SIGNAL_LEVEL, wirelessSignalLevel); - return map; + Map mapOut = new java.util.HashMap<>(); + mapOut.put(KEY_NETWORK_TYPE, networkType); + mapOut.put(KEY_NETWORK_ACCESS, hasNetworkAccess); + mapOut.put(KEY_INTERNET_ACCESS, hasNetworkAccess); + mapOut.put(KEY_WIRELESS_SIGNAL_LEVEL, wirelessSignalLevel); + mapOut.put(KEY_VPN_ACTIVE, isVpnActive); + return mapOut; } public static int getWifiSignalLevel(WifiInfo wifiInfo) diff --git a/android/app/src/main/res/xml/accessibility_service_config.xml b/android/app/src/main/res/xml/accessibility_service_config.xml new file mode 100644 index 00000000..53e1997d --- /dev/null +++ b/android/app/src/main/res/xml/accessibility_service_config.xml @@ -0,0 +1,5 @@ + + diff --git a/android/gradle.properties b/android/gradle.properties index 58e8f376..ed2180d1 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -24,3 +24,4 @@ org.gradle.daemon=true android.useAndroidX=true android.nonTransitiveRClass=false android.nonFinalResIds=false +android.enableR8.fullMode=true diff --git a/assets/icon.png b/assets/icon.png index 62120671..39ca53c8 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/commitlint.config.js b/commitlint.config.js deleted file mode 100644 index 8f69fc09..00000000 --- a/commitlint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - extends: ['@commitlint/config-conventional'], - rules: { - 'type-enum': [ - 2, - 'always', - [ - 'build', - 'ci', - 'chore', - 'docs', - 'feat', - 'fix', - 'perf', - 'refactor', - 'revert', - 'style', - 'test', - 'improvement' - ] - ] - } -}; diff --git a/drift_schemas/drift_schema_v8.json b/drift_schemas/drift_schema_v8.json new file mode 100644 index 00000000..70b4a2c1 --- /dev/null +++ b/drift_schemas/drift_schema_v8.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.1.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"apps","was_declared_in_moor":false,"columns":[{"name":"package_name","getter_name":"packageName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"version","getter_name":"version","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hidden","getter_name":"hidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"hidden\" IN (0, 1))","default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"last_launched_at","getter_name":"lastLaunchedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["package_name"]}},{"id":1,"references":[],"type":"table","data":{"name":"categories","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"sort","getter_name":"sort","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(CategorySort.values)","dart_type_name":"CategorySort"}},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(CategoryType.values)","dart_type_name":"CategoryType"}},{"name":"row_height","getter_name":"rowHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(110)","default_client_dart":null,"dsl_features":[]},{"name":"columns_count","getter_name":"columnsCount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(6)","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[1,0],"type":"table","data":{"name":"apps_categories","was_declared_in_moor":false,"columns":[{"name":"category_id","getter_name":"categoryId","moor_type":"int","nullable":false,"customConstraints":"REFERENCES categories(id) ON DELETE CASCADE","default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"app_package_name","getter_name":"appPackageName","moor_type":"string","nullable":false,"customConstraints":"REFERENCES apps(package_name) ON DELETE CASCADE","default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["category_id","app_package_name"]}},{"id":3,"references":[],"type":"table","data":{"name":"launcher_spacers","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"height","getter_name":"height","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]} \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/8103.txt b/fastlane/metadata/android/en-US/changelogs/8103.txt new file mode 100644 index 00000000..48260624 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/8103.txt @@ -0,0 +1,16 @@ +- Added system Accessibility Settings page with support for viewing default status and opening the system home picker. +- Performance: Capped background icon pre-cache concurrency via pool to prevent Platform channel congestion. +- Performance: Optimized SettingsService with local caching to reduce synchronous disk I/O overhead. +- Performance: Fixed N+1 Drift database query during app categorization, and optimized new app category lookups to O(1). +- Bug Fix: Capped focused card scaling factor to prevent premium banners from being cropped by sibling cards. +- Bug Fix: Fixed ethernet data usage reporting, and fixed wallpaper not immediately updating after settings change. +- Security: Disabled insecure Android auto-backups. +- Testing: Substantially increased unit and widget test coverage across models, widgets, and services. +- Added system-wide floating notification overlay popups (requires Android overlay permission). +- Added "System-wide Popup Alert" settings switch toggle under Notification Access. +- Added premium "Get it on Downloader" badge to the README supporting permanent numeric code 7259827. +- Add support for 'Continue Watching' (Watch Next) row on home screen. +- Add status bar settings toggle to show/hide the notification bell icon. +- Improve system-wide popup alert permission status detection on app resume. +- Add visible focus outline selector for notification dismissal button. +- Add a focusable 'Clear All' button at the bottom of the notifications side panel. diff --git a/fastlane/metadata/android/en-US/changelogs/8104.txt b/fastlane/metadata/android/en-US/changelogs/8104.txt new file mode 100644 index 00000000..bf111716 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/8104.txt @@ -0,0 +1,5 @@ +- Fix key click sound on D-pad key press navigation. +- Fix local cache out-of-sync issue when reordering apps in custom sections. +- Fix active VPN connection detection to correctly display the VPN key icon instead of Wi-Fi. +- Make tapping the VPN status bar icon open native VPN settings instead of Wi-Fi settings. +- Consolidate GitHub Actions badge generation workflows to prevent build push conflicts. diff --git a/fastlane/metadata/android/en-US/changelogs/8105.txt b/fastlane/metadata/android/en-US/changelogs/8105.txt new file mode 100644 index 00000000..b04e6edb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/8105.txt @@ -0,0 +1,7 @@ +- Improve Backup & Restore performance with batch inserts. +- Fix bug where unhiding apps required a launcher restart. +- Fix race condition where rapid reordering of apps caused duplicate icons or stuck states. +- Support using the Back button (and gamepad B button) to exit reorder mode. +- Auto-hide empty notification bell icon (optional setting). +- Force notification badge count text color to white. +- Fix bugs and visually polish Continue Watching. diff --git a/fastlane/metadata/android/en-US/changelogs/8106.txt b/fastlane/metadata/android/en-US/changelogs/8106.txt new file mode 100644 index 00000000..3e6b2917 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/8106.txt @@ -0,0 +1,7 @@ +- Render both physical connection (WiFi/Ethernet/Cellular) and VPN status icons simultaneously. +- Fix PACKAGE_USAGE_STATS permission check fallback (ADB/system settings default AppOps status). +- Fix reorder mode race condition and duplicate move-icon glitches during rapid app reordering. +- Fix back button handling in reorder mode to prevent home screensaver from launching on exit. +- Fix D-pad navigation scrolling for Sections settings list with more than 6 sections. +- Add Spanish translations for "Continue Watching" panel settings. +- Skip Gradle release signing configuration if keystore file is absent. diff --git a/l10n.yaml b/l10n.yaml index 4e6692e5..d5dd1e46 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,3 +1,4 @@ arb-dir: lib/l10n template-arb-file: app_en.arb -output-localization-file: app_localizations.dart \ No newline at end of file +output-localization-file: app_localizations.dart +synthetic-package: false \ No newline at end of file diff --git a/lib/actions.dart b/lib/actions.dart index 19e4d6f5..dc81c4ca 100644 --- a/lib/actions.dart +++ b/lib/actions.dart @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import 'package:flauncher/flauncher_channel.dart'; import 'package:flauncher/providers/apps_service.dart'; import 'package:flauncher/providers/launcher_state.dart'; import 'package:flauncher/providers/settings_service.dart'; @@ -34,6 +35,7 @@ class SoundFeedbackDirectionalFocusAction extends DirectionalFocusAction { SettingsService settingsService = context.read(); if (settingsService.appKeyClickEnabled) { + FLauncherChannel().playClickSound(); Feedback.forTap(context); } else { silentForTap(context); diff --git a/lib/database.dart b/lib/database.dart index 4fa2b40b..7ff72dab 100644 --- a/lib/database.dart +++ b/lib/database.dart @@ -53,13 +53,13 @@ class Categories extends Table TextColumn get name => text()(); - IntColumn get sort => intEnum().withDefault(Constant(Category.Sort.index))(); + IntColumn get sort => intEnum().withDefault(const Constant(0))(); - IntColumn get type => intEnum().withDefault(Constant(Category.Type.index))(); + IntColumn get type => intEnum().withDefault(const Constant(0))(); - IntColumn get rowHeight => integer().withDefault(const Constant(Category.RowHeight))(); + IntColumn get rowHeight => integer().withDefault(const Constant(110))(); - IntColumn get columnsCount => integer().withDefault(const Constant(Category.ColumnsCount))(); + IntColumn get columnsCount => integer().withDefault(const Constant(6))(); IntColumn get order => integer()(); } @@ -105,13 +105,19 @@ class FLauncherDatabase extends _$FLauncherDatabase await migrator.createAll(); }, onUpgrade: (migrator, from, to) async { - if (from <= 1) { - await migrator.alterTable(TableMigration(apps, newColumns: [apps.hidden])); + if (from <= 1 && to >= 2) { + await migrator.alterTable(TableMigration( + apps, + newColumns: [ + apps.hidden, + if (to >= 8) apps.lastLaunchedAt, + ], + )); } - if (from <= 2 && from != 1) { + if (from <= 2 && from != 1 && to >= 3) { await migrator.addColumn(apps, apps.hidden); } - if (from <= 3) { + if (from <= 3 && to >= 4) { await migrator.addColumn(categories, categories.sort); await migrator.addColumn(categories, categories.type); await migrator.addColumn(categories, categories.rowHeight); @@ -119,15 +125,26 @@ class FLauncherDatabase extends _$FLauncherDatabase await (update(categories)..where((tbl) => tbl.name.equals("Applications"))) .write(const CategoriesCompanion(type: Value(CategoryType.grid))); } - if (from < 6) { - await customStatement("ALTER TABLE apps DROP COLUMN banner;"); - await customStatement("ALTER TABLE apps DROP COLUMN icon;"); + if (from < 6 && to >= 6) { + final columns = await customSelect("PRAGMA table_info(apps);").get(); + final hasBanner = columns.any((row) => row.read('name') == 'banner'); + final hasIcon = columns.any((row) => row.read('name') == 'icon'); + if (hasBanner) { + await customStatement("ALTER TABLE apps DROP COLUMN banner;"); + } + if (hasIcon) { + await customStatement("ALTER TABLE apps DROP COLUMN icon;"); + } } - if (from < 7) { + if (from < 7 && to >= 7) { await migrator.createTable(launcherSpacers); - await migrator.dropColumn(apps, "sideloaded"); + final columns = await customSelect("PRAGMA table_info(apps);").get(); + final hasSideloaded = columns.any((row) => row.read('name') == 'sideloaded'); + if (hasSideloaded) { + await migrator.dropColumn(apps, "sideloaded"); + } } - if (from < 8) { + if (from < 8 && from != 1 && to >= 8) { await migrator.addColumn(apps, apps.lastLaunchedAt); } }, diff --git a/lib/database.drift.dart b/lib/database.drift.dart index 1dbc0bde..80cd00ff 100644 --- a/lib/database.drift.dart +++ b/lib/database.drift.dart @@ -231,7 +231,7 @@ class $CategoriesTable extends Categories GeneratedColumn('sort', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: false, - defaultValue: Constant(Category.Sort.index)) + defaultValue: const Constant(0)) .withConverter($CategoriesTable.$convertersort); static const VerificationMeta _typeMeta = const VerificationMeta('type'); @override @@ -239,7 +239,7 @@ class $CategoriesTable extends Categories GeneratedColumn('type', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: false, - defaultValue: Constant(Category.Type.index)) + defaultValue: const Constant(0)) .withConverter($CategoriesTable.$convertertype); static const VerificationMeta _rowHeightMeta = const VerificationMeta('rowHeight'); @@ -248,7 +248,7 @@ class $CategoriesTable extends Categories 'row_height', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: false, - defaultValue: const Constant(Category.RowHeight)); + defaultValue: const Constant(110)); static const VerificationMeta _columnsCountMeta = const VerificationMeta('columnsCount'); @override @@ -256,7 +256,7 @@ class $CategoriesTable extends Categories 'columns_count', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: false, - defaultValue: const Constant(Category.ColumnsCount)); + defaultValue: const Constant(6)); static const VerificationMeta _orderMeta = const VerificationMeta('order'); @override late final GeneratedColumn order = GeneratedColumn( diff --git a/lib/flauncher.dart b/lib/flauncher.dart index ed38470c..765c9451 100644 --- a/lib/flauncher.dart +++ b/lib/flauncher.dart @@ -28,6 +28,9 @@ import 'package:flauncher/widgets/launcher_alternative_view.dart'; import 'package:flauncher/widgets/focus_aware_app_bar.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:flauncher/widgets/continue_watching_row.dart'; +import 'package:flauncher/providers/watch_next_service.dart'; +import 'package:flauncher/providers/settings_service.dart'; import 'package:flauncher/l10n/app_localizations.dart'; import 'models/category.dart'; @@ -74,7 +77,15 @@ class _FLauncherState extends State { child: Consumer( builder: (context, appsService, _) { if (appsService.initialized) { - return SingleChildScrollView(child: _sections(appsService.launcherSections)); + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ContinueWatchingRow(), + _sections(appsService.launcherSections), + ], + ), + ); } else { return _emptyState(context); @@ -90,8 +101,12 @@ class _FLauncherState extends State { ); Widget _sections(List sections) { + final settingsService = Provider.of(context, listen: false); + final watchNextService = Provider.of(context, listen: false); + final bool continueWatchingActive = settingsService.showContinueWatching && watchNextService.programs.isNotEmpty; + List children = []; - bool firstCategoryFound = false; + bool firstCategoryFound = continueWatchingActive; for (var section in sections) { final Key sectionKey = Key(section.id.toString()); diff --git a/lib/flauncher_app.dart b/lib/flauncher_app.dart index 520c7cc4..8d75ff3d 100644 --- a/lib/flauncher_app.dart +++ b/lib/flauncher_app.dart @@ -36,19 +36,6 @@ class FLauncherApp extends StatelessWidget BackIntent() ]); - static const MaterialColor _swatch = MaterialColor(0xFF011526, { - 50: Color(0xFF36A0FA), - 100: Color(0xFF067BDE), - 200: Color(0xFF045CA7), - 300: Color(0xFF033662), - 400: Color(0xFF022544), - 500: Color(0xFF011526), - 600: Color(0xFF000508), - 700: Color(0xFF000000), - 800: Color(0xFF000000), - 900: Color(0xFF000000), - }); - const FLauncherApp(); @override diff --git a/lib/flauncher_channel.dart b/lib/flauncher_channel.dart index 31eaa13a..e2a2cfbe 100644 --- a/lib/flauncher_channel.dart +++ b/lib/flauncher_channel.dart @@ -24,6 +24,7 @@ class FLauncherChannel { static const _methodChannel = MethodChannel('me.efesser.flauncher/method'); static const _appsEventChannel = EventChannel('me.efesser.flauncher/event_apps'); static const _networkEventChannel = EventChannel('me.efesser.flauncher/event_network'); + static const _notificationsEventChannel = EventChannel('me.efesser.flauncher/event_notifications'); Future>> getApplications() async { List>? applications = await _methodChannel.invokeListMethod("getApplications"); @@ -96,6 +97,25 @@ class FLauncherChannel { Future openWifiSettings() async => await _methodChannel.invokeMethod("openWifiSettings"); + Future openVpnSettings() async { + try { + await _methodChannel.invokeMethod("openVpnSettings"); + } catch (_) { + // Ignore error in non-Android or test environments + } + } + + Future openDefaultLauncherSettings() async => + await _methodChannel.invokeMethod("openDefaultLauncherSettings"); + + Future playClickSound() async { + try { + await _methodChannel.invokeMethod("playClickSound"); + } catch (_) { + // Ignore error in non-Android or test environments + } + } + Future startAmbientMode() async => await _methodChannel.invokeMethod("startAmbientMode"); void addAppsChangedListener(void Function(Map) listener) => @@ -109,4 +129,124 @@ class FLauncherChannel { Map eventMap = event; listener(eventMap.cast()); }); + + Future>> getTvInputs() async { + try { + final List inputs = await _methodChannel.invokeMethod("getTvInputs"); + return inputs.cast>(); + } catch (_) { + return []; + } + } + + Future launchTvInput(String inputId) async { + try { + final bool success = await _methodChannel.invokeMethod("launchTvInput", inputId); + return success; + } catch (_) { + return false; + } + } + + Future checkNotificationListenerPermission() async => + await _methodChannel.invokeMethod("checkNotificationListenerPermission"); + + Future requestNotificationListenerPermission() async { + final bool? success = await _methodChannel.invokeMethod("requestNotificationListenerPermission"); + return success ?? false; + } + + Future checkOverlayPermission() async => + await _methodChannel.invokeMethod("checkOverlayPermission"); + + Future requestOverlayPermission() async { + final bool? success = await _methodChannel.invokeMethod("requestOverlayPermission"); + return success ?? false; + } + + Future checkAccessibilityPermission() async => + await _methodChannel.invokeMethod("checkAccessibilityPermission"); + + Future requestAccessibilityPermission() async { + final bool? success = await _methodChannel.invokeMethod("requestAccessibilityPermission"); + return success ?? false; + } + + Future>> getActiveNotifications() async { + try { + final List list = await _methodChannel.invokeMethod("getActiveNotifications"); + return list.cast>(); + } catch (_) { + return []; + } + } + + StreamSubscription addNotificationsChangedListener(void Function(List>) listener) => + _notificationsEventChannel.receiveBroadcastStream().listen((event) { + final List eventList = event; + listener(eventList.cast>()); + }); + + Future dismissNotification(String key) async { + try { + final bool success = await _methodChannel.invokeMethod("dismissNotification", {"key": key}); + return success; + } catch (_) { + return false; + } + } + + Future dismissAllNotifications() async { + try { + final bool success = await _methodChannel.invokeMethod("dismissAllNotifications"); + return success; + } catch (_) { + return false; + } + } + + Future>> getWatchNextPrograms() async { + try { + final List? list = await _methodChannel.invokeMethod("getWatchNextPrograms"); + return list?.cast>() ?? []; + } catch (_) { + return []; + } + } + + Future getWatchNextPoster(String posterArtUri) async { + try { + final Uint8List? bytes = await _methodChannel.invokeMethod("getWatchNextPoster", {"posterArtUri": posterArtUri}); + return bytes; + } catch (_) { + return null; + } + } + + Future launchWatchNextProgram(String intentUri) async { + try { + final bool success = await _methodChannel.invokeMethod("launchWatchNextProgram", {"intentUri": intentUri}); + return success; + } catch (_) { + return false; + } + } + + Future checkWatchNextPermission() async { + try { + final bool? allowed = await _methodChannel.invokeMethod("checkWatchNextPermission"); + return allowed ?? false; + } catch (_) { + return false; + } + } + + Future requestWatchNextPermission() async { + try { + final bool? allowed = await _methodChannel.invokeMethod("requestWatchNextPermission"); + return allowed ?? false; + } catch (_) { + return false; + } + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a7317b4f..522fc330 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -106,5 +106,60 @@ "withEllipsisAddTo": "Add to...", "timeBasedWallpaper": "Time based wallpaper", "pickDayWallpaper": "Pick day wallpaper", - "pickNightWallpaper": "Pick night wallpaper" + "pickNightWallpaper": "Pick night wallpaper", + "accessibility": "Accessibility", + "defaultLauncherIsDefault": "LTvLauncher is the default launcher", + "defaultLauncherNotDefault": "LTvLauncher is not the default launcher", + "setAsDefaultLauncher": "Set as default launcher", + "defaultLauncherDescription": "When set as the default launcher, the Home button will always return to LTvLauncher. The TV will also boot directly into LTvLauncher.", + "inputs": "Inputs", + "inputSources": "Input Sources", + "backupAndRestore": "Backup & Restore", + "exportBackup": "Export Backup", + "importBackup": "Import Backup", + "exportSuccess": "Backup exported successfully to {path}", + "@exportSuccess": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "importSuccess": "Backup imported successfully", + "importConfirm": "Are you sure you want to import the backup? This will overwrite your current settings and layout.", + "importError": "Failed to import backup: {error}", + "@importError": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "exportError": "Failed to export backup: {error}", + "@exportError": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "shareBackup": "Share Backup", + "shareBackupDescription": "Share backup with other devices on local network", + "stopSharing": "Stop Sharing", + "localNetworkSharingActive": "Local network sharing is active!", + "localNetworkSharingInstructions": "Connect another device to the same Wi-Fi network and open the following URL in a web browser:", + "localNetworkSharingDetails": "Here you can download your TV settings/layout or upload a backup file back to this TV.", + "failedToStartServer": "Failed to start sharing server: {error}", + "@failedToStartServer": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "notificationBell": "Notification Bell", + "autoHideNotificationBell": "Auto-hide Notification Bell", + "continueWatching": "Continue Watching", + "showContinueWatchingOnHome": "Show Continue Watching on Home", + "permissionDeniedContinueWatching": "Permission required to show Continue Watching" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 065302f1..45dcceda 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,5 +1,5 @@ { - "aboutFlauncher": "Acerca de FLauncher", + "aboutFlauncher": "Acerca de LTvLauncher", "addCategory": "Agregar categoría", "addSection": "Agregar sección", "alphabetical": "Alfabético", @@ -45,6 +45,7 @@ } }, "gradient": "Gradiente", + "favoriteApps": "Apps Favoritas", "grid": "Cuadrícula", "height": "Altura", "hide": "Ocultar", @@ -63,7 +64,7 @@ "open": "Abrir", "orSelectFormatSpecifiers": "O seleccione especificadores de formato", "picture": "Imagen", - "removeFrom": "Remover de {name}", + "removeFrom": "Eliminar de {name}", "@removeFrom": { "placeholders": { "name": { "type": "String" } @@ -71,8 +72,8 @@ }, "renameCategory": "Renombrar categoría", "reorder": "Reordenar", - "row": "Row", - "rowHeight": "Row height", + "row": "Fila", + "rowHeight": "Altura de fila", "save": "Guardar", "spacer": "Espaciador", "spacerMaxHeightRequirement": "Debe ser mayor a cero y menor o igual a 500", @@ -102,5 +103,63 @@ "typeInTheHourFormat": "Escriba el formato de hora", "uninstall": "Desinstalar", "wallpaper": "Fondo de pantalla", - "withEllipsisAddTo": "Añadir a..." -} \ No newline at end of file + "withEllipsisAddTo": "Añadir a...", + "timeBasedWallpaper": "Fondo de pantalla según hora", + "pickDayWallpaper": "Elegir fondo de pantalla diurno", + "pickNightWallpaper": "Elegir fondo de pantalla nocturno", + "accessibility": "Accesibilidad", + "defaultLauncherIsDefault": "LTvLauncher es el lanzador predeterminado", + "defaultLauncherNotDefault": "LTvLauncher no es el lanzador predeterminado", + "setAsDefaultLauncher": "Establecer como lanzador predeterminado", + "defaultLauncherDescription": "Cuando se establece como lanzador predeterminado, el botón de inicio siempre regresará a LTvLauncher. El TV también iniciará directamente en LTvLauncher.", + "inputs": "Entradas", + "inputSources": "Fuentes de Entrada", + "backupAndRestore": "Copia de seguridad y restauración", + "exportBackup": "Exportar copia de seguridad", + "importBackup": "Importar copia de seguridad", + "exportSuccess": "Copia de seguridad exportada con éxito a {path}", + "@exportSuccess": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "importSuccess": "Copia de seguridad importada con éxito", + "importConfirm": "¿Está seguro de que desea importar la copia de seguridad? Esto sobrescribirá su configuración y diseño actuales.", + "importError": "Error al importar la copia de seguridad: {error}", + "@importError": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "exportError": "Error al exportar la copia de seguridad: {error}", + "@exportError": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "shareBackup": "Compartir copia", + "shareBackupDescription": "Comparta la copia de seguridad con otros dispositivos en la red local", + "stopSharing": "Detener uso compartido", + "localNetworkSharingActive": "¡El uso compartido en red local está activo!", + "localNetworkSharingInstructions": "Conecte otro dispositivo a la misma red Wi-Fi y abra la siguiente URL en un navegador web:", + "localNetworkSharingDetails": "Aquí puede descargar la configuración/diseño de su TV o subir un archivo de copia de seguridad a esta TV.", + "failedToStartServer": "Error al iniciar el servidor de uso compartido: {error}", + "@failedToStartServer": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "notificationBell": "Campana de notificaciones", + "autoHideNotificationBell": "Ocultar campana de notificaciones automáticamente", + "continueWatching": "Continuar viendo", + "showContinueWatchingOnHome": "Mostrar Continuar viendo en Inicio", + "permissionDeniedContinueWatching": "Se requiere permiso para mostrar Continuar viendo" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bd1a1d9a..14e606ad 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -62,8 +62,7 @@ import 'app_localizations_es.dart'; /// be consistent with the languages listed in the AppLocalizations.supportedLocales /// property. abstract class AppLocalizations { - AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -71,8 +70,7 @@ abstract class AppLocalizations { return Localizations.of(context, AppLocalizations); } - static const LocalizationsDelegate delegate = - _AppLocalizationsDelegate(); + static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. @@ -84,8 +82,7 @@ abstract class AppLocalizations { /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. - static const List> localizationsDelegates = - >[ + static const List> localizationsDelegates = >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, @@ -547,10 +544,171 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Pick night wallpaper'** String get pickNightWallpaper; + + /// No description provided for @accessibility. + /// + /// In en, this message translates to: + /// **'Accessibility'** + String get accessibility; + + /// No description provided for @defaultLauncherIsDefault. + /// + /// In en, this message translates to: + /// **'LTvLauncher is the default launcher'** + String get defaultLauncherIsDefault; + + /// No description provided for @defaultLauncherNotDefault. + /// + /// In en, this message translates to: + /// **'LTvLauncher is not the default launcher'** + String get defaultLauncherNotDefault; + + /// No description provided for @setAsDefaultLauncher. + /// + /// In en, this message translates to: + /// **'Set as default launcher'** + String get setAsDefaultLauncher; + + /// No description provided for @defaultLauncherDescription. + /// + /// In en, this message translates to: + /// **'When set as the default launcher, the Home button will always return to LTvLauncher. The TV will also boot directly into LTvLauncher.'** + String get defaultLauncherDescription; + + /// No description provided for @inputs. + /// + /// In en, this message translates to: + /// **'Inputs'** + String get inputs; + + /// No description provided for @inputSources. + /// + /// In en, this message translates to: + /// **'Input Sources'** + String get inputSources; + + /// No description provided for @backupAndRestore. + /// + /// In en, this message translates to: + /// **'Backup & Restore'** + String get backupAndRestore; + + /// No description provided for @exportBackup. + /// + /// In en, this message translates to: + /// **'Export Backup'** + String get exportBackup; + + /// No description provided for @importBackup. + /// + /// In en, this message translates to: + /// **'Import Backup'** + String get importBackup; + + /// No description provided for @exportSuccess. + /// + /// In en, this message translates to: + /// **'Backup exported successfully to {path}'** + String exportSuccess(String path); + + /// No description provided for @importSuccess. + /// + /// In en, this message translates to: + /// **'Backup imported successfully'** + String get importSuccess; + + /// No description provided for @importConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to import the backup? This will overwrite your current settings and layout.'** + String get importConfirm; + + /// No description provided for @importError. + /// + /// In en, this message translates to: + /// **'Failed to import backup: {error}'** + String importError(String error); + + /// No description provided for @exportError. + /// + /// In en, this message translates to: + /// **'Failed to export backup: {error}'** + String exportError(String error); + + /// No description provided for @shareBackup. + /// + /// In en, this message translates to: + /// **'Share Backup'** + String get shareBackup; + + /// No description provided for @shareBackupDescription. + /// + /// In en, this message translates to: + /// **'Share backup with other devices on local network'** + String get shareBackupDescription; + + /// No description provided for @stopSharing. + /// + /// In en, this message translates to: + /// **'Stop Sharing'** + String get stopSharing; + + /// No description provided for @localNetworkSharingActive. + /// + /// In en, this message translates to: + /// **'Local network sharing is active!'** + String get localNetworkSharingActive; + + /// No description provided for @localNetworkSharingInstructions. + /// + /// In en, this message translates to: + /// **'Connect another device to the same Wi-Fi network and open the following URL in a web browser:'** + String get localNetworkSharingInstructions; + + /// No description provided for @localNetworkSharingDetails. + /// + /// In en, this message translates to: + /// **'Here you can download your TV settings/layout or upload a backup file back to this TV.'** + String get localNetworkSharingDetails; + + /// No description provided for @failedToStartServer. + /// + /// In en, this message translates to: + /// **'Failed to start sharing server: {error}'** + String failedToStartServer(String error); + + /// No description provided for @notificationBell. + /// + /// In en, this message translates to: + /// **'Notification Bell'** + String get notificationBell; + + /// No description provided for @autoHideNotificationBell. + /// + /// In en, this message translates to: + /// **'Auto-hide Notification Bell'** + String get autoHideNotificationBell; + + /// No description provided for @continueWatching. + /// + /// In en, this message translates to: + /// **'Continue Watching'** + String get continueWatching; + + /// No description provided for @showContinueWatchingOnHome. + /// + /// In en, this message translates to: + /// **'Show Continue Watching on Home'** + String get showContinueWatchingOnHome; + + /// No description provided for @permissionDeniedContinueWatching. + /// + /// In en, this message translates to: + /// **'Permission required to show Continue Watching'** + String get permissionDeniedContinueWatching; } -class _AppLocalizationsDelegate - extends LocalizationsDelegate { +class _AppLocalizationsDelegate extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override @@ -559,25 +717,25 @@ class _AppLocalizationsDelegate } @override - bool isSupported(Locale locale) => - ['en', 'es'].contains(locale.languageCode); + bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; } AppLocalizations lookupAppLocalizations(Locale locale) { + + // Lookup logic when only language code is specified. switch (locale.languageCode) { - case 'en': - return AppLocalizationsEn(); - case 'es': - return AppLocalizationsEs(); + case 'en': return AppLocalizationsEn(); + case 'es': return AppLocalizationsEs(); } throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.'); + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.' + ); } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index df1e1211..2c4907b6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1,5 +1,3 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -66,8 +64,7 @@ class AppLocalizationsEn extends AppLocalizations { String get dialogOptionBackButtonActionShowClock => 'Show clock'; @override - String get dialogTextNoFileExplorer => - 'Please install a file explorer in order to pick a picture.'; + String get dialogTextNoFileExplorer => 'Please install a file explorer in order to pick a picture.'; @override String get dialogTitleBackButtonAction => 'Choose the back button action'; @@ -171,8 +168,7 @@ class AppLocalizationsEn extends AppLocalizations { String get spacer => 'Spacer'; @override - String get spacerMaxHeightRequirement => - 'Must be greater than 0 and less than or equal to 500'; + String get spacerMaxHeightRequirement => 'Must be greater than 0 and less than or equal to 500'; @override String get statusBar => 'Status bar'; @@ -190,12 +186,10 @@ class AppLocalizationsEn extends AppLocalizations { String get themes => 'Themes'; @override - String get hideHighlightOutlineOnHomescreen => - 'Hide highlight outline on homescreen'; + String get hideHighlightOutlineOnHomescreen => 'Hide highlight outline on homescreen'; @override - String get appSelectorTransitionAnimation => - 'App selector transition animation'; + String get appSelectorTransitionAnimation => 'App selector transition animation'; @override String get sort => 'Sort'; @@ -215,8 +209,7 @@ class AppLocalizationsEn extends AppLocalizations { String get time => 'Time'; @override - String get titleStatusBarSettingsPage => - 'Choose what to display in the status bar'; + String get titleStatusBarSettingsPage => 'Choose what to display in the status bar'; @override String get tvApplications => 'TV Apps'; @@ -247,4 +240,93 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pickNightWallpaper => 'Pick night wallpaper'; + + @override + String get accessibility => 'Accessibility'; + + @override + String get defaultLauncherIsDefault => 'LTvLauncher is the default launcher'; + + @override + String get defaultLauncherNotDefault => 'LTvLauncher is not the default launcher'; + + @override + String get setAsDefaultLauncher => 'Set as default launcher'; + + @override + String get defaultLauncherDescription => 'When set as the default launcher, the Home button will always return to LTvLauncher. The TV will also boot directly into LTvLauncher.'; + + @override + String get inputs => 'Inputs'; + + @override + String get inputSources => 'Input Sources'; + + @override + String get backupAndRestore => 'Backup & Restore'; + + @override + String get exportBackup => 'Export Backup'; + + @override + String get importBackup => 'Import Backup'; + + @override + String exportSuccess(String path) { + return 'Backup exported successfully to $path'; + } + + @override + String get importSuccess => 'Backup imported successfully'; + + @override + String get importConfirm => 'Are you sure you want to import the backup? This will overwrite your current settings and layout.'; + + @override + String importError(String error) { + return 'Failed to import backup: $error'; + } + + @override + String exportError(String error) { + return 'Failed to export backup: $error'; + } + + @override + String get shareBackup => 'Share Backup'; + + @override + String get shareBackupDescription => 'Share backup with other devices on local network'; + + @override + String get stopSharing => 'Stop Sharing'; + + @override + String get localNetworkSharingActive => 'Local network sharing is active!'; + + @override + String get localNetworkSharingInstructions => 'Connect another device to the same Wi-Fi network and open the following URL in a web browser:'; + + @override + String get localNetworkSharingDetails => 'Here you can download your TV settings/layout or upload a backup file back to this TV.'; + + @override + String failedToStartServer(String error) { + return 'Failed to start sharing server: $error'; + } + + @override + String get notificationBell => 'Notification Bell'; + + @override + String get autoHideNotificationBell => 'Auto-hide Notification Bell'; + + @override + String get continueWatching => 'Continue Watching'; + + @override + String get showContinueWatchingOnHome => 'Show Continue Watching on Home'; + + @override + String get permissionDeniedContinueWatching => 'Permission required to show Continue Watching'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 43df3341..b3a307b3 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1,5 +1,3 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -9,7 +7,7 @@ class AppLocalizationsEs extends AppLocalizations { AppLocalizationsEs([String locale = 'es']) : super(locale); @override - String get aboutFlauncher => 'Acerca de FLauncher'; + String get aboutFlauncher => 'Acerca de LTvLauncher'; @override String get addCategory => 'Agregar categoría'; @@ -60,19 +58,16 @@ class AppLocalizationsEs extends AppLocalizations { String get dialogOptionBackButtonActionDoNothing => 'Nada'; @override - String get dialogOptionBackButtonActionShowScreensaver => - 'Mostrar salvapantallas'; + String get dialogOptionBackButtonActionShowScreensaver => 'Mostrar salvapantallas'; @override String get dialogOptionBackButtonActionShowClock => 'Mostrar reloj'; @override - String get dialogTextNoFileExplorer => - 'Por favor, instale un gestor de archivos para seleccionar una imagen.'; + String get dialogTextNoFileExplorer => 'Por favor, instale un gestor de archivos para seleccionar una imagen.'; @override - String get dialogTitleBackButtonAction => - 'Elegir la acción del botón \'Atrás\''; + String get dialogTitleBackButtonAction => 'Elegir la acción del botón \'Atrás\''; @override String disambiguateCategoryTitle(String title) { @@ -93,7 +88,7 @@ class AppLocalizationsEs extends AppLocalizations { String get gradient => 'Gradiente'; @override - String get favoriteApps => 'Favorite Apps'; + String get favoriteApps => 'Apps Favoritas'; @override String get grid => 'Cuadrícula'; @@ -144,15 +139,14 @@ class AppLocalizationsEs extends AppLocalizations { String get open => 'Abrir'; @override - String get orSelectFormatSpecifiers => - 'O seleccione especificadores de formato'; + String get orSelectFormatSpecifiers => 'O seleccione especificadores de formato'; @override String get picture => 'Imagen'; @override String removeFrom(String name) { - return 'Remover de $name'; + return 'Eliminar de $name'; } @override @@ -162,10 +156,10 @@ class AppLocalizationsEs extends AppLocalizations { String get reorder => 'Reordenar'; @override - String get row => 'Row'; + String get row => 'Fila'; @override - String get rowHeight => 'Row height'; + String get rowHeight => 'Altura de fila'; @override String get save => 'Guardar'; @@ -174,8 +168,7 @@ class AppLocalizationsEs extends AppLocalizations { String get spacer => 'Espaciador'; @override - String get spacerMaxHeightRequirement => - 'Debe ser mayor a cero y menor o igual a 500'; + String get spacerMaxHeightRequirement => 'Debe ser mayor a cero y menor o igual a 500'; @override String get statusBar => 'Barra de estado'; @@ -193,12 +186,10 @@ class AppLocalizationsEs extends AppLocalizations { String get themes => 'Temas'; @override - String get hideHighlightOutlineOnHomescreen => - 'Ocultar el contorno de resaltado en la pantalla de inicio'; + String get hideHighlightOutlineOnHomescreen => 'Ocultar el contorno de resaltado en la pantalla de inicio'; @override - String get appSelectorTransitionAnimation => - 'Animación de transición del selector de aplicaciones'; + String get appSelectorTransitionAnimation => 'Animación de transición del selector de aplicaciones'; @override String get sort => 'Orden'; @@ -218,8 +209,7 @@ class AppLocalizationsEs extends AppLocalizations { String get time => 'Hora'; @override - String get titleStatusBarSettingsPage => - 'Elija la información a mostrar en la barra de estado'; + String get titleStatusBarSettingsPage => 'Elija la información a mostrar en la barra de estado'; @override String get tvApplications => 'Aplicaciones del televisor'; @@ -243,11 +233,100 @@ class AppLocalizationsEs extends AppLocalizations { String get withEllipsisAddTo => 'Añadir a...'; @override - String get timeBasedWallpaper => 'Time based wallpaper'; + String get timeBasedWallpaper => 'Fondo de pantalla según hora'; @override - String get pickDayWallpaper => 'Pick day wallpaper'; + String get pickDayWallpaper => 'Elegir fondo de pantalla diurno'; @override - String get pickNightWallpaper => 'Pick night wallpaper'; + String get pickNightWallpaper => 'Elegir fondo de pantalla nocturno'; + + @override + String get accessibility => 'Accesibilidad'; + + @override + String get defaultLauncherIsDefault => 'LTvLauncher es el lanzador predeterminado'; + + @override + String get defaultLauncherNotDefault => 'LTvLauncher no es el lanzador predeterminado'; + + @override + String get setAsDefaultLauncher => 'Establecer como lanzador predeterminado'; + + @override + String get defaultLauncherDescription => 'Cuando se establece como lanzador predeterminado, el botón de inicio siempre regresará a LTvLauncher. El TV también iniciará directamente en LTvLauncher.'; + + @override + String get inputs => 'Entradas'; + + @override + String get inputSources => 'Fuentes de Entrada'; + + @override + String get backupAndRestore => 'Copia de seguridad y restauración'; + + @override + String get exportBackup => 'Exportar copia de seguridad'; + + @override + String get importBackup => 'Importar copia de seguridad'; + + @override + String exportSuccess(String path) { + return 'Copia de seguridad exportada con éxito a $path'; + } + + @override + String get importSuccess => 'Copia de seguridad importada con éxito'; + + @override + String get importConfirm => '¿Está seguro de que desea importar la copia de seguridad? Esto sobrescribirá su configuración y diseño actuales.'; + + @override + String importError(String error) { + return 'Error al importar la copia de seguridad: $error'; + } + + @override + String exportError(String error) { + return 'Error al exportar la copia de seguridad: $error'; + } + + @override + String get shareBackup => 'Compartir copia'; + + @override + String get shareBackupDescription => 'Comparta la copia de seguridad con otros dispositivos en la red local'; + + @override + String get stopSharing => 'Detener uso compartido'; + + @override + String get localNetworkSharingActive => '¡El uso compartido en red local está activo!'; + + @override + String get localNetworkSharingInstructions => 'Conecte otro dispositivo a la misma red Wi-Fi y abra la siguiente URL en un navegador web:'; + + @override + String get localNetworkSharingDetails => 'Aquí puede descargar la configuración/diseño de su TV o subir un archivo de copia de seguridad a esta TV.'; + + @override + String failedToStartServer(String error) { + return 'Error al iniciar el servidor de uso compartido: $error'; + } + + @override + String get notificationBell => 'Campana de notificaciones'; + + @override + String get autoHideNotificationBell => 'Ocultar campana de notificaciones automáticamente'; + + @override + String get continueWatching => 'Continuar viendo'; + + @override + String get showContinueWatchingOnHome => 'Mostrar Continuar viendo en Inicio'; + + @override + String get permissionDeniedContinueWatching => 'Se requiere permiso para mostrar Continuar viendo'; } diff --git a/lib/main.dart b/lib/main.dart index 406282b7..06b48a82 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,10 @@ import 'package:flauncher/providers/network_service.dart'; import 'package:flauncher/providers/settings_service.dart'; import 'package:flauncher/providers/brightness_service.dart'; import 'package:flauncher/providers/wallpaper_service.dart'; +import 'package:flauncher/providers/tv_inputs_service.dart'; +import 'package:flauncher/providers/notifications_service.dart'; +import 'package:flauncher/providers/watch_next_service.dart'; +import 'package:flauncher/providers/backup_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -43,6 +47,9 @@ Future main() async { runApp(MultiProvider( providers: [ + Provider( + create: (_) => BackupService(fLauncherDatabase, sharedPreferences), + ), ChangeNotifierProvider( create: (_) => SettingsService(sharedPreferences), lazy: false), @@ -58,6 +65,9 @@ Future main() async { ChangeNotifierProvider( create: (_) => BrightnessService(sharedPreferences), lazy: false), + ChangeNotifierProvider(create: (_) => TvInputsService(fLauncherChannel)), + ChangeNotifierProvider(create: (_) => NotificationsService(fLauncherChannel)), + ChangeNotifierProvider(create: (_) => WatchNextService(fLauncherChannel)), ], child: FLauncherApp() ) diff --git a/lib/models/category_with_apps.dart b/lib/models/category_with_apps.dart deleted file mode 100644 index 994b4279..00000000 --- a/lib/models/category_with_apps.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'category.dart'; -import 'app.dart'; - -class CategoryWithApps { - final Category category; - final List applications; - - CategoryWithApps(this.category, this.applications); -} diff --git a/lib/models/tv_input.dart b/lib/models/tv_input.dart new file mode 100644 index 00000000..89722530 --- /dev/null +++ b/lib/models/tv_input.dart @@ -0,0 +1,38 @@ +enum TvInputType { + tuner, + hdmi, + av, + other +} + +class TvInput { + final String id; + final String label; + final TvInputType type; + + TvInput({ + required this.id, + required this.label, + required this.type, + }); + + factory TvInput.fromMap(Map map) { + final int typeInt = map['type'] as int? ?? 1000; + TvInputType type; + if (typeInt == 1007) { + type = TvInputType.hdmi; + } else if (typeInt == 0 || typeInt == 2 || typeInt == 3) { + type = TvInputType.tuner; + } else if (typeInt >= 1001 && typeInt <= 1004) { + type = TvInputType.av; + } else { + type = TvInputType.other; + } + + return TvInput( + id: map['id'] as String? ?? '', + label: map['label'] as String? ?? '', + type: type, + ); + } +} diff --git a/lib/models/watch_next_program.dart b/lib/models/watch_next_program.dart new file mode 100644 index 00000000..dd253515 --- /dev/null +++ b/lib/models/watch_next_program.dart @@ -0,0 +1,44 @@ +import 'dart:typed_data'; + +class WatchNextProgram { + final int id; + final String packageName; + final String title; + final String description; + final int watchNextType; + final int lastEngagementTime; + final int playbackPosition; + final int duration; + final String intentUri; + final String posterArtUri; + Uint8List? posterBytes; + + WatchNextProgram({ + required this.id, + required this.packageName, + required this.title, + required this.description, + required this.watchNextType, + required this.lastEngagementTime, + required this.playbackPosition, + required this.duration, + required this.intentUri, + required this.posterArtUri, + this.posterBytes, + }); + + factory WatchNextProgram.fromMap(Map map) { + return WatchNextProgram( + id: map['id'] as int? ?? 0, + packageName: map['packageName'] as String? ?? '', + title: map['title'] as String? ?? '', + description: map['description'] as String? ?? '', + watchNextType: map['watchNextType'] as int? ?? 0, + lastEngagementTime: map['lastEngagementTime'] as int? ?? 0, + playbackPosition: map['playbackPosition'] as int? ?? 0, + duration: map['duration'] as int? ?? 0, + intentUri: map['intentUri'] as String? ?? '', + posterArtUri: map['posterArtUri'] as String? ?? '', + ); + } +} diff --git a/lib/providers/apps_service.dart b/lib/providers/apps_service.dart index bd215ddf..3eaf53e1 100644 --- a/lib/providers/apps_service.dart +++ b/lib/providers/apps_service.dart @@ -63,16 +63,20 @@ class AppsService extends ChangeNotifier { String? _pendingReorderFocusPackage; int? _pendingReorderFocusCategoryId; + int? _pendingReorderFocusIndex; String? get pendingReorderFocusPackage => _pendingReorderFocusPackage; int? get pendingReorderFocusCategoryId => _pendingReorderFocusCategoryId; + int? get pendingReorderFocusIndex => _pendingReorderFocusIndex; void clearPendingReorderFocusPackage() { _pendingReorderFocusPackage = null; _pendingReorderFocusCategoryId = null; + _pendingReorderFocusIndex = null; } - void setPendingReorderFocus(String packageName, int categoryId) { + void setPendingReorderFocus(String packageName, int categoryId, int index) { _pendingReorderFocusPackage = packageName; _pendingReorderFocusCategoryId = categoryId; + _pendingReorderFocusIndex = index; } List get applications => UnmodifiableListView( @@ -259,6 +263,8 @@ class AppsService extends ChangeNotifier { }); } + Future refreshState() => _refreshState(shouldNotifyListeners: true); + Future _refreshState({bool shouldNotifyListeners = true}) async { Future> appsFromDatabaseFuture = _database.getApplications(); Future> appsCategoriesFuture = @@ -274,17 +280,9 @@ class AppsService extends ChangeNotifier { Map appsFromSystemByPackageName = Map.fromEntries(appEntries); - List appsFromDatabase = await appsFromDatabaseFuture; - final Iterable appsRemovedFromSystem = appsFromDatabase.where( - (app) => !appsFromSystemByPackageName.containsKey(app.packageName)); - - final List uninstalledApplications = - appsRemovedFromSystem.map((app) => app.packageName).toList(); - await _database.transaction(() async { await _database.persistApps( appsFromSystemByPackageName.values.map((record) => record.$2)); - await _database.deleteApps(uninstalledApplications); }); appsFromDatabaseFuture = _database.getApplications(); @@ -296,7 +294,7 @@ class AppsService extends ChangeNotifier { spacersFuture ]); - appsFromDatabase = await appsFromDatabaseFuture; + List appsFromDatabase = await appsFromDatabaseFuture; List appsCategories = await appsCategoriesFuture; List categories = await categoriesFuture; List spacers = await spacersFuture; @@ -305,6 +303,8 @@ class AppsService extends ChangeNotifier { categories.map((category) => MapEntry(category.id, category))); _invalidateCategoryCache(); _applications = Map.fromEntries(appsFromDatabase + .where((application) => + appsFromSystemByPackageName.containsKey(application.packageName)) .map((application) => MapEntry(application.packageName, application))); _launcherSections.clear(); @@ -333,7 +333,7 @@ class AppsService extends ChangeNotifier { } } - if (appsCategories.isNotEmpty && !application.hidden) { + if (appsCategories.isNotEmpty) { List? currentApplicationCategories = appsCategoriesByPackage[application.packageName]; @@ -342,7 +342,9 @@ class AppsService extends ChangeNotifier { final category = _categoriesById[appCategory.categoryId]; if (category != null) { application.categoryOrders[category.id] = appCategory.order; - category.applications.add(application); + if (!application.hidden) { + category.applications.add(application); + } } } } @@ -640,6 +642,7 @@ class AppsService extends ChangeNotifier { List orderedAppCategories = []; for (int i = 0; i < applications.length; ++i) { + applications[i].categoryOrders[categoryFound.id] = i; orderedAppCategories.add(AppsCategoriesCompanion( categoryId: Value(categoryFound.id), appPackageName: Value(applications[i].packageName), @@ -683,9 +686,6 @@ class AppsService extends ChangeNotifier { // Remove from current await removeFromCategory(app, currentCategory); - // Set pending focus package so AppCard can reclaim focus and reorder mode - _pendingReorderFocusPackage = app.packageName; - // Add to target // DB Insert Logic // 1. Get current items in target @@ -698,6 +698,9 @@ class AppsService extends ChangeNotifier { targetApps.add(app); // Insert at bottom } + int newIndex = direction == AxisDirection.down ? 0 : targetApps.length - 1; + setPendingReorderFocus(app.packageName, targetCategory.id, newIndex); + // 3. Update orders for all items in target category List orderedAppCategories = []; for (int i = 0; i < targetApps.length; ++i) { @@ -734,23 +737,16 @@ class AppsService extends ChangeNotifier { int columnsCount = Category.ColumnsCount, int rowHeight = Category.RowHeight, bool shouldNotifyListeners = true}) async { - List orderedCategories = []; - int categoryOrder = 1, newCategoryId = -1; - for (Category category in _categoriesById.values) { - orderedCategories.add(CategoriesCompanion( - id: Value(category.id), order: Value(categoryOrder++))); - } + int order = _launcherSections.length; + int newCategoryId = -1; try { newCategoryId = await _database.transaction(() async { int newCategoryId = await _database.insertCategory( - CategoriesCompanion.insert(name: categoryName, order: 0)); - await _database.updateCategories(orderedCategories); - + CategoriesCompanion.insert(name: categoryName, order: order)); return newCategoryId; }); - Map newCategories = Map(); Category newCategory = Category( id: newCategoryId, name: categoryName, @@ -758,16 +754,9 @@ class AppsService extends ChangeNotifier { type: type, columnsCount: columnsCount, rowHeight: rowHeight, - order: 0); - newCategories[newCategoryId] = newCategory; - - categoryOrder = 1; - for (Category category in _categoriesById.values) { - newCategories[category.id] = category; - category.order = categoryOrder++; - } + order: order); - _categoriesById = newCategories; + _categoriesById[newCategoryId] = newCategory; _invalidateCategoryCache(); _launcherSections.add(newCategory); diff --git a/lib/providers/backup_service.dart b/lib/providers/backup_service.dart new file mode 100644 index 00000000..41591d39 --- /dev/null +++ b/lib/providers/backup_service.dart @@ -0,0 +1,272 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:drift/drift.dart'; +import 'package:flauncher/database.dart'; +import 'package:flauncher/models/app.dart'; +import 'package:flauncher/models/category.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class BackupService { + final FLauncherDatabase _database; + final SharedPreferences _sharedPreferences; + + BackupService(this._database, this._sharedPreferences); + + /// Gets the path to the backup file. + /// Tries Downloads folder first, falls back to external files directory. + Future getBackupFile() async { + Directory? dir; + try { + dir = await getDownloadsDirectory(); + } catch (_) { + // Ignored, fallback below + } + if (dir == null) { + try { + dir = await getExternalStorageDirectory(); + } catch (_) { + // Ignored, fallback below + } + } + if (dir == null) { + try { + dir = await getApplicationDocumentsDirectory(); + } catch (_) { + // Ignored + } + } + if (dir == null) { + throw const FileSystemException("Could not find any suitable directory for backup"); + } + return File(path.join(dir.path, 'ltv_backup.json')); + } + + Future getBackupDirectory() async { + final file = await getBackupFile(); + return file.parent; + } + + /// Gets the list of available backup files. + Future> getBackupFiles() async { + final Directory dir = await getBackupDirectory(); + if (!await dir.exists()) { + return []; + } + final List entries = []; + await for (final entity in dir.list()) { + if (entity is File) { + final name = path.basename(entity.path); + if (name.startsWith('ltv_backup') && name.endsWith('.json')) { + final lastModified = await entity.lastModified(); + final size = await entity.length(); + entries.add(BackupFileEntry( + file: entity, + name: name, + lastModified: lastModified, + size: size, + )); + } + } + } + // Sort by modification date (newest first) + entries.sort((a, b) => b.lastModified.compareTo(a.lastModified)); + return entries; + } + + /// Exports categories, apps, spacers, and settings to a JSON file. + Future exportBackup() async { + final Directory dir = await getBackupDirectory(); + final now = DateTime.now(); + final timestamp = "${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}_" + "${now.hour.toString().padLeft(2, '0')}${now.minute.toString().padLeft(2, '0')}${now.second.toString().padLeft(2, '0')}"; + final File file = File(path.join(dir.path, 'ltv_backup_$timestamp.json')); + + // 1. Fetch SharedPreferences settings + final Map settingsMap = {}; + final Set keys = _sharedPreferences.getKeys(); + for (final key in keys) { + final value = _sharedPreferences.get(key); + if (value != null) { + settingsMap[key] = value; + } + } + + // 2. Fetch database tables + final List categories = await _database.getCategories(); + final List apps = await _database.getApplications(); + final List appsCategories = await _database.getAppsCategories(); + final List spacers = await _database.getLauncherSpacers(); + + // 3. Serialize everything + final Map backupData = { + "version": 1, + "settings": settingsMap, + "apps": apps.map((a) => { + "packageName": a.packageName, + "name": a.name, + "version": a.version, + "hidden": a.hidden, + "lastLaunchedAt": a.lastLaunchedAt?.millisecondsSinceEpoch, + }).toList(), + "categories": categories.map((c) => { + "id": c.id, + "name": c.name, + "sort": c.sort.index, + "type": c.type.index, + "rowHeight": c.rowHeight, + "columnsCount": c.columnsCount, + "order": c.order, + }).toList(), + "appsCategories": appsCategories.map((ac) => { + "categoryId": ac.categoryId, + "appPackageName": ac.appPackageName, + "order": ac.order, + }).toList(), + "spacers": spacers.map((s) => { + "id": s.id, + "height": s.height, + "order": s.order, + }).toList(), + }; + + final String jsonStr = const JsonEncoder.withIndent(' ').convert(backupData); + await file.writeAsString(jsonStr); + return file.path; + } + + /// Imports categories, apps, spacers, and settings from the JSON file. + /// If [file] is not provided, tries to import the latest backup file. + Future importBackup([File? file]) async { + File? backupFile = file; + if (backupFile == null) { + final List backups = await getBackupFiles(); + if (backups.isEmpty) { + throw FileNotFoundException("No backup files found"); + } + backupFile = backups.first.file; + } + if (!await backupFile.exists()) { + throw FileNotFoundException("Backup file not found at ${backupFile.path}"); + } + + final String jsonStr = await backupFile.readAsString(); + final Map backupData = json.decode(jsonStr) as Map; + + if (backupData["version"] != 1) { + throw FormatException("Invalid backup file version"); + } + + // 1. Restore SharedPreferences + final Map settingsMap = Map.from(backupData["settings"] as Map); + for (final entry in settingsMap.entries) { + final key = entry.key; + final value = entry.value; + if (value is bool) { + await _sharedPreferences.setBool(key, value); + } else if (value is int) { + await _sharedPreferences.setInt(key, value); + } else if (value is double) { + await _sharedPreferences.setDouble(key, value); + } else if (value is String) { + await _sharedPreferences.setString(key, value); + } else if (value is List) { + await _sharedPreferences.setStringList(key, value.cast()); + } + } + + // 2. Restore database tables in a single transaction + await _database.transaction(() async { + // Clear existing records + await _database.customStatement('DELETE FROM apps_categories;'); + await _database.customStatement('DELETE FROM launcher_spacers;'); + await _database.customStatement('DELETE FROM categories;'); + await _database.customStatement('DELETE FROM apps;'); + + // Insert Apps + final List appsJson = backupData["apps"] as List; + final List appsCompanions = appsJson.map((a) { + final Map map = Map.from(a as Map); + final int? lla = map["lastLaunchedAt"] as int?; + return AppsCompanion( + packageName: Value(map["packageName"] as String), + name: Value(map["name"] as String), + version: Value(map["version"] as String), + hidden: Value(map["hidden"] as bool), + lastLaunchedAt: Value(lla != null ? DateTime.fromMillisecondsSinceEpoch(lla) : null), + ); + }).toList(); + await _database.batch((batch) { + batch.insertAll(_database.apps, appsCompanions); + }); + + // Insert Categories + final List categoriesJson = backupData["categories"] as List; + final List categoriesCompanions = categoriesJson.map((c) { + final Map map = Map.from(c as Map); + return CategoriesCompanion( + id: Value(map["id"] as int), + name: Value(map["name"] as String), + sort: Value(CategorySort.values[map["sort"] as int]), + type: Value(CategoryType.values[map["type"] as int]), + rowHeight: Value(map["rowHeight"] as int), + columnsCount: Value(map["columnsCount"] as int), + order: Value(map["order"] as int), + ); + }).toList(); + await _database.batch((batch) { + batch.insertAll(_database.categories, categoriesCompanions); + }); + + // Insert AppsCategories + final List appsCategoriesJson = backupData["appsCategories"] as List; + final List appsCategoriesCompanions = appsCategoriesJson.map((ac) { + final Map map = Map.from(ac as Map); + return AppsCategoriesCompanion( + categoryId: Value(map["categoryId"] as int), + appPackageName: Value(map["appPackageName"] as String), + order: Value(map["order"] as int), + ); + }).toList(); + await _database.batch((batch) { + batch.insertAll(_database.appsCategories, appsCategoriesCompanions); + }); + + // Insert Spacers + final List spacersJson = backupData["spacers"] as List; + final List spacersCompanions = spacersJson.map((s) { + final Map map = Map.from(s as Map); + return LauncherSpacersCompanion( + id: Value(map["id"] as int), + height: Value(map["height"] as int), + order: Value(map["order"] as int), + ); + }).toList(); + await _database.batch((batch) { + batch.insertAll(_database.launcherSpacers, spacersCompanions); + }); + }); + } +} + +class BackupFileEntry { + final File file; + final String name; + final DateTime lastModified; + final int size; + + BackupFileEntry({ + required this.file, + required this.name, + required this.lastModified, + required this.size, + }); +} + +class FileNotFoundException implements Exception { + final String message; + FileNotFoundException(this.message); + @override + String toString() => message; +} diff --git a/lib/providers/network_service.dart b/lib/providers/network_service.dart index e6592393..69e270b8 100644 --- a/lib/providers/network_service.dart +++ b/lib/providers/network_service.dart @@ -57,7 +57,7 @@ enum CellularNetworkType Nr, // 20 } -class NetworkService extends ChangeNotifier +class NetworkService extends ChangeNotifier with WidgetsBindingObserver { final FLauncherChannel _channel; @@ -67,7 +67,10 @@ class NetworkService extends ChangeNotifier int _wirelessNetworkSignalLevel; int _dailyDataUsage; // In bytes bool _hasUsageStatsPermission; + bool _vpnActive; Timer? _usageTimer; + int _callCount = 0; + int _usageCallCount = 0; NetworkService(this._channel) : @@ -76,7 +79,8 @@ class NetworkService extends ChangeNotifier _networkType = NetworkType.Unknown, _wirelessNetworkSignalLevel = 0, _dailyDataUsage = 0, - _hasUsageStatsPermission = false + _hasUsageStatsPermission = false, + _vpnActive = false { _channel.addNetworkChangedListener(_onNetworkChanged); @@ -92,46 +96,56 @@ class NetworkService extends ChangeNotifier }); _checkPermissionAndStartPolling(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + refreshPermissionAndUsage(); + } } void _checkPermissionAndStartPolling() async { - _hasUsageStatsPermission = await _channel.checkUsageStatsPermission(); + final localCallCount = ++_callCount; + final bool allowed = await _channel.checkUsageStatsPermission(); + if (localCallCount != _callCount) return; + + _hasUsageStatsPermission = allowed; if (_hasUsageStatsPermission) { _fetchUsage(); - _usageTimer = Timer.periodic(const Duration(minutes: 5), (_) => _fetchUsage()); + if (_usageTimer == null || !_usageTimer!.isActive) { + _usageTimer = Timer.periodic(const Duration(minutes: 5), (_) => _fetchUsage()); + } + } else { + _usageTimer?.cancel(); + _usageTimer = null; } notifyListeners(); } Future requestPermission() async { await _channel.requestUsageStatsPermission(); - // Wait a bit for user to return, or just rely on Lifecycle, but here we can't easily hook into lifecycle. - // We can assume if they clicked it, they might come back. - // Ideally we recheck when app resumes. For now let's just recheck after a delay or expose a method to recheck. } // Call this when app resumes Future refreshPermissionAndUsage() async { - _hasUsageStatsPermission = await _channel.checkUsageStatsPermission(); - if (_hasUsageStatsPermission) { - _fetchUsage(); - if (_usageTimer == null || !_usageTimer!.isActive) { - _usageTimer = Timer.periodic(const Duration(minutes: 5), (_) => _fetchUsage()); - } - } else { - _usageTimer?.cancel(); - } - notifyListeners(); + _checkPermissionAndStartPolling(); } Future openWifiSettings() async { await _channel.openWifiSettings(); } + Future openVpnSettings() async { + await _channel.openVpnSettings(); + } + Future _fetchUsage() async { - // This will be called with the current period from the widget - // For now, default to daily + final localCallCount = ++_usageCallCount; int usage = await _channel.getDailyDataUsage(); + if (localCallCount != _usageCallCount) return; + if (usage != -1) { _dailyDataUsage = usage; notifyListeners(); @@ -153,6 +167,7 @@ class NetworkService extends ChangeNotifier @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _usageTimer?.cancel(); super.dispose(); } @@ -163,6 +178,7 @@ class NetworkService extends ChangeNotifier int get wirelessNetworkSignalLevel => _wirelessNetworkSignalLevel; int get dailyDataUsage => _dailyDataUsage; bool get hasUsageStatsPermission => _hasUsageStatsPermission; + bool get vpnActive => _vpnActive; CellularNetworkType _getCellularNetworkType(int index) { @@ -181,11 +197,12 @@ class NetworkService extends ChangeNotifier int networkTypeInt = map["networkType"] as int; _hasInternetAccess = map["internetAccess"] as bool; _networkType = NetworkType.values[networkTypeInt]; + _vpnActive = map["vpnActive"] as bool? ?? false; if (_networkType == NetworkType.Cellular || _networkType == NetworkType.Wifi) { _wirelessNetworkSignalLevel = map["wirelessSignalLevel"] as int; } - log("NetworkService: parsed type $_networkType, signal $_wirelessNetworkSignalLevel"); + log("NetworkService: parsed type $_networkType, signal $_wirelessNetworkSignalLevel, vpn $_vpnActive"); } catch (e) { log("NetworkService error parsing: $e"); } @@ -201,6 +218,7 @@ class NetworkService extends ChangeNotifier case "NETWORK_UNAVAILABLE": _hasInternetAccess = false; _networkType = NetworkType.Unknown; + _vpnActive = false; break; case "CAPABILITIES_CHANGED": Map map = event["arguments"]; diff --git a/lib/providers/notifications_service.dart b/lib/providers/notifications_service.dart new file mode 100644 index 00000000..f4e0f37f --- /dev/null +++ b/lib/providers/notifications_service.dart @@ -0,0 +1,232 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flauncher/flauncher_channel.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class NotificationItem { + final String key; + final String packageName; + final String title; + final String text; + final bool isClearable; + + NotificationItem({ + required this.key, + required this.packageName, + required this.title, + required this.text, + required this.isClearable, + }); + + factory NotificationItem.fromMap(Map map) { + return NotificationItem( + key: map['key'] as String? ?? '', + packageName: map['packageName'] as String? ?? '', + title: map['title'] as String? ?? '', + text: map['text'] as String? ?? '', + isClearable: map['isClearable'] as bool? ?? false, + ); + } +} + +class NotificationsService extends ChangeNotifier with WidgetsBindingObserver { + final FLauncherChannel _channel; + Map _notificationCounts = {}; + List _notifications = []; + bool _hasPermission = false; + bool _hasOverlayPermission = false; + bool _systemPopupEnabled = false; + bool _initialized = false; + StreamSubscription? _subscription; + int _callCount = 0; + SharedPreferences? _prefs; + + NotificationsService(this._channel) { + _init(); + WidgetsBinding.instance.addObserver(this); + } + + Map get notificationCounts => Map.unmodifiable(_notificationCounts); + List get notifications => List.unmodifiable(_notifications); + bool get hasPermission => _hasPermission; + bool get hasOverlayPermission => _hasOverlayPermission; + bool get systemPopupEnabled => _systemPopupEnabled; + bool get initialized => _initialized; + + int getNotificationCount(String packageName) { + return _notificationCounts[packageName] ?? 0; + } + + Future _init() async { + final localCallCount = ++_callCount; + _prefs = await SharedPreferences.getInstance(); + if (localCallCount != _callCount) return; + + _systemPopupEnabled = _prefs?.getBool('system_notifications_popup') ?? false; + + final bool allowed = await _channel.checkNotificationListenerPermission(); + if (localCallCount != _callCount) return; + + _hasPermission = allowed; + + final bool overlayAllowed = await _channel.checkOverlayPermission(); + if (localCallCount != _callCount) return; + + _hasOverlayPermission = overlayAllowed; + + if (_hasPermission) { + final List> list = await _channel.getActiveNotifications(); + if (localCallCount != _callCount) return; + + _updateNotificationCounts(list); + + _subscription = _channel.addNotificationsChangedListener((eventList) { + _updateNotificationCounts(eventList); + }); + } + + _initialized = true; + notifyListeners(); + } + + Future checkPermission() async { + final localCallCount = ++_callCount; + final bool allowed = await _channel.checkNotificationListenerPermission(); + if (localCallCount != _callCount) return; + + if (_hasPermission != allowed) { + _hasPermission = allowed; + notifyListeners(); + } + } + + Future refreshNotifications() async { + if (!_hasPermission) return; + + final localCallCount = ++_callCount; + final List> list = await _channel.getActiveNotifications(); + if (localCallCount != _callCount) return; + + _updateNotificationCounts(list); + } + + void _updateNotificationCounts(List> list) { + final Map newCounts = {}; + final List newNotifications = []; + + for (final item in list) { + final String? pkg = item['packageName'] as String?; + final int? count = item['count'] as int?; + if (pkg != null) { + if (count != null) { + newCounts[pkg] = count; + for (int i = 0; i < count; i++) { + newNotifications.add(NotificationItem( + key: '${pkg}_$i', + packageName: pkg, + title: 'Notification', + text: 'Content', + isClearable: true, + )); + } + } else { + final notification = NotificationItem.fromMap(item); + newNotifications.add(notification); + if (notification.isClearable) { + newCounts[pkg] = (newCounts[pkg] ?? 0) + 1; + } + } + } + } + + // Direct comparison to avoid unnecessary notifies + bool changed = false; + if (_notificationCounts.length != newCounts.length) { + changed = true; + } else { + for (final key in newCounts.keys) { + if (_notificationCounts[key] != newCounts[key]) { + changed = true; + break; + } + } + } + + if (!changed) { + if (_notifications.length != newNotifications.length) { + changed = true; + } else { + for (int i = 0; i < _notifications.length; i++) { + if (_notifications[i].key != newNotifications[i].key || + _notifications[i].title != newNotifications[i].title || + _notifications[i].text != newNotifications[i].text || + _notifications[i].isClearable != newNotifications[i].isClearable) { + changed = true; + break; + } + } + } + } + + if (changed) { + _notificationCounts = newCounts; + _notifications = newNotifications; + notifyListeners(); + } + } + + Future requestPermission() async { + return await _channel.requestNotificationListenerPermission(); + } + + Future checkOverlayPermission() async { + final localCallCount = ++_callCount; + final bool allowed = await _channel.checkOverlayPermission(); + if (localCallCount != _callCount) return; + + if (_hasOverlayPermission != allowed) { + _hasOverlayPermission = allowed; + notifyListeners(); + } + } + + Future requestOverlayPermission() async { + return await _channel.requestOverlayPermission(); + } + + Future setSystemPopupEnabled(bool enabled) async { + _systemPopupEnabled = enabled; + await _prefs?.setBool('system_notifications_popup', enabled); + notifyListeners(); + } + + Future dismiss(String key) async { + final bool success = await _channel.dismissNotification(key); + if (success) { + await refreshNotifications(); + } + } + + Future dismissAll() async { + final bool success = await _channel.dismissAllNotifications(); + if (success) { + await refreshNotifications(); + } + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + checkPermission(); + checkOverlayPermission(); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _subscription?.cancel(); + super.dispose(); + } +} diff --git a/lib/providers/settings_service.dart b/lib/providers/settings_service.dart index 10418f3b..8540abb0 100644 --- a/lib/providers/settings_service.dart +++ b/lib/providers/settings_service.dart @@ -43,6 +43,10 @@ const String _showNetworkIndicatorInStatusBarKey = "show_network_indicator_in_st const String _accentColorKey = "accent_color"; const String _screensaverClockStyleKey = "screensaver_clock_style"; const String _timeBasedWallpaperEnabledKey = "time_based_wallpaper_enabled"; +const String _showInputsWidgetInStatusBarKey = "show_inputs_widget_in_status_bar"; +const String _showContinueWatchingKey = "show_continue_watching"; +const String _showNotificationsWidgetInStatusBarKey = "show_notifications_widget_in_status_bar"; +const String _autoHideNotificationsWidgetKey = "auto_hide_notifications_widget"; // WiFi usage period options const String DATA_USAGE_DAILY = "daily"; @@ -91,6 +95,10 @@ class SettingsService extends ChangeNotifier { late String _accentColorHex; late String _screensaverClockStyle; late bool _timeBasedWallpaperEnabled; + late bool _showInputsWidgetInStatusBar; + late bool _showContinueWatching; + late bool _showNotificationsWidgetInStatusBar; + late bool _autoHideNotificationsWidget; bool get appHighlightAnimationEnabled => _appHighlightAnimationEnabled; @@ -126,6 +134,11 @@ class SettingsService extends ChangeNotifier { bool get showNetworkIndicatorInStatusBar => _showNetworkIndicatorInStatusBar; + bool get showInputsWidgetInStatusBar => _showInputsWidgetInStatusBar; + bool get showContinueWatching => _showContinueWatching; + bool get showNotificationsWidgetInStatusBar => _showNotificationsWidgetInStatusBar; + bool get autoHideNotificationsWidget => _autoHideNotificationsWidget; + String get accentColorHex => _accentColorHex; String get screensaverClockStyle => _screensaverClockStyle; @@ -136,6 +149,10 @@ class SettingsService extends ChangeNotifier { } SettingsService(this._sharedPreferences) { + reload(); + } + + void reload() { _appHighlightAnimationEnabled = _sharedPreferences.getBool(_appHighlightAnimationEnabledKey) ?? true; _appKeyClickEnabled = _sharedPreferences.getBool(_appKeyClickEnabledKey) ?? true; _autoHideAppBarEnabled = _sharedPreferences.getBool(_autoHideAppBarKey) ?? false; @@ -156,6 +173,11 @@ class SettingsService extends ChangeNotifier { _accentColorHex = _sharedPreferences.getString(_accentColorKey) ?? ACCENT_COLOR_PURPLE; _screensaverClockStyle = _sharedPreferences.getString(_screensaverClockStyleKey) ?? "minimal"; _timeBasedWallpaperEnabled = _sharedPreferences.getBool(_timeBasedWallpaperEnabledKey) ?? false; + _showInputsWidgetInStatusBar = _sharedPreferences.getBool(_showInputsWidgetInStatusBarKey) ?? true; + _showContinueWatching = _sharedPreferences.getBool(_showContinueWatchingKey) ?? true; + _showNotificationsWidgetInStatusBar = _sharedPreferences.getBool(_showNotificationsWidgetInStatusBarKey) ?? true; + _autoHideNotificationsWidget = _sharedPreferences.getBool(_autoHideNotificationsWidgetKey) ?? false; + notifyListeners(); } Future setAppHighlightAnimationEnabled(bool value) async { @@ -277,4 +299,28 @@ class SettingsService extends ChangeNotifier { _timeBasedWallpaperEnabled = enabled; notifyListeners(); } + + Future setShowInputsWidgetInStatusBar(bool show) async { + await _sharedPreferences.setBool(_showInputsWidgetInStatusBarKey, show); + _showInputsWidgetInStatusBar = show; + notifyListeners(); + } + + Future setShowContinueWatching(bool show) async { + await _sharedPreferences.setBool(_showContinueWatchingKey, show); + _showContinueWatching = show; + notifyListeners(); + } + + Future setShowNotificationsWidgetInStatusBar(bool show) async { + await _sharedPreferences.setBool(_showNotificationsWidgetInStatusBarKey, show); + _showNotificationsWidgetInStatusBar = show; + notifyListeners(); + } + + Future setAutoHideNotificationsWidget(bool value) async { + await _sharedPreferences.setBool(_autoHideNotificationsWidgetKey, value); + _autoHideNotificationsWidget = value; + notifyListeners(); + } } diff --git a/lib/providers/tv_inputs_service.dart b/lib/providers/tv_inputs_service.dart new file mode 100644 index 00000000..57d45892 --- /dev/null +++ b/lib/providers/tv_inputs_service.dart @@ -0,0 +1,33 @@ +import 'package:flutter/foundation.dart'; +import 'package:flauncher/flauncher_channel.dart'; +import 'package:flauncher/models/tv_input.dart'; + +class TvInputsService extends ChangeNotifier { + final FLauncherChannel _channel; + List _inputs = []; + bool _initialized = false; + + TvInputsService(this._channel) { + _init(); + } + + List get inputs => List.unmodifiable(_inputs); + bool get hasInputs => _inputs.isNotEmpty; + bool get initialized => _initialized; + + Future _init() async { + await refreshInputs(); + _initialized = true; + notifyListeners(); + } + + Future refreshInputs() async { + final List> rawInputs = await _channel.getTvInputs(); + _inputs = rawInputs.map((map) => TvInput.fromMap(map)).toList(); + notifyListeners(); + } + + Future switchInput(String id) async { + return await _channel.launchTvInput(id); + } +} diff --git a/lib/providers/watch_next_service.dart b/lib/providers/watch_next_service.dart new file mode 100644 index 00000000..8fb41fec --- /dev/null +++ b/lib/providers/watch_next_service.dart @@ -0,0 +1,168 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io' show Platform; +import 'package:flutter/foundation.dart'; +import 'package:flauncher/flauncher_channel.dart'; +import '../models/watch_next_program.dart'; + +class WatchNextService extends ChangeNotifier { + final FLauncherChannel _channel; + List _programs = []; + bool _initialized = false; + Timer? _refreshTimer; + int _callCount = 0; + + bool get _isTest => Platform.environment.containsKey('FLUTTER_TEST'); + + WatchNextService(this._channel) { + _init(); + } + + List get programs => List.unmodifiable(_programs); + bool get initialized => _initialized; + + Future _init() async { + await refresh(); + _initialized = true; + notifyListeners(); + + // Refresh every 30 seconds to keep EPG/Playback progress up to date + _refreshTimer = Timer.periodic(const Duration(seconds: 30), (_) => refresh()); + } + + Future refresh() async { + final int callSnapshot = ++_callCount; + try { + final bool hasPermission = await checkPermission(); + if (callSnapshot != _callCount) return; + + if (!hasPermission) { + if (_programs.isNotEmpty) { + _programs = []; + if (callSnapshot == _callCount) notifyListeners(); + } + return; + } + + List> list; + try { + list = await _channel.getWatchNextPrograms(); + } catch (e) { + if (kReleaseMode) { + rethrow; + } else { + list = const []; + } + } + if (callSnapshot != _callCount) return; + + if (!kReleaseMode && !_isTest && list.isEmpty) { + list = _getMockPrograms(); + } + + // Phase 1: Emit programs immediately with cached posters where available + final List newPrograms = []; + for (final map in list) { + final program = WatchNextProgram.fromMap(map); + // Reuse existing poster bytes if same program already loaded + final existing = _findExisting(program.id); + if (existing != null && existing.posterBytes != null) { + program.posterBytes = existing.posterBytes; + } + newPrograms.add(program); + } + _programs = newPrograms; + if (callSnapshot == _callCount) notifyListeners(); + + // Phase 2: Fetch missing posters concurrently, then notify again + final needsPoster = newPrograms.where( + (p) => p.posterArtUri.isNotEmpty && p.posterBytes == null + ).toList(); + if (needsPoster.isNotEmpty) { + await Future.wait( + needsPoster.map((p) async { + try { + p.posterBytes = await _channel.getWatchNextPoster(p.posterArtUri); + } catch (e) { + log('Failed to fetch poster for ${p.title}', name: 'WatchNextService', error: e); + } + }), + ); + if (callSnapshot == _callCount) notifyListeners(); + } + } catch (e) { + log('Failed to refresh watch next programs', name: 'WatchNextService', error: e); + } + } + + List> _getMockPrograms() { + return [ + { + 'id': 9991, + 'packageName': 'com.google.android.youtube', + 'title': 'Mock Video 1 (YouTube)', + 'description': 'Description for mock video 1', + 'watchNextType': 0, + 'lastEngagementTime': DateTime.now().millisecondsSinceEpoch, + 'playbackPosition': 500000, + 'duration': 1000000, + 'intentUri': 'https://www.youtube.com', + 'posterArtUri': '', + }, + { + 'id': 9992, + 'packageName': 'com.netflix.mediaclient', + 'title': 'Mock Show 2 (Netflix)', + 'description': 'S1 E2 • Episode Title', + 'watchNextType': 1, + 'lastEngagementTime': DateTime.now().millisecondsSinceEpoch - 100000, + 'playbackPosition': 200000, + 'duration': 1800000, + 'intentUri': 'https://www.netflix.com', + 'posterArtUri': '', + }, + ]; + } + + WatchNextProgram? _findExisting(int id) { + for (final p in _programs) { + if (p.id == id) return p; + } + return null; + } + + Future checkPermission() async { + if (!kReleaseMode && !_isTest) return true; + return await _channel.checkWatchNextPermission(); + } + + Future requestPermission() async { + if (!kReleaseMode && !_isTest) return true; + final bool granted = await _channel.requestWatchNextPermission(); + if (granted) { + await refresh(); + } + return granted; + } + + Future launch(WatchNextProgram program) async { + if (program.intentUri.isNotEmpty) { + return await _channel.launchWatchNextProgram(program.intentUri); + } else if (program.packageName.isNotEmpty) { + try { + await _channel.launchApp(program.packageName); + return true; + } catch (e) { + log('Failed to launch app ${program.packageName}', name: 'WatchNextService', error: e); + return false; + } + } + return false; + } + + @override + void dispose() { + _refreshTimer?.cancel(); + super.dispose(); + } +} diff --git a/lib/widgets/app_card.dart b/lib/widgets/app_card.dart index 6416cd31..9b62eb5f 100644 --- a/lib/widgets/app_card.dart +++ b/lib/widgets/app_card.dart @@ -24,6 +24,7 @@ import 'package:flauncher/providers/apps_service.dart'; import 'package:flauncher/providers/settings_service.dart'; import 'package:flauncher/widgets/application_info_panel.dart'; import 'package:flauncher/widgets/focus_keyboard_listener.dart'; +import 'package:flauncher/providers/notifications_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -40,6 +41,7 @@ class AppCard extends StatefulWidget final bool autofocus; final void Function(AxisDirection) onMove; final VoidCallback onMoveEnd; + final int index; final bool handleUpNavigationToSettings; final bool isFirstInRow; final bool isLastInRow; @@ -51,6 +53,7 @@ class AppCard extends StatefulWidget required this.autofocus, required this.onMove, required this.onMoveEnd, + this.index = 0, this.handleUpNavigationToSettings = false, this.isFirstInRow = false, this.isLastInRow = false, @@ -98,9 +101,11 @@ class _AppCardState extends State with TickerProviderStateMixin { // Check if we need to restore focus/reorder mode after a move WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; final appsService = Provider.of(context, listen: false); if (appsService.pendingReorderFocusPackage == widget.application.packageName && - appsService.pendingReorderFocusCategoryId == widget.category.id) { + appsService.pendingReorderFocusCategoryId == widget.category.id && + widget.index == appsService.pendingReorderFocusIndex) { appsService.clearPendingReorderFocusPackage(); _focusNode.requestFocus(); @@ -117,9 +122,11 @@ class _AppCardState extends State with TickerProviderStateMixin { // Check for pending focus on update as well WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; final appsService = Provider.of(context, listen: false); if (appsService.pendingReorderFocusPackage == widget.application.packageName && - appsService.pendingReorderFocusCategoryId == widget.category.id) { + appsService.pendingReorderFocusCategoryId == widget.category.id && + widget.index == appsService.pendingReorderFocusIndex) { appsService.clearPendingReorderFocusPackage(); _focusNode.requestFocus(); @@ -226,6 +233,47 @@ class _AppCardState extends State with TickerProviderStateMixin { child: Stack( fit: StackFit.expand, children: [ + Selector( + selector: (_, service) => service.getNotificationCount(widget.application.packageName), + builder: (context, count, _) { + if (count <= 0) return const SizedBox.shrink(); + return Positioned( + top: 8, + right: 8, + child: IgnorePointer( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(10), + boxShadow: const [ + BoxShadow( + color: Colors.black45, + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + constraints: const BoxConstraints( + minWidth: 20, + minHeight: 20, + ), + child: Center( + child: Text( + '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + height: 1.0, + ), + ), + ), + ), + ), + ); + }, + ), InkWell( focusNode: _focusNode, autofocus: widget.autofocus, @@ -466,13 +514,16 @@ class _AppCardState extends State with TickerProviderStateMixin { else { return const Padding( padding: EdgeInsets.all(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), - SizedBox(height: 0, width: 16), - Text("Loading") - ], + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 0, width: 16), + Text("Loading") + ], + ), ), ); } @@ -589,7 +640,10 @@ class _AppCardState extends State with TickerProviderStateMixin { } else if (key == LogicalKeyboardKey.arrowDown) { widget.onMove(AxisDirection.down); - } else if (_validationKeys.contains(key) || key == LogicalKeyboardKey.escape) { + } else if (_validationKeys.contains(key) || + key == LogicalKeyboardKey.escape || + key == LogicalKeyboardKey.goBack || + key == LogicalKeyboardKey.gameButtonB) { setState(() => _moving = false); widget.onMoveEnd(); diff --git a/lib/widgets/application_info_panel.dart b/lib/widgets/application_info_panel.dart index 753b9205..33d09fbd 100644 --- a/lib/widgets/application_info_panel.dart +++ b/lib/widgets/application_info_panel.dart @@ -24,8 +24,8 @@ import 'package:flauncher/providers/apps_service.dart'; import 'package:flauncher/widgets/add_to_category_dialog.dart'; import 'package:flauncher/widgets/side_panel_dialog.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations_en.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; +import 'package:flauncher/l10n/app_localizations_en.dart'; import '../models/app.dart'; import '../models/category.dart'; diff --git a/lib/widgets/apps_grid.dart b/lib/widgets/apps_grid.dart index b104bc67..2e8a07af 100644 --- a/lib/widgets/apps_grid.dart +++ b/lib/widgets/apps_grid.dart @@ -64,13 +64,14 @@ class AppsGrid extends StatelessWidget return AppCard( key: Key(applications[index].packageName), + index: index, category: category, application: applications[index], autofocus: index == 0, handleUpNavigationToSettings: isFirstSection && index < category.columnsCount, isFirstInRow: isFirstInRow, isLastInRow: isLastInRow, - onMove: (direction) => _onMove(context, direction, index), + onMove: (direction) => _onMove(context, direction, applications[index]), onMoveEnd: () => _saveOrder(context) ); } @@ -111,7 +112,10 @@ class AppsGrid extends StatelessWidget return index >= 0 ? index : null; } - void _onMove(BuildContext context, AxisDirection direction, int index) { + void _onMove(BuildContext context, AxisDirection direction, App movingApp) { + final index = applications.indexOf(movingApp); + if (index == -1) return; + final currentRow = (index / category.columnsCount).floor(); final totalRows = ((applications.length - 1) / category.columnsCount).floor(); @@ -142,12 +146,11 @@ class AppsGrid extends StatelessWidget } if (newIndex != null) { final appsService = context.read(); - final movingApp = applications[index]; appsService.reorderApplication(category, index, newIndex); // Set pending focus so the app at the new position will request focus - appsService.setPendingReorderFocus(movingApp.packageName, category.id); + appsService.setPendingReorderFocus(movingApp.packageName, category.id, newIndex); } } diff --git a/lib/widgets/category_row.dart b/lib/widgets/category_row.dart index d28d34d0..b6a1a306 100644 --- a/lib/widgets/category_row.dart +++ b/lib/widgets/category_row.dart @@ -60,13 +60,14 @@ class CategoryRow extends StatelessWidget key: Key(applications[index].packageName), padding: const EdgeInsets.symmetric(horizontal: 8), child: AppCard( + index: index, category: category, application: applications[index], autofocus: index == 0, handleUpNavigationToSettings: isFirstSection, isFirstInRow: index == 0, isLastInRow: index == applications.length - 1, - onMove: (direction) => _onMove(context, direction, index), + onMove: (direction) => _onMove(context, direction, applications[index]), onMoveEnd: () => _onMoveEnd(context) ) ) @@ -104,7 +105,10 @@ class CategoryRow extends StatelessWidget int _findChildIndex(Key key) => applications.indexWhere((app) => app.packageName == (key as ValueKey).value); - void _onMove(BuildContext context, AxisDirection direction, int index) { + void _onMove(BuildContext context, AxisDirection direction, App movingApp) { + final index = applications.indexOf(movingApp); + if (index == -1) return; + int newIndex = 0; if (direction == AxisDirection.right && index < applications.length - 1) { @@ -117,11 +121,10 @@ class CategoryRow extends StatelessWidget } final appsService = context.read(); - final movingApp = applications[index]; appsService.reorderApplication(category, index, newIndex); // Set pending focus so the app at the new position will request focus - appsService.setPendingReorderFocus(movingApp.packageName, category.id); + appsService.setPendingReorderFocus(movingApp.packageName, category.id, newIndex); } void _onMoveEnd(BuildContext context) { diff --git a/lib/widgets/continue_watching_row.dart b/lib/widgets/continue_watching_row.dart new file mode 100644 index 00000000..4f18e831 --- /dev/null +++ b/lib/widgets/continue_watching_row.dart @@ -0,0 +1,379 @@ +import 'package:flauncher/models/watch_next_program.dart'; +import 'package:flauncher/providers/apps_service.dart'; +import 'package:flauncher/providers/settings_service.dart'; +import 'package:flauncher/providers/watch_next_service.dart'; +import 'package:flauncher/actions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; + +class ContinueWatchingRow extends StatelessWidget { + const ContinueWatchingRow({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final settingsService = Provider.of(context); + if (!settingsService.showContinueWatching) { + return const SizedBox.shrink(); + } + + return Consumer2( + builder: (context, watchNextService, appsService, _) { + final List programs = watchNextService.programs; + if (programs.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, bottom: 8), + child: Text( + AppLocalizations.of(context)!.continueWatching, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + shadows: [ + const Shadow( + color: Colors.black54, + offset: Offset(1, 1), + blurRadius: 8, + ) + ], + ), + ), + ), + SizedBox( + height: 170, // Increased for larger cards + child: ListView.builder( + clipBehavior: Clip.none, + padding: const EdgeInsets.all(8), + scrollDirection: Axis.horizontal, + itemCount: programs.length, + itemBuilder: (context, index) { + final program = programs[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: WatchNextCard( + program: program, + appsService: appsService, + watchNextService: watchNextService, + ), + ); + }, + ), + ), + ], + ), + ); + }, + ); + } +} + +class WatchNextCard extends StatefulWidget { + final WatchNextProgram program; + final AppsService appsService; + final WatchNextService watchNextService; + + const WatchNextCard({ + Key? key, + required this.program, + required this.appsService, + required this.watchNextService, + }) : super(key: key); + + @override + State createState() => _WatchNextCardState(); +} + +class _WatchNextCardState extends State { + late final FocusNode _focusNode; + bool _focused = false; + Future? _iconFuture; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + _focusNode.addListener(_onFocusChange); + _iconFuture = widget.appsService.getAppIcon(widget.program.packageName); + } + + @override + void didUpdateWidget(covariant WatchNextCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.program.packageName != widget.program.packageName) { + _iconFuture = widget.appsService.getAppIcon(widget.program.packageName); + } + } + + void _onFocusChange() { + setState(() { + _focused = _focusNode.hasFocus; + }); + if (_focusNode.hasFocus) { + Scrollable.ensureVisible( + context, + alignment: 0.5, + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 100), + ); + } + } + + @override + void dispose() { + _focusNode.removeListener(_onFocusChange); + _focusNode.dispose(); + super.dispose(); + } + + void _onPressed() { + widget.watchNextService.launch(widget.program); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final settingsService = Provider.of(context, listen: false); + final accentColor = settingsService.accentColor; + + const double cardWidth = 260; + const double cardHeight = 146; + + // Progress percentage + double progress = 0; + if (widget.program.duration > 0 && widget.program.playbackPosition >= 0) { + progress = widget.program.playbackPosition / widget.program.duration; + if (progress > 1.0) progress = 1.0; + } + + return Focus( + focusNode: _focusNode, + onKeyEvent: (node, event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.select || + event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.gameButtonA) { + _onPressed(); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + Actions.invoke(context, const MoveFocusToSettingsIntent()); + return KeyEventResult.handled; + } + } + // Handle key repeats for directional navigation only + if (event is KeyRepeatEvent) { + if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + Actions.invoke(context, const MoveFocusToSettingsIntent()); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: GestureDetector( + onTap: _onPressed, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + width: cardWidth, + height: cardHeight, + transform: _focused + ? (Matrix4.identity()..scale(1.05, 1.05)) + : Matrix4.identity(), + transformAlignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: _focused ? accentColor : Colors.white10, + width: _focused ? 2.5 : 1.0, + ), + boxShadow: _focused + ? [ + BoxShadow( + color: accentColor.withOpacity(0.35), + blurRadius: 16, + spreadRadius: 2, + ), + BoxShadow( + color: accentColor.withOpacity(0.15), + blurRadius: 32, + spreadRadius: 4, + ), + ] + : [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Stack( + children: [ + // Poster background + Positioned.fill( + child: widget.program.posterBytes != null + ? Image.memory( + widget.program.posterBytes!, + fit: BoxFit.cover, + ) + : _emptyPosterFallback(theme), + ), + // App icon badge (top-right, glass effect) + Positioned( + top: 8, + right: 8, + child: FutureBuilder( + future: _iconFuture, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: Colors.black45, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: Colors.white.withOpacity(0.15), + width: 0.5, + ), + ), + padding: const EdgeInsets.all(3), + child: Image.memory(snapshot.data!), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + // Title + progress overlay (bottom) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withOpacity(0.9), + Colors.black.withOpacity(0.5), + Colors.transparent, + ], + stops: const [0.0, 0.6, 1.0], + ), + ), + padding: const EdgeInsets.fromLTRB(10, 28, 10, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.program.title, + style: theme.textTheme.bodyMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + shadows: [ + const Shadow( + color: Colors.black87, + blurRadius: 4, + offset: Offset(0, 1), + ) + ], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (widget.program.description.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + widget.program.description, + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.white60, + fontSize: 10, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (progress > 0) + Padding( + padding: const EdgeInsets.only(top: 6), + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(2), + child: LinearProgressIndicator( + value: progress, + backgroundColor: Colors.white.withOpacity(0.15), + valueColor: AlwaysStoppedAnimation(accentColor), + minHeight: 4, + ), + ), + ), + const SizedBox(width: 6), + Text( + '${(progress * 100).round()}%', + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.white54, + fontSize: 9, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _emptyPosterFallback(ThemeData theme) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.grey.shade900, + Colors.grey.shade800, + ], + ), + ), + child: Center( + child: FutureBuilder( + future: _iconFuture, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Opacity( + opacity: 0.4, + child: Image.memory(snapshot.data!, width: 48, height: 48), + ); + } + return Icon( + Icons.play_circle_outline, + size: 48, + color: Colors.white.withOpacity(0.15), + ); + }, + ), + ), + ); + } +} diff --git a/lib/widgets/focus_aware_app_bar.dart b/lib/widgets/focus_aware_app_bar.dart index 6b3e3f8d..3ae97df6 100644 --- a/lib/widgets/focus_aware_app_bar.dart +++ b/lib/widgets/focus_aware_app_bar.dart @@ -1,4 +1,8 @@ import 'package:flauncher/widgets/settings/settings_panel.dart'; +import 'package:flauncher/widgets/settings/inputs_panel.dart'; +import 'package:flauncher/widgets/settings/notifications_panel.dart'; +import 'package:flauncher/providers/tv_inputs_service.dart'; +import 'package:flauncher/providers/notifications_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -24,16 +28,26 @@ class FocusAwareAppBarState extends State { bool focused = false; late FocusNode _settingsFocusNode; + late FocusNode _inputsFocusNode; + late FocusNode _notificationsFocusNode; + + FocusNode get settingsFocusNode => _settingsFocusNode; + FocusNode get inputsFocusNode => _inputsFocusNode; + FocusNode get notificationsFocusNode => _notificationsFocusNode; @override void initState() { super.initState(); _settingsFocusNode = FocusNode(); + _inputsFocusNode = FocusNode(); + _notificationsFocusNode = FocusNode(); } @override void dispose() { _settingsFocusNode.dispose(); + _inputsFocusNode.dispose(); + _notificationsFocusNode.dispose(); super.dispose(); } @@ -80,6 +94,69 @@ class FocusAwareAppBarState extends State focusNode: _settingsFocusNode, onPressed: () => showDialog(context: context, builder: (_) => const SettingsPanel()), ), + Selector( + selector: (_, settings) => settings.showInputsWidgetInStatusBar, + builder: (context, showInputs, _) => showInputs + ? Consumer( + builder: (context, tvInputsService, _) { + if (tvInputsService.hasInputs) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 16), + _FocusableIconButton( + icon: Icons.tv_outlined, + focusNode: _inputsFocusNode, + onPressed: () => showDialog( + context: context, + builder: (_) => const InputsPanel(), + ), + ), + ], + ); + } + return const SizedBox.shrink(); + }, + ) + : const SizedBox.shrink(), + ), + Selector( + selector: (_, settings) => ( + settings.showNotificationsWidgetInStatusBar, + settings.autoHideNotificationsWidget + ), + builder: (context, settingsState, _) { + final (showNotifications, autoHide) = settingsState; + return showNotifications + ? Consumer( + builder: (context, notificationsService, _) { + if (notificationsService.hasPermission) { + final count = notificationsService.notifications.length; + if (autoHide && count == 0) { + return const SizedBox.shrink(); + } + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 16), + _FocusableIconButton( + icon: count > 0 ? Icons.notifications_active_outlined : Icons.notifications_outlined, + focusNode: _notificationsFocusNode, + badgeCount: count, + onPressed: () => showDialog( + context: context, + builder: (_) => const NotificationsPanel(), + ), + ), + ], + ); + } + return const SizedBox.shrink(); + }, + ) + : const SizedBox.shrink(); + }, + ), const SizedBox(width: 16), // Network indicator (conditionally shown) Selector( @@ -165,8 +242,9 @@ class _FocusableIconButton extends StatefulWidget { final IconData icon; final VoidCallback onPressed; final FocusNode? focusNode; + final int badgeCount; - const _FocusableIconButton({required this.icon, required this.onPressed, this.focusNode}); + const _FocusableIconButton({required this.icon, required this.onPressed, this.focusNode, this.badgeCount = 0}); @override State<_FocusableIconButton> createState() => _FocusableIconButtonState(); @@ -199,11 +277,21 @@ class _FocusableIconButtonState extends State<_FocusableIconButton> { ? const [BoxShadow(color: Colors.black54, blurRadius: 8, spreadRadius: 1)] : null, ), - child: Icon(widget.icon, - shadows: const [ - Shadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 2)) - ], - ), + child: widget.badgeCount > 0 + ? Badge( + label: Text(widget.badgeCount.toString(), style: const TextStyle(color: Colors.white)), + backgroundColor: Colors.red, + child: Icon(widget.icon, + shadows: const [ + Shadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 2)) + ], + ), + ) + : Icon(widget.icon, + shadows: const [ + Shadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 2)) + ], + ), ), ), ), diff --git a/lib/widgets/focus_keyboard_listener.dart b/lib/widgets/focus_keyboard_listener.dart index 1efea4c5..f6967724 100644 --- a/lib/widgets/focus_keyboard_listener.dart +++ b/lib/widgets/focus_keyboard_listener.dart @@ -39,6 +39,7 @@ class FocusKeyboardListener extends StatefulWidget { class _FocusKeyboardListenerState extends State { int? _keyDownAt; + final Set _handledKeys = {}; @override Widget build(BuildContext context) => Focus( @@ -60,7 +61,11 @@ class _FocusKeyboardListenerState extends State { KeyEventResult _keyDownEvent(BuildContext context, LogicalKeyboardKey key) { if (!longPressableKeys.contains(key)) { - return widget.onPressed?.call(key) ?? KeyEventResult.ignored; + final result = widget.onPressed?.call(key) ?? KeyEventResult.ignored; + if (result == KeyEventResult.handled) { + _handledKeys.add(key); + } + return result; } if (_keyDownAt == null) { _keyDownAt = DateTime.now().millisecondsSinceEpoch; @@ -73,6 +78,9 @@ class _FocusKeyboardListenerState extends State { } KeyEventResult _keyUpEvent(BuildContext context, LogicalKeyboardKey key) { + if (_handledKeys.remove(key)) { + return KeyEventResult.handled; + } if (_keyDownAt != null) { _keyDownAt = null; return widget.onPressed?.call(key) ?? KeyEventResult.ignored; diff --git a/lib/widgets/network_widget.dart b/lib/widgets/network_widget.dart index 26703c97..dc6998bf 100644 --- a/lib/widgets/network_widget.dart +++ b/lib/widgets/network_widget.dart @@ -8,12 +8,12 @@ class NetworkWidget extends StatelessWidget @override Widget build(BuildContext context) { - return Selector( - selector: (_, ns) => (ns.networkType, ns.cellularNetworkType, ns.wirelessNetworkSignalLevel), + return Selector( + selector: (_, ns) => (ns.networkType, ns.cellularNetworkType, ns.wirelessNetworkSignalLevel, ns.vpnActive), builder: (context, state, _) { - final (networkType, cellularNetworkType, wirelessSignalLevel) = state; + final (networkType, cellularNetworkType, wirelessSignalLevel, vpnActive) = state; final networkService = context.read(); - IconData iconData; + IconData physicalIcon = Icons.link_off; Color? iconColor; switch (networkType) @@ -22,63 +22,89 @@ class NetworkWidget extends StatelessWidget switch (cellularNetworkType) { case CellularNetworkType.Cdma || CellularNetworkType.Gsm || CellularNetworkType.Gprs: - iconData = Icons.g_mobiledata; + physicalIcon = Icons.g_mobiledata; - case CellularNetworkType.Edge: iconData = Icons.e_mobiledata; + case CellularNetworkType.Edge: physicalIcon = Icons.e_mobiledata; case CellularNetworkType.Hspa || CellularNetworkType.Hsdpa || CellularNetworkType.Hsupa: - iconData = Icons.h_mobiledata; + physicalIcon = Icons.h_mobiledata; - case CellularNetworkType.Hspap: iconData = Icons.h_plus_mobiledata; + case CellularNetworkType.Hspap: physicalIcon = Icons.h_plus_mobiledata; case CellularNetworkType.Umts || CellularNetworkType.TdScdma: - iconData = Icons.three_g_mobiledata; break; + physicalIcon = Icons.three_g_mobiledata; break; - case CellularNetworkType.Lte: iconData = Icons.four_g_mobiledata_outlined; break; - case CellularNetworkType.Nr: iconData = Icons.five_g; + case CellularNetworkType.Lte: physicalIcon = Icons.four_g_mobiledata_outlined; break; + case CellularNetworkType.Nr: physicalIcon = Icons.five_g; - default: iconData = Icons.question_mark; break; + default: physicalIcon = Icons.question_mark; break; } break; case NetworkType.Wifi: if (wirelessSignalLevel == 0) { - iconData = Icons.signal_wifi_0_bar; + physicalIcon = Icons.signal_wifi_0_bar; } else if (wirelessSignalLevel == 1) { - iconData = Icons.network_wifi_1_bar; + physicalIcon = Icons.network_wifi_1_bar; } else if (wirelessSignalLevel == 2) { - iconData = Icons.network_wifi_2_bar; + physicalIcon = Icons.network_wifi_2_bar; } else if (wirelessSignalLevel == 3) { - iconData = Icons.network_wifi_3_bar; + physicalIcon = Icons.network_wifi_3_bar; } else { - iconData = Icons.signal_wifi_4_bar; + physicalIcon = Icons.signal_wifi_4_bar; } break; - case NetworkType.Vpn: iconData = Icons.vpn_key; break; - case NetworkType.Wired: iconData = Icons.lan; break; + case NetworkType.Vpn: physicalIcon = Icons.vpn_key; break; + case NetworkType.Wired: physicalIcon = Icons.lan; break; case NetworkType.Unknown: - iconData = Icons.link_off; + physicalIcon = Icons.link_off; iconColor = Colors.red; // Make no connection icon red break; } + final List icons = [ + Icon(physicalIcon, + color: iconColor, + shadows: const [ + Shadow( + color: Colors.black54, + offset: Offset(0, 2), + blurRadius: 8 + ) + ] + ), + ]; + + if (vpnActive) { + icons.add(const SizedBox(width: 4)); + icons.add(const Icon(Icons.vpn_key, + shadows: [ + Shadow( + color: Colors.black54, + offset: Offset(0, 2), + blurRadius: 8 + ) + ] + )); + } + return InkWell( - onTap: () => networkService.openWifiSettings(), + onTap: () { + if (vpnActive) { + networkService.openVpnSettings(); + } else { + networkService.openWifiSettings(); + } + }, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.all(4.0), - child: Icon(iconData, - color: iconColor, - shadows: const [ - Shadow( - color: Colors.black54, - offset: Offset(0, 2), - blurRadius: 8 - ) - ] + child: Row( + mainAxisSize: MainAxisSize.min, + children: icons, ), ), ); diff --git a/lib/widgets/settings/accessibility_page.dart b/lib/widgets/settings/accessibility_page.dart new file mode 100644 index 00000000..92f50469 --- /dev/null +++ b/lib/widgets/settings/accessibility_page.dart @@ -0,0 +1,191 @@ +import 'package:flauncher/flauncher_channel.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; +import 'package:flauncher/providers/apps_service.dart'; +import 'package:flauncher/providers/launcher_state.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'focusable_settings_tile.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class AccessibilityPage extends StatefulWidget { + static const String routeName = "accessibility"; + + const AccessibilityPage({super.key}); + + @override + State createState() => _AccessibilityPageState(); +} + +class _AccessibilityPageState extends State with WidgetsBindingObserver { + bool _accessibilityEnabled = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _refreshStatus(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _refreshStatus(); + } + } + + Future _refreshStatus() async { + final appsService = context.read(); + final launcherState = context.read(); + await launcherState.refresh(appsService); + + final bool enabled = await FLauncherChannel().checkAccessibilityPermission(); + if (mounted) { + setState(() { + _accessibilityEnabled = enabled; + }); + } + } + + @override + Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + LauncherState launcherState = context.watch(); + bool isDefault = launcherState.isDefaultLauncher; + + return Column( + children: [ + Text(localizations.accessibility, style: Theme.of(context).textTheme.titleLarge), + const Divider(), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + FocusableSettingsTile( + autofocus: true, + leading: Icon( + isDefault ? Icons.check_circle : Icons.cancel, + color: isDefault ? Colors.green : Colors.redAccent, + ), + title: Text( + isDefault + ? localizations.defaultLauncherIsDefault + : localizations.defaultLauncherNotDefault, + style: Theme.of(context).textTheme.bodyMedium, + ), + onPressed: _refreshStatus, + ), + const SizedBox(height: 8), + FocusableSettingsTile( + leading: const Icon(Icons.home), + title: Text( + localizations.setAsDefaultLauncher, + style: Theme.of(context).textTheme.bodyMedium, + ), + onPressed: () { + FLauncherChannel().openDefaultLauncherSettings(); + }, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + localizations.defaultLauncherDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white54, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 8), + FocusableSettingsTile( + leading: Icon( + _accessibilityEnabled ? Icons.settings_accessibility : Icons.accessibility_new, + color: _accessibilityEnabled ? Colors.green : Colors.orange, + ), + title: Text( + 'Home Button Fix (Google TV)', + style: Theme.of(context).textTheme.bodyMedium, + ), + trailing: Text( + _accessibilityEnabled ? 'Enabled' : 'Disabled', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: _accessibilityEnabled ? Colors.green : Colors.orange, + ), + ), + onPressed: () async { + final success = await FLauncherChannel().requestAccessibilityPermission(); + if (!success && context.mounted) { + _showAccessibilityPermissionGuide(context); + } + }, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + 'If you are using Google TV, enable "Home Button Fix" under Accessibility settings to make the Home button open this launcher.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white54, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ], + ); + } + + Future _showAccessibilityPermissionGuide(BuildContext context) async { + final packageInfo = await PackageInfo.fromPlatform(); + final packageName = packageInfo.packageName; + if (!context.mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Accessibility Permission'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'On this device, the Accessibility settings screen could not be opened automatically.\n\n' + 'To enable Home Button Fix, you can grant permission manually by running this ADB command from a computer connected to the TV:', + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(4), + ), + child: SelectableText( + 'adb shell settings put secure enabled_accessibility_services $packageName/$packageName.LauncherAccessibilityService', + style: const TextStyle(fontFamily: 'monospace', fontSize: 12), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/settings/backup_restore_page.dart b/lib/widgets/settings/backup_restore_page.dart new file mode 100644 index 00000000..45cb1a7d --- /dev/null +++ b/lib/widgets/settings/backup_restore_page.dart @@ -0,0 +1,238 @@ +import 'dart:io'; +import 'package:flauncher/providers/apps_service.dart'; +import 'package:flauncher/providers/backup_service.dart'; +import 'package:flauncher/providers/settings_service.dart'; +import 'package:flauncher/widgets/settings/focusable_settings_tile.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; +import 'package:share_plus/share_plus.dart'; + +class BackupRestorePage extends StatelessWidget { + static const String routeName = "backup_restore_panel"; + + const BackupRestorePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + + return Column( + children: [ + Text(localizations.backupAndRestore, style: Theme.of(context).textTheme.titleLarge), + const Divider(), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + FocusableSettingsTile( + autofocus: true, + leading: const Icon(Icons.upload_file), + title: Text(localizations.exportBackup, style: Theme.of(context).textTheme.bodyMedium), + onPressed: () => _export(context, localizations), + ), + FocusableSettingsTile( + leading: const Icon(Icons.download_done), + title: Text(localizations.importBackup, style: Theme.of(context).textTheme.bodyMedium), + onPressed: () => _confirmImport(context, localizations), + ), + FocusableSettingsTile( + leading: const Icon(Icons.share), + title: Text(localizations.shareBackup, style: Theme.of(context).textTheme.bodyMedium), + onPressed: () => _share(context, localizations), + ), + ], + ), + ), + ), + ], + ); + } + + Future _share(BuildContext context, AppLocalizations localizations) async { + try { + final pathStr = await context.read().exportBackup(); + await Share.shareXFiles([XFile(pathStr)], text: 'LTvLauncher Backup'); + } catch (e) { + if (context.mounted) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Share Failed"), + content: Text("Failed to share backup: $e"), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("OK"), + ), + ], + ), + ); + } + } + } + + Future _export(BuildContext context, AppLocalizations localizations) async { + try { + final path = await context.read().exportBackup(); + if (context.mounted) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Export Success"), + content: Text(localizations.exportSuccess(path)), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("OK"), + ), + ], + ), + ); + } + } catch (e) { + if (context.mounted) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Export Failed"), + content: Text(localizations.exportError(e.toString())), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("OK"), + ), + ], + ), + ); + } + } + } + + String _formatSize(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + } + + String _formatDate(DateTime dateTime) { + return "${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} " + "${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}"; + } + + Future _confirmImport(BuildContext context, AppLocalizations localizations) async { + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Text(localizations.importBackup), + content: Container( + width: 400, + constraints: const BoxConstraints(maxHeight: 300), + child: FutureBuilder>( + future: context.read().getBackupFiles(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox( + height: 100, + child: Center(child: CircularProgressIndicator()), + ); + } + if (snapshot.hasError) { + return Text( + "Error loading backups: ${snapshot.error}", + style: const TextStyle(color: Colors.red), + ); + } + final entries = snapshot.data ?? []; + if (entries.isEmpty) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 24.0), + child: Text("No backup files found."), + ), + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text("OK"), + ), + ], + ); + } + return ListView.builder( + shrinkWrap: true, + itemCount: entries.length, + itemBuilder: (context, index) { + final entry = entries[index]; + return FocusableSettingsTile( + leading: const Icon(Icons.settings_backup_restore), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + entry.name, + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + "${_formatDate(entry.lastModified)} (${_formatSize(entry.size)})", + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey, + ), + ), + ], + ), + onPressed: () { + Navigator.of(dialogContext).pop(); + _confirmFileImport(context, localizations, entry); + }, + ); + }, + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text("Cancel"), + ), + ], + ); + }, + ); + } + + Future _confirmFileImport( + BuildContext context, AppLocalizations localizations, BackupFileEntry entry) async { + final backupService = context.read(); + final settingsService = context.read(); + final appsService = context.read(); + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(localizations.importBackup), + content: Text(localizations.importConfirm), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () async { + Navigator.of(dialogContext).pop(); + try { + await backupService.importBackup(entry.file); + settingsService.reload(); + await appsService.refreshState(); + } catch (_) {} + }, + child: const Text("Import"), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/settings/general_settings_page.dart b/lib/widgets/settings/general_settings_page.dart index e9192a5f..613fb782 100644 --- a/lib/widgets/settings/general_settings_page.dart +++ b/lib/widgets/settings/general_settings_page.dart @@ -19,12 +19,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flauncher/l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:flauncher/providers/notifications_service.dart'; import 'focusable_settings_tile.dart'; import 'brightness_settings_page.dart'; import 'date_time_format_page.dart'; import 'back_button_action_page.dart'; import 'data_usage_period_page.dart'; import 'screensaver_clock_style_page.dart'; +import 'backup_restore_page.dart'; class GeneralSettingsPage extends StatelessWidget { @@ -75,7 +79,65 @@ class GeneralSettingsPage extends StatelessWidget { title: Text('Data Usage Period', style: Theme.of(context).textTheme.bodyMedium), onPressed: () => Navigator.of(context).pushNamed(DataUsagePeriodPage.routeName), ), - + FocusableSettingsTile( + leading: const Icon(Icons.settings_backup_restore), + title: Text(localizations.backupAndRestore, style: Theme.of(context).textTheme.bodyMedium), + onPressed: () => Navigator.of(context).pushNamed(BackupRestorePage.routeName), + ), + Consumer( + builder: (context, service, _) { + return Column( + children: [ + FocusableSettingsTile( + leading: const Icon(Icons.notifications_active_outlined), + title: Text('Notification Access', style: Theme.of(context).textTheme.bodyMedium), + trailing: Text( + service.hasPermission ? 'Granted' : 'Permission Required', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: service.hasPermission ? Colors.green : Colors.orange, + ), + ), + onPressed: () async { + if (!service.hasPermission) { + final success = await service.requestPermission(); + if (!success && context.mounted) { + _showNotificationPermissionGuide(context); + } + } else { + await service.checkPermission(); + } + }, + ), + if (service.hasPermission) + FocusableSettingsTile( + leading: const Icon(Icons.picture_in_picture_alt_outlined), + title: Text('System-wide Popup Alert', style: Theme.of(context).textTheme.bodyMedium), + trailing: Text( + !service.hasOverlayPermission + ? 'Overlay Permission Required' + : (service.systemPopupEnabled ? 'Enabled' : 'Disabled'), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: !service.hasOverlayPermission + ? Colors.orange + : (service.systemPopupEnabled ? Colors.green : Colors.grey), + ), + ), + onPressed: () async { + await service.checkOverlayPermission(); + if (!service.hasOverlayPermission) { + final success = await service.requestOverlayPermission(); + if (!success && context.mounted) { + _showOverlayPermissionGuide(context); + } + } else { + await service.setSystemPopupEnabled(!service.systemPopupEnabled); + } + }, + ), + ], + ); + }, + ), ], ), ), @@ -84,6 +146,88 @@ class GeneralSettingsPage extends StatelessWidget { ); } + Future _showNotificationPermissionGuide(BuildContext context) async { + final packageInfo = await PackageInfo.fromPlatform(); + final packageName = packageInfo.packageName; + if (!context.mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Notification Access'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'On this device, the Notification Access settings screen could not be opened automatically.\n\n' + 'To enable notifications, you can grant permission manually by running this ADB command from a computer connected to the TV:', + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(4), + ), + child: SelectableText( + 'adb shell cmd notification allow_listener $packageName/$packageName.LauncherNotificationListenerService', + style: const TextStyle(fontFamily: 'monospace', fontSize: 12), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + + Future _showOverlayPermissionGuide(BuildContext context) async { + final packageInfo = await PackageInfo.fromPlatform(); + final packageName = packageInfo.packageName; + if (!context.mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Overlay Permission'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'On this device, the Overlay Permission settings screen could not be opened automatically.\n\n' + 'To enable overlay popups, you can grant permission manually by running this ADB command from a computer connected to the TV:', + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(4), + ), + child: SelectableText( + 'adb shell appops set $packageName SYSTEM_ALERT_WINDOW allow', + style: const TextStyle(fontFamily: 'monospace', fontSize: 12), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + Future _openScreensaverSettings() async { const platform = MethodChannel('me.efesser.flauncher/method'); platform.invokeMethod('openScreensaverSettings'); diff --git a/lib/widgets/settings/inputs_panel.dart b/lib/widgets/settings/inputs_panel.dart new file mode 100644 index 00000000..d130740c --- /dev/null +++ b/lib/widgets/settings/inputs_panel.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flauncher/widgets/side_panel_dialog.dart'; +import 'package:flauncher/widgets/settings/focusable_settings_tile.dart'; +import 'package:flauncher/providers/tv_inputs_service.dart'; +import 'package:flauncher/models/tv_input.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; + +class InputsPanel extends StatelessWidget { + const InputsPanel({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + final theme = Theme.of(context); + + return Scaffold( + backgroundColor: Colors.black54, // Dim background + body: Stack( + children: [ + // Tap outside to close + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container(color: Colors.transparent), + ), + SidePanelDialog( + width: 350, + isRightSide: false, + child: Consumer( + builder: (context, tvInputsService, _) { + final List inputs = tvInputsService.inputs; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), + child: Text( + localizations.inputSources, + style: theme.textTheme.titleLarge, + ), + ), + const Divider(), + Expanded( + child: inputs.isEmpty + ? Center( + child: Text( + "No inputs detected", + style: theme.textTheme.bodyMedium, + ), + ) + : ListView.builder( + itemCount: inputs.length, + itemBuilder: (context, index) { + final input = inputs[index]; + IconData iconData; + switch (input.type) { + case TvInputType.hdmi: + iconData = Icons.settings_input_hdmi; + break; + case TvInputType.tuner: + iconData = Icons.settings_input_antenna_outlined; + break; + case TvInputType.av: + iconData = Icons.settings_input_component_outlined; + break; + default: + iconData = Icons.input_outlined; + } + + return FocusableSettingsTile( + leading: Icon(iconData, color: theme.colorScheme.primary), + title: Text( + input.label, + style: theme.textTheme.bodyMedium, + ), + autofocus: index == 0, + onPressed: () async { + Navigator.of(context).pop(); + await tvInputsService.switchInput(input.id); + }, + ); + }, + ), + ), + ], + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/settings/launcher_section_panel_page.dart b/lib/widgets/settings/launcher_section_panel_page.dart index 1582a830..03036788 100644 --- a/lib/widgets/settings/launcher_section_panel_page.dart +++ b/lib/widgets/settings/launcher_section_panel_page.dart @@ -187,6 +187,41 @@ class LauncherSectionPanelPage extends StatelessWidget children: [ Text(title, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center), Divider(), + if (creating) ...[ + _listTile( + context, + Text(localizations.type), + Padding( + padding: EdgeInsets.only(top: 4), + child: DropdownButtonFormField( + decoration: InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.white, width: 2)), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + isDense: true, + isExpanded: true, + value: sectionType, + onChanged: (value) { + if (value != null) { + state.setSectionType(value); + } + }, + items: [ + DropdownMenuItem( + value: LauncherSectionType.Category, + child: Text(localizations.category, style: Theme.of(context).textTheme.bodySmall), + ), + DropdownMenuItem( + value: LauncherSectionType.Spacer, + child: Text(localizations.spacer, style: Theme.of(context).textTheme.bodySmall), + ), + ], + ), + ), + ), + Divider(), + ], sectionSpecificSettings, Divider(), Selector<_SettingsState, bool>( diff --git a/lib/widgets/settings/launcher_sections_panel_page.dart b/lib/widgets/settings/launcher_sections_panel_page.dart index 05a53bc6..11dda296 100644 --- a/lib/widgets/settings/launcher_sections_panel_page.dart +++ b/lib/widgets/settings/launcher_sections_panel_page.dart @@ -36,6 +36,19 @@ class LauncherSectionsPanelPage extends StatefulWidget { class _LauncherSectionsPanelPageState extends State { int? _movingIndex; + late AppsService _appsService; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _appsService = Provider.of(context, listen: false); + } + + @override + void dispose() { + _appsService.persistSectionsOrder(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -50,6 +63,7 @@ class _LauncherSectionsPanelPageState extends State { return Expanded( child: ReorderableListView.builder( + cacheExtent: 2000, buildDefaultDragHandles: false, padding: EdgeInsets.only(bottom: 80), itemCount: sections.length, diff --git a/lib/widgets/settings/misc_panel_page.dart b/lib/widgets/settings/misc_panel_page.dart index 82879a0e..9aeb6bdb 100644 --- a/lib/widgets/settings/misc_panel_page.dart +++ b/lib/widgets/settings/misc_panel_page.dart @@ -1,7 +1,7 @@ import 'package:flauncher/providers/settings_service.dart'; +import 'package:flauncher/providers/watch_next_service.dart'; import 'package:flauncher/widgets/rounded_switch_list_tile.dart'; -import 'package:flauncher/widgets/settings/focusable_settings_tile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flauncher/l10n/app_localizations.dart'; @@ -61,6 +61,32 @@ class MiscPanelPage extends StatelessWidget { title: Text(localizations.appSelectorTransitionAnimation, style: Theme.of(context).textTheme.bodyMedium), secondary: Icon(Icons.animation), ), + RoundedSwitchListTile( + value: settingsService.showContinueWatching, + onChanged: (value) async { + if (value) { + final watchNextService = Provider.of(context, listen: false); + final hasPermission = await watchNextService.checkPermission(); + if (!context.mounted) return; + if (!hasPermission) { + final granted = await watchNextService.requestPermission(); + if (!context.mounted) return; + if (!granted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(localizations.permissionDeniedContinueWatching), + duration: const Duration(seconds: 3), + ), + ); + return; + } + } + } + settingsService.setShowContinueWatching(value); + }, + title: Text(localizations.showContinueWatchingOnHome, style: Theme.of(context).textTheme.bodyMedium), + secondary: const Icon(Icons.play_circle_outline), + ), ], ), ), diff --git a/lib/widgets/settings/notifications_panel.dart b/lib/widgets/settings/notifications_panel.dart new file mode 100644 index 00000000..25332722 --- /dev/null +++ b/lib/widgets/settings/notifications_panel.dart @@ -0,0 +1,181 @@ +import 'package:collection/collection.dart'; +import 'package:flauncher/providers/apps_service.dart'; +import 'package:flauncher/providers/notifications_service.dart'; +import 'package:flauncher/widgets/settings/focusable_settings_tile.dart'; +import 'package:flauncher/widgets/side_panel_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +class NotificationsPanel extends StatelessWidget { + const NotificationsPanel({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + backgroundColor: Colors.black54, // Dim background + body: Stack( + children: [ + // Tap outside to close + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container(color: Colors.transparent), + ), + SidePanelDialog( + width: 400, + isRightSide: false, + child: Consumer2( + builder: (context, notificationsService, appsService, _) { + final List notifications = notificationsService.notifications; + final bool hasClearable = notifications.any((n) => n.isClearable); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Notifications", + style: theme.textTheme.titleLarge, + ), + if (hasClearable) + TextButton.icon( + onPressed: () async { + await notificationsService.dismissAll(); + }, + icon: const Icon(Icons.clear_all, size: 18), + label: const Text("Clear All"), + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.primary, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + ), + ), + ], + ), + ), + const Divider(), + Expanded( + child: notifications.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.notifications_off_outlined, + size: 64, + color: theme.hintColor.withOpacity(0.3), + ), + const SizedBox(height: 16), + Text( + "All caught up!", + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.hintColor, + ), + ), + ], + ), + ) + : ListView.builder( + itemCount: notifications.length, + itemBuilder: (context, index) { + final notification = notifications[index]; + final app = appsService.applications.firstWhereOrNull( + (a) => a.packageName == notification.packageName, + ); + final appName = app?.name ?? notification.packageName; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + child: Material( + color: theme.cardColor.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + clipBehavior: Clip.antiAlias, + child: Focus( + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowLeft && + notification.isClearable) { + notificationsService.dismiss(notification.key); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: FocusableSettingsTile( + autofocus: index == 0, + leading: FutureBuilder( + future: appsService.getAppIcon(notification.packageName), + builder: (context, snapshot) { + if (snapshot.hasData) { + return ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Image.memory( + snapshot.data, + width: 36, + height: 36, + ), + ); + } + return const Icon(Icons.android, size: 36); + }, + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appName, + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 2), + if (notification.title.isNotEmpty) + Text( + notification.title, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + if (notification.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text( + notification.text, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.hintColor, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + trailing: null, + onPressed: () { + if (app != null) { + Navigator.of(context).pop(); + appsService.launchApp(app); + } + }, + ), + ), + ), + ); + }, + ), + ), + ], + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/settings/settings_panel.dart b/lib/widgets/settings/settings_panel.dart index c27ba26d..868e3152 100644 --- a/lib/widgets/settings/settings_panel.dart +++ b/lib/widgets/settings/settings_panel.dart @@ -35,6 +35,8 @@ import 'package:flauncher/widgets/settings/interface_settings_page.dart'; import 'package:flauncher/widgets/settings/general_settings_page.dart'; import 'package:flauncher/widgets/settings/screensaver_clock_style_page.dart'; import 'package:flauncher/widgets/settings/themes_page.dart'; +import 'package:flauncher/widgets/settings/accessibility_page.dart'; +import 'package:flauncher/widgets/settings/backup_restore_page.dart'; import 'package:flauncher/models/app.dart'; import 'package:flutter/material.dart'; @@ -107,6 +109,10 @@ class _SettingsPanelState extends State { return _FastPageRoute(builder: (_) => AccentColorPage()); case BrightnessSettingsPage.routeName: return _FastPageRoute(builder: (_) => BrightnessSettingsPage()); + case AccessibilityPage.routeName: + return _FastPageRoute(builder: (_) => const AccessibilityPage()); + case BackupRestorePage.routeName: + return _FastPageRoute(builder: (_) => const BackupRestorePage()); case AppDetailsPage.routeName: return _FastPageRoute( builder: (_) => AppDetailsPage(application: settings.arguments as App)); diff --git a/lib/widgets/settings/settings_panel_page.dart b/lib/widgets/settings/settings_panel_page.dart index 3095b144..ea710f3d 100644 --- a/lib/widgets/settings/settings_panel_page.dart +++ b/lib/widgets/settings/settings_panel_page.dart @@ -17,6 +17,7 @@ */ import 'package:flauncher/providers/apps_service.dart'; +import 'package:flauncher/widgets/settings/accessibility_page.dart'; import 'package:flauncher/widgets/settings/applications_panel_page.dart'; import 'package:flauncher/widgets/settings/flauncher_about_dialog.dart'; import 'package:flauncher/widgets/settings/interface_settings_page.dart'; @@ -59,6 +60,11 @@ class SettingsPanelPage extends StatelessWidget { title: Text('System', style: Theme.of(context).textTheme.bodyMedium), onPressed: () => Navigator.of(context).pushNamed(GeneralSettingsPage.routeName), ), + FocusableSettingsTile( + leading: const Icon(Icons.accessibility_new), + title: Text('Accessibility', style: Theme.of(context).textTheme.bodyMedium), + onPressed: () => Navigator.of(context).pushNamed(AccessibilityPage.routeName), + ), const Divider(), FocusableSettingsTile( leading: const Icon(Icons.settings_outlined), diff --git a/lib/widgets/settings/status_bar_panel_page.dart b/lib/widgets/settings/status_bar_panel_page.dart index 862a176c..c0d37343 100644 --- a/lib/widgets/settings/status_bar_panel_page.dart +++ b/lib/widgets/settings/status_bar_panel_page.dart @@ -71,6 +71,25 @@ class StatusBarPanelPage extends StatelessWidget { title: Text('Network Indicator'), secondary: Icon(Icons.signal_wifi_4_bar) ), + RoundedSwitchListTile( + value: settingsService.showInputsWidgetInStatusBar, + onChanged: (value) => settingsService.setShowInputsWidgetInStatusBar(value), + title: Text(localizations.inputs), + secondary: Icon(Icons.tv_outlined), + ), + RoundedSwitchListTile( + value: settingsService.showNotificationsWidgetInStatusBar, + onChanged: (value) => settingsService.setShowNotificationsWidgetInStatusBar(value), + title: Text(localizations.notificationBell), + secondary: Icon(Icons.notifications_outlined), + ), + if (settingsService.showNotificationsWidgetInStatusBar) + RoundedSwitchListTile( + value: settingsService.autoHideNotificationsWidget, + onChanged: (value) => settingsService.setAutoHideNotificationsWidget(value), + title: Text(localizations.autoHideNotificationBell), + secondary: Icon(Icons.notifications_paused_outlined), + ), ], ), ), diff --git a/pubspec.lock b/pubspec.lock index 9365400e..1292338c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.3.0" charcode: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" code_builder: dependency: transitive description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.19.1" + version: "1.18.0" convert: dependency: transitive description: @@ -201,14 +201,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" - dart_service_announcement: - dependency: transitive - description: - name: dart_service_announcement - sha256: "90cac44984b3dd8b509af2f5130c7f179d375a8dab6c87213f734b8ef0172674" - url: "https://pub.dev" - source: hosted - version: "1.2.3" dart_style: dependency: transitive description: @@ -233,22 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.19.1" - drift_local_storage_inspector: - dependency: "direct main" - description: - name: drift_local_storage_inspector - sha256: "2cd4471d0faee164d2b26c3f4c98e9029be79bd87fa210d91e626d16a86e1c80" - url: "https://pub.dev" - source: hosted - version: "0.7.0" fake_async: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.1" ffi: dependency: transitive description: @@ -369,10 +353,10 @@ packages: dependency: "direct main" description: name: google_fonts - sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.2.1" graphs: dependency: transitive description: @@ -481,10 +465,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.19.0" io: dependency: transitive description: @@ -513,26 +497,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" lints: dependency: transitive description: @@ -553,34 +537,34 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.6" mockito: dependency: "direct dev" description: @@ -633,10 +617,10 @@ packages: dependency: "direct main" description: name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -710,13 +694,13 @@ packages: source: hosted version: "2.1.8" pool: - dependency: transitive + dependency: "direct main" description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" posix: dependency: transitive description: @@ -749,14 +733,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" - source: hosted - version: "3.2.2" recase: dependency: transitive description: @@ -813,6 +789,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "59dfd53f497340a0c3a81909b220cfdb9b8973a91055c4e5ab9b9b9ad7c513c0" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + url: "https://pub.dev" + source: hosted + version: "5.0.2" shared_preferences: dependency: "direct main" description: @@ -889,7 +881,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.0" + version: "0.0.99" source_gen: dependency: transitive description: @@ -906,14 +898,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" sqlite3: dependency: transitive description: @@ -942,26 +926,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - storage_inspector: - dependency: transitive - description: - name: storage_inspector - sha256: "79b2b33dde369a2e744f83c36e57d257ed49c50c63bbb97932bfecb045d35459" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -978,14 +954,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" - url: "https://pub.dev" - source: hosted - version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -998,10 +966,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.2" timing: dependency: transitive description: @@ -1018,14 +986,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" - source: hosted - version: "2.0.2" typed_data: dependency: transitive description: @@ -1034,30 +994,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - uri: + url_launcher_linux: dependency: transitive description: - name: uri - sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "3.2.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" uuid: dependency: transitive description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.5.3" vector_math: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" vm_service: dependency: transitive description: @@ -1131,5 +1115,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.5" diff --git a/pubspec.yaml b/pubspec.yaml index 2796e2f4..578fb886 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flauncher description: Flutter-based Android TV launcher. -version: 2026.04.26+6102 +version: 2026.07.12+8106 environment: sdk: ">=3.4.3" @@ -14,18 +14,18 @@ dependencies: image_picker: ^1.0.7 intl: any drift: ^2.14.1 - drift_local_storage_inspector: ^0.7.0 path: ^1.9.0 path_provider: ^2.0.13 package_info_plus: ^7.0.0 provider: ^6.0.5 shared_preferences: ^2.3.3 - google_fonts: ^6.2.1 + google_fonts: 6.2.1 screen_brightness: ^1.0.1 sqlite3_flutter_libs: ^0.5.13 flutter_localizations: sdk: flutter pool: ^1.5.2 + share_plus: ^10.0.0 dev_dependencies: flutter_test: @@ -55,4 +55,4 @@ flutter: generate: true uses-material-design: true assets: - - assets/ + - assets/icon.png diff --git a/test/database_migration_test.dart b/test/database_migration_test.dart index 2c9fccea..449e0df3 100644 --- a/test/database_migration_test.dart +++ b/test/database_migration_test.dart @@ -19,6 +19,7 @@ import 'package:drift/drift.dart'; import 'package:drift_dev/api/migrations.dart'; import 'package:flauncher/database.dart'; +import 'package:flauncher/models/category.dart'; import 'package:flutter_test/flutter_test.dart'; import 'generated_migrations/schema.dart'; @@ -26,7 +27,6 @@ import 'generated_migrations/schema_v1.dart' as v1; import 'generated_migrations/schema_v2.dart' as v2; import 'generated_migrations/schema_v3.dart' as v3; import 'generated_migrations/schema_v4.dart' as v4; -import 'generated_migrations/schema_v5.dart' as v5; void main() { late SchemaVerifier verifier; @@ -35,184 +35,155 @@ void main() { verifier = SchemaVerifier(GeneratedHelper()); }); - test("upgrade from v1 to v5", () async { + test("upgrade from v1 to v8", () async { final schema = await verifier.schemaAt(1); final oldDb = v1.DatabaseAtV1(schema.newConnection().executor); - await oldDb.into(oldDb.apps).insert( - v1.AppsCompanion.insert( - packageName: "me.efesser.flauncher", - name: "FLauncher", - className: ".MainActivity", - version: "0.0.1", - ), - ); - final categoryId = await oldDb.into(oldDb.categories).insert( - v1.CategoriesCompanion.insert(name: "Applications", order: 0), - ); - await oldDb.into(oldDb.appsCategories).insert( - v1.AppsCategoriesCompanion.insert(categoryId: categoryId, appPackageName: "me.efesser.flauncher", order: 0), - ); + await oldDb.customStatement( + "INSERT INTO apps (package_name, name, class_name, version) VALUES ('me.efesser.flauncher', 'FLauncher', '.MainActivity', '0.0.1');", + ); + await oldDb.customStatement( + "INSERT INTO categories (id, name, \"order\") VALUES (1, 'Applications', 0);", + ); + await oldDb.customStatement( + "INSERT INTO apps_categories (category_id, app_package_name, \"order\") VALUES (1, 'me.efesser.flauncher', 0);", + ); await oldDb.close(); final db = FLauncherDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, 5); - await db.close(); + await verifier.migrateAndValidate(db, 8); - final migratedDb = v5.DatabaseAtV5(schema.newConnection().executor); - final v5.AppsData app = await migratedDb.select(migratedDb.apps).getSingle(); - final v5.CategoriesData category = await migratedDb.select(migratedDb.categories).getSingle(); - final v5.AppsCategoriesData appsCategory = await migratedDb.select(migratedDb.appsCategories).getSingle(); + final app = await db.select(db.apps).getSingle(); + final category = await db.select(db.categories).getSingle(); + final appsCategory = await db.select(db.appsCategories).getSingle(); expect(app.packageName, "me.efesser.flauncher"); expect(app.name, "FLauncher"); expect(app.version, "0.0.1"); expect(app.hidden, false); - expect(app.sideloaded, false); expect(category.id, 1); expect(category.name, "Applications"); expect(category.order, 0); - expect(category.sort, 0); - expect(category.type, 1); + expect(category.sort, CategorySort.manual); + expect(category.type, CategoryType.grid); expect(category.columnsCount, 6); expect(category.rowHeight, 110); expect(appsCategory.appPackageName, "me.efesser.flauncher"); expect(appsCategory.categoryId, 1); expect(appsCategory.order, 0); - await migratedDb.close(); + await db.close(); }); - test("upgrade from v2 to v5", () async { + test("upgrade from v2 to v8", () async { final schema = await verifier.schemaAt(2); final oldDb = v2.DatabaseAtV2(schema.newConnection().executor); - await oldDb.into(oldDb.apps).insert( - v2.AppsCompanion.insert( - packageName: "me.efesser.flauncher", - name: "FLauncher", - version: "0.0.1", - ), - ); - final categoryId = await oldDb.into(oldDb.categories).insert( - v2.CategoriesCompanion.insert(name: "Applications", order: 0), - ); - await oldDb.into(oldDb.appsCategories).insert( - v2.AppsCategoriesCompanion.insert(categoryId: categoryId, appPackageName: "me.efesser.flauncher", order: 0), - ); + await oldDb.customStatement( + "INSERT INTO apps (package_name, name, version) VALUES ('me.efesser.flauncher', 'FLauncher', '0.0.1');", + ); + await oldDb.customStatement( + "INSERT INTO categories (id, name, \"order\") VALUES (1, 'Applications', 0);", + ); + await oldDb.customStatement( + "INSERT INTO apps_categories (category_id, app_package_name, \"order\") VALUES (1, 'me.efesser.flauncher', 0);", + ); await oldDb.close(); final db = FLauncherDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, 5); - await db.close(); + await verifier.migrateAndValidate(db, 8); - final migratedDb = v5.DatabaseAtV5(schema.newConnection().executor); - final v5.AppsData app = await migratedDb.select(migratedDb.apps).getSingle(); - final v5.CategoriesData category = await migratedDb.select(migratedDb.categories).getSingle(); - final v5.AppsCategoriesData appsCategory = await migratedDb.select(migratedDb.appsCategories).getSingle(); + final app = await db.select(db.apps).getSingle(); + final category = await db.select(db.categories).getSingle(); + final appsCategory = await db.select(db.appsCategories).getSingle(); expect(app.packageName, "me.efesser.flauncher"); expect(app.name, "FLauncher"); expect(app.version, "0.0.1"); expect(app.hidden, false); - expect(app.sideloaded, false); expect(category.id, 1); expect(category.name, "Applications"); expect(category.order, 0); - expect(category.sort, 0); - expect(category.type, 1); + expect(category.sort, CategorySort.manual); + expect(category.type, CategoryType.grid); expect(category.columnsCount, 6); expect(category.rowHeight, 110); expect(appsCategory.appPackageName, "me.efesser.flauncher"); expect(appsCategory.categoryId, 1); expect(appsCategory.order, 0); - await migratedDb.close(); + await db.close(); }); - test("upgrade from v3 to v5", () async { + test("upgrade from v3 to v8", () async { final schema = await verifier.schemaAt(3); final oldDb = v3.DatabaseAtV3(schema.newConnection().executor); - await oldDb.into(oldDb.apps).insert( - v3.AppsCompanion.insert( - packageName: "me.efesser.flauncher", - name: "FLauncher", - version: "0.0.1", - ), - ); - final categoryId = await oldDb.into(oldDb.categories).insert( - v3.CategoriesCompanion.insert(name: "Applications", order: 0), - ); - await oldDb.into(oldDb.appsCategories).insert( - v3.AppsCategoriesCompanion.insert(categoryId: categoryId, appPackageName: "me.efesser.flauncher", order: 0), - ); + await oldDb.customStatement( + "INSERT INTO apps (package_name, name, version) VALUES ('me.efesser.flauncher', 'FLauncher', '0.0.1');", + ); + await oldDb.customStatement( + "INSERT INTO categories (id, name, \"order\") VALUES (1, 'Applications', 0);", + ); + await oldDb.customStatement( + "INSERT INTO apps_categories (category_id, app_package_name, \"order\") VALUES (1, 'me.efesser.flauncher', 0);", + ); await oldDb.close(); final db = FLauncherDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, 5); - await db.close(); + await verifier.migrateAndValidate(db, 8); - final migratedDb = v5.DatabaseAtV5(schema.newConnection().executor); - final v5.AppsData app = await migratedDb.select(migratedDb.apps).getSingle(); - final v5.CategoriesData category = await migratedDb.select(migratedDb.categories).getSingle(); - final v5.AppsCategoriesData appsCategory = await migratedDb.select(migratedDb.appsCategories).getSingle(); + final app = await db.select(db.apps).getSingle(); + final category = await db.select(db.categories).getSingle(); + final appsCategory = await db.select(db.appsCategories).getSingle(); expect(app.packageName, "me.efesser.flauncher"); expect(app.name, "FLauncher"); expect(app.version, "0.0.1"); expect(app.hidden, false); - expect(app.sideloaded, false); expect(category.id, 1); expect(category.name, "Applications"); expect(category.order, 0); - expect(category.sort, 0); - expect(category.type, 1); + expect(category.sort, CategorySort.manual); + expect(category.type, CategoryType.grid); expect(category.columnsCount, 6); expect(category.rowHeight, 110); expect(appsCategory.appPackageName, "me.efesser.flauncher"); expect(appsCategory.categoryId, 1); expect(appsCategory.order, 0); - await migratedDb.close(); + await db.close(); }); - test("upgrade from v4 to v5", () async { + test("upgrade from v4 to v8", () async { final schema = await verifier.schemaAt(4); final oldDb = v4.DatabaseAtV4(schema.newConnection().executor); - await oldDb.into(oldDb.apps).insert( - v4.AppsCompanion.insert( - packageName: "me.efesser.flauncher", - name: "FLauncher", - version: "0.0.1", - ), - ); - final categoryId = await oldDb.into(oldDb.categories).insert( - v4.CategoriesCompanion.insert(name: "Applications", type: Value(1), order: 0), - ); - await oldDb.into(oldDb.appsCategories).insert( - v4.AppsCategoriesCompanion.insert(categoryId: categoryId, appPackageName: "me.efesser.flauncher", order: 0), - ); + await oldDb.customStatement( + "INSERT INTO apps (package_name, name, version) VALUES ('me.efesser.flauncher', 'FLauncher', '0.0.1');", + ); + await oldDb.customStatement( + "INSERT INTO categories (id, name, type, \"order\") VALUES (1, 'Applications', 1, 0);", + ); + await oldDb.customStatement( + "INSERT INTO apps_categories (category_id, app_package_name, \"order\") VALUES (1, 'me.efesser.flauncher', 0);", + ); await oldDb.close(); final db = FLauncherDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, 5); - await db.close(); + await verifier.migrateAndValidate(db, 8); - final migratedDb = v5.DatabaseAtV5(schema.newConnection().executor); - final v5.AppsData app = await migratedDb.select(migratedDb.apps).getSingle(); - final v5.CategoriesData category = await migratedDb.select(migratedDb.categories).getSingle(); - final v5.AppsCategoriesData appsCategory = await migratedDb.select(migratedDb.appsCategories).getSingle(); + final app = await db.select(db.apps).getSingle(); + final category = await db.select(db.categories).getSingle(); + final appsCategory = await db.select(db.appsCategories).getSingle(); expect(app.packageName, "me.efesser.flauncher"); expect(app.name, "FLauncher"); expect(app.version, "0.0.1"); expect(app.hidden, false); - expect(app.sideloaded, false); expect(category.id, 1); expect(category.name, "Applications"); expect(category.order, 0); - expect(category.sort, 0); - expect(category.type, 1); + expect(category.sort, CategorySort.manual); + expect(category.type, CategoryType.grid); expect(category.columnsCount, 6); expect(category.rowHeight, 110); expect(appsCategory.appPackageName, "me.efesser.flauncher"); expect(appsCategory.categoryId, 1); expect(appsCategory.order, 0); - await migratedDb.close(); + await db.close(); }); } diff --git a/test/flauncher_channel_test.dart b/test/flauncher_channel_test.dart index ba81b06b..4980d23f 100644 --- a/test/flauncher_channel_test.dart +++ b/test/flauncher_channel_test.dart @@ -248,4 +248,21 @@ void main() { expect(usage, -1); }); + + test("openVpnSettings", () async { + final channel = MethodChannel('me.efesser.flauncher/method'); + bool called = false; + channel.setMockMethodCallHandler((call) async { + if (call.method == "openVpnSettings") { + called = true; + return; + } + fail("Unhandled method name"); + }); + final fLauncherChannel = FLauncherChannel(); + + await fLauncherChannel.openVpnSettings(); + + expect(called, isTrue); + }); } diff --git a/test/flauncher_test.dart b/test/flauncher_test.dart index 68528b5d..d30f02d0 100644 --- a/test/flauncher_test.dart +++ b/test/flauncher_test.dart @@ -21,20 +21,29 @@ import 'package:flauncher/flauncher.dart'; import 'package:flauncher/flauncher_channel.dart'; import 'package:flauncher/gradients.dart'; import 'package:flauncher/providers/apps_service.dart'; +import 'package:flauncher/models/category.dart'; import 'package:flauncher/providers/launcher_state.dart'; import 'package:flauncher/providers/network_service.dart'; import 'package:flauncher/providers/settings_service.dart'; +import 'package:flauncher/providers/tv_inputs_service.dart'; import 'package:flauncher/providers/wallpaper_service.dart'; +import 'package:flauncher/providers/notifications_service.dart'; +import 'package:flauncher/providers/watch_next_service.dart'; import 'package:flauncher/widgets/application_info_panel.dart'; import 'package:flauncher/widgets/apps_grid.dart'; import 'package:flauncher/widgets/category_row.dart'; +import 'package:flauncher/widgets/app_card.dart'; +import 'package:flauncher/widgets/focus_aware_app_bar.dart'; import 'package:flauncher/widgets/settings/settings_panel_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; +import 'package:transparent_image/transparent_image.dart'; import 'helpers.dart'; import 'mocks.dart'; @@ -54,21 +63,19 @@ void main() { final appsService = mkAppService(); final favoritesCategory = fakeCategory(name: "Favorites", order: 0, type: CategoryType.row); final applicationsCategory = fakeCategory(name: "Applications", order: 1); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(favoritesCategory, [ - fakeApp( - packageName: "me.efesser.flauncher.1", - name: "FLauncher 1", - version: "1.0.0", - ) - ]), - CategoryWithApps(applicationsCategory, [ - fakeApp( - packageName: "me.efesser.flauncher.2", - name: "FLauncher 2", - version: "2.0.0", - ) - ]), + favoritesCategory.applications.add(fakeApp( + packageName: "me.efesser.flauncher.1", + name: "FLauncher 1", + version: "1.0.0", + )); + applicationsCategory.applications.add(fakeApp( + packageName: "me.efesser.flauncher.2", + name: "FLauncher 2", + version: "2.0.0", + )); + when(appsService.launcherSections).thenReturn([ + favoritesCategory, + applicationsCategory, ]); await _pumpWidgetWith(tester, appsService); @@ -76,9 +83,9 @@ void main() { expect(find.text("Applications"), findsOneWidget); expect(find.text("Favorites"), findsOneWidget); expect(find.byType(AppsGrid), findsOneWidget); - expect(find.byKey(Key("${applicationsCategory.id}-me.efesser.flauncher.2")), findsOneWidget); + expect(find.byKey(Key("me.efesser.flauncher.2")), findsOneWidget); expect(find.byType(CategoryRow), findsOneWidget); - expect(find.byKey(Key("${favoritesCategory.id}-me.efesser.flauncher.1")), findsOneWidget); + expect(find.byKey(Key("me.efesser.flauncher.1")), findsOneWidget); // This was changed by how the the image is made, I don't know what it now should be //expect(tester.widget(find.byKey(Key("background"))), isA()); @@ -89,9 +96,9 @@ void main() { final appsService = mkAppService(); final applicationsCategory = fakeCategory(name: "Applications", order: 0, type: CategoryType.grid); final favoritesCategory = fakeCategory(name: "Favorites", order: 1, type: CategoryType.row); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(applicationsCategory, []), - CategoryWithApps(favoritesCategory, []), + when(appsService.launcherSections).thenReturn([ + applicationsCategory, + favoritesCategory, ]); await _pumpWidgetWith(tester, appsService); @@ -105,16 +112,16 @@ void main() { testWidgets("Home page displays background image", (tester) async { final appsService = mkAppService(); - when(appsService.categoriesWithApps).thenReturn([]); + when(appsService.launcherSections).thenReturn([]); await _pumpWidgetWith(tester, appsService); - expect(tester.widget(find.byKey(Key("background"))), isA()); + expect(find.byType(Image), findsOneWidget); }); testWidgets("Home page displays background gradient", (tester) async { final appsService = mkAppService(); - when(appsService.categoriesWithApps).thenReturn([]); + when(appsService.launcherSections).thenReturn([]); await _pumpWidgetWithProviders(tester, mkWallpaperService(false), appsService, mkSettingsService()); @@ -123,18 +130,19 @@ void main() { testWidgets("Pressing select on settings icon opens SettingsPanel", (tester) async { final appsService = mkAppService(); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "Favorites", order: 0), []), - CategoryWithApps(fakeCategory(name: "Applications", order: 1), []), + when(appsService.launcherSections).thenReturn([ + fakeCategory(name: "Favorites", order: 0), + fakeCategory(name: "Applications", order: 1), ]); await _pumpWidgetWith(tester, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + final settingsNode = getSettingsFocusNode(tester); + settingsNode!.requestFocus(); await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + expect(find.byType(SettingsPanelPage), findsOneWidget); }); @@ -145,34 +153,45 @@ void main() { name: "FLauncher", version: "1.0.0", ); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "Favorites", order: 0), []), - CategoryWithApps(fakeCategory(name: "Applications", order: 1), [app]), + final fav = fakeCategory(name: "Favorites", order: 0); + final apps = fakeCategory(name: "Applications", order: 1); + apps.applications.add(app); + when(appsService.launcherSections).thenReturn([ + fav, + apps, ]); await _pumpWidgetWith(tester, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.select); + final inkWellFinder = find.descendant( + of: find.byKey(Key("me.efesser.flauncher")), + matching: find.byType(InkWell), + ); + final FocusNode focusNode = tester.widget(inkWellFinder).focusNode!; + focusNode.requestFocus(); await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.select); + await tester.pump(const Duration(milliseconds: 150)); + await tester.pump(const Duration(milliseconds: 500)); + verify(appsService.launchApp(app)); }); testWidgets("Long pressing on app opens ApplicationInfoPanel", (tester) async { final appsService = mkAppService(); final applicationsCategory = fakeCategory(name: "Applications", order: 1); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "Favorites", order: 0), []), - CategoryWithApps(applicationsCategory, [ - fakeApp( - packageName: "me.efesser.flauncher", - name: "FLauncher", - version: "1.0.0", - ) - ]), + applicationsCategory.applications.add(fakeApp( + packageName: "me.efesser.flauncher", + name: "FLauncher", + version: "1.0.0", + )); + when(appsService.launcherSections).thenReturn([ + fakeCategory(name: "Favorites", order: 0), + applicationsCategory, ]); await _pumpWidgetWith(tester, appsService); - await tester.longPress(find.byKey(Key("${applicationsCategory.id}-me.efesser.flauncher"))); + await tester.longPress(find.byKey(Key("me.efesser.flauncher"))); await tester.pump(); expect(find.byType(ApplicationInfoPanel), findsOneWidget); @@ -181,33 +200,61 @@ void main() { testWidgets("AppCard moves in grid", (tester) async { final appsService = mkAppService(); final applicationsCategory = fakeCategory(name: "Applications", order: 1, type: CategoryType.grid); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "Favorites", order: 0), []), - CategoryWithApps(applicationsCategory, [ - fakeApp( - packageName: "me.efesser.flauncher", - name: "FLauncher", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.flauncher.2", - name: "FLauncher 2", - version: "1.0.0", - ) - ]), + applicationsCategory.applications.add(fakeApp( + packageName: "me.efesser.flauncher", + name: "FLauncher", + version: "1.0.0", + )); + applicationsCategory.applications.add(fakeApp( + packageName: "me.efesser.flauncher.2", + name: "FLauncher 2", + version: "1.0.0", + )); + when(appsService.launcherSections).thenReturn([ + fakeCategory(name: "Favorites", order: 0), + applicationsCategory, ]); await _pumpWidgetWith(tester, appsService); - await tester.longPress(find.byKey(Key("${applicationsCategory.id}-me.efesser.flauncher"))); + await tester.longPress(find.byKey(Key("me.efesser.flauncher"))); + await tester.pumpAndSettle(); + await tester.tap(find.text("Reorder")); + await tester.pumpAndSettle(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + verify(appsService.reorderApplication(applicationsCategory, 0, 1)); + await tester.sendKeyEvent(LogicalKeyboardKey.select); await tester.pump(); + verify(appsService.saveApplicationOrderInCategory(applicationsCategory)); + }); + + testWidgets("AppCard reorder cancels on Back button", (tester) async { + final appsService = mkAppService(); + final applicationsCategory = fakeCategory(name: "Applications", order: 1, type: CategoryType.grid); + applicationsCategory.applications.add(fakeApp( + packageName: "me.efesser.flauncher", + name: "FLauncher", + version: "1.0.0", + )); + applicationsCategory.applications.add(fakeApp( + packageName: "me.efesser.flauncher.2", + name: "FLauncher 2", + version: "1.0.0", + )); + when(appsService.launcherSections).thenReturn([ + fakeCategory(name: "Favorites", order: 0), + applicationsCategory, + ]); + await _pumpWidgetWith(tester, appsService); + + await tester.longPress(find.byKey(Key("me.efesser.flauncher"))); + await tester.pumpAndSettle(); + await tester.tap(find.text("Reorder")); + await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pump(); verify(appsService.reorderApplication(applicationsCategory, 0, 1)); - await tester.sendKeyEvent(LogicalKeyboardKey.select); + await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonB); await tester.pump(); verify(appsService.saveApplicationOrderInCategory(applicationsCategory)); }); @@ -215,29 +262,26 @@ void main() { testWidgets("AppCard moves in row", (tester) async { final appsService = mkAppService(); final applicationsCategory = fakeCategory(name: "Applications", order: 1, type: CategoryType.row); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "Favorites", order: 0), []), - CategoryWithApps(applicationsCategory, [ - fakeApp( - packageName: "me.efesser.flauncher", - name: "FLauncher", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.flauncher.2", - name: "FLauncher 2", - version: "1.0.0", - ) - ]), + applicationsCategory.applications.add(fakeApp( + packageName: "me.efesser.flauncher", + name: "FLauncher", + version: "1.0.0", + )); + applicationsCategory.applications.add(fakeApp( + packageName: "me.efesser.flauncher.2", + name: "FLauncher 2", + version: "1.0.0", + )); + when(appsService.launcherSections).thenReturn([ + fakeCategory(name: "Favorites", order: 0), + applicationsCategory, ]); await _pumpWidgetWith(tester, appsService); - await tester.longPress(find.byKey(Key("${applicationsCategory.id}-me.efesser.flauncher"))); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pump(); + await tester.longPress(find.byKey(Key("me.efesser.flauncher"))); + await tester.pumpAndSettle(); + await tester.tap(find.text("Reorder")); + await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pump(); verify(appsService.reorderApplication(applicationsCategory, 0, 1)); @@ -256,81 +300,93 @@ void main() { * ▭ ▭ * ▭ ▭ ▭ */ - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "tv", order: 0), [ - fakeApp( - packageName: "me.efesser.tv1", - name: "tv 1", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.tv2", - name: "tv 2", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.tv3", - name: "tv 3", - version: "1.0.0", - ) - ]), - CategoryWithApps(fakeCategory(name: "music", order: 1), [ - fakeApp( - packageName: "me.efesser.music1", - name: "music 1", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.music2", - name: "music 2", - version: "1.0.0", - ) - ]), - CategoryWithApps(fakeCategory(name: "games", order: 2), [ - fakeApp( - packageName: "me.efesser.game1", - name: "game 1", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.game2", - name: "game 2", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.game3", - name: "game 3", - version: "1.0.0", - ) - ]), + final tvCat = fakeCategory(name: "tv", order: 0); + tvCat.applications.addAll([ + fakeApp( + packageName: "me.efesser.tv1", + name: "tv 1", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.tv2", + name: "tv 2", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.tv3", + name: "tv 3", + version: "1.0.0", + ), + ]); + final musicCat = fakeCategory(name: "music", order: 1); + musicCat.applications.addAll([ + fakeApp( + packageName: "me.efesser.music1", + name: "music 1", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.music2", + name: "music 2", + version: "1.0.0", + ), + ]); + final gamesCat = fakeCategory(name: "games", order: 2); + gamesCat.applications.addAll([ + fakeApp( + packageName: "me.efesser.game1", + name: "game 1", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.game2", + name: "game 2", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.game3", + name: "game 3", + version: "1.0.0", + ), + ]); + when(appsService.launcherSections).thenReturn([ + tvCat, + musicCat, + gamesCat, ]); await _pumpWidgetWith(tester, appsService); // when await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.pump(); // then Element? tv1 = findAppCardByPackageName(tester, "me.efesser.tv1"); expect(tv1, isNotNull); Element? music2 = findAppCardByPackageName(tester, "me.efesser.music2"); expect(music2, isNotNull); - expect(Focus.of(tv1!).hasFocus, isFalse); - expect(Focus.of(music2!).hasFocus, isTrue); // this is new, before it was going straight to the third row + expect(isAppCardFocused(tester, "me.efesser.tv1"), isFalse); + expect(isAppCardFocused(tester, "me.efesser.music2"), isTrue); // this is new, before it was going straight to the third row await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.pump(); Element? game2 = findAppCardByPackageName(tester, "me.efesser.game2"); expect(game2, isNotNull); - expect(Focus.of(tv1).hasFocus, isFalse); - expect(Focus.of(music2).hasFocus, isFalse); - expect(Focus.of(game2!).hasFocus, isTrue); + expect(isAppCardFocused(tester, "me.efesser.tv1"), isFalse); + expect(isAppCardFocused(tester, "me.efesser.music2"), isFalse); + expect(isAppCardFocused(tester, "me.efesser.game2"), isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - expect(Focus.of(tv1).hasFocus, isFalse); - expect(Focus.of(music2).hasFocus, isTrue); - expect(Focus.of(game2).hasFocus, isFalse); + await tester.pump(); + expect(isAppCardFocused(tester, "me.efesser.tv1"), isFalse); + expect(isAppCardFocused(tester, "me.efesser.music2"), isTrue); + expect(isAppCardFocused(tester, "me.efesser.game2"), isFalse); }); testWidgets("Moving left or right stays on the same row", (tester) async { @@ -342,46 +398,50 @@ void main() { * ▭ ▭ * ▭ ▭ ▭ ▭ ▭ */ - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "tv", order: 0), [ - fakeApp( - packageName: "me.efesser.tv1", - name: "tv 1", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.tv2", - name: "tv 2", - version: "1.0.0", - ), - ]), - CategoryWithApps(fakeCategory(name: "music", order: 1, columnsCount: 5), [ - fakeApp( - packageName: "me.efesser.music1", - name: "music 1", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.music2", - name: "music 2", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.music3", - name: "music 3", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.music4", - name: "music 4", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.music5", - name: "music 5", - version: "1.0.0", - ), - ]), + final tvCat = fakeCategory(name: "tv", order: 0); + tvCat.applications.addAll([ + fakeApp( + packageName: "me.efesser.tv1", + name: "tv 1", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.tv2", + name: "tv 2", + version: "1.0.0", + ), + ]); + final musicCat = fakeCategory(name: "music", order: 1, columnsCount: 5); + musicCat.applications.addAll([ + fakeApp( + packageName: "me.efesser.music1", + name: "music 1", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.music2", + name: "music 2", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.music3", + name: "music 3", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.music4", + name: "music 4", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.music5", + name: "music 5", + version: "1.0.0", + ), + ]); + when(appsService.launcherSections).thenReturn([ + tvCat, + musicCat, ]); await _pumpWidgetWith(tester, appsService); @@ -389,50 +449,58 @@ void main() { // then Element? tv1 = findAppCardByPackageName(tester, "me.efesser.tv1"); expect(tv1, isNotNull); - expect(Focus.of(tv1!).hasFocus, isTrue); + expect(isAppCardFocused(tester, "me.efesser.tv1"), isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.pump(); Element? music1 = findAppCardByPackageName(tester, "me.efesser.music1"); expect(music1, isNotNull); - expect(Focus.of(tv1).hasFocus, isFalse); - expect(Focus.of(music1!).hasFocus, isTrue); + expect(isAppCardFocused(tester, "me.efesser.tv1"), isFalse); + expect(isAppCardFocused(tester, "me.efesser.music1"), isTrue); // check right direction await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); Element? music2 = findAppCardByPackageName(tester, "me.efesser.music2"); expect(music2, isNotNull); - expect(Focus.of(tv1).hasFocus, isFalse); - expect(Focus.of(music1).hasFocus, isFalse); - expect(Focus.of(music2!).hasFocus, isTrue); + expect(isAppCardFocused(tester, "me.efesser.tv1"), isFalse); + expect(isAppCardFocused(tester, "me.efesser.music1"), isFalse); + expect(isAppCardFocused(tester, "me.efesser.music2"), isTrue); // check if right on the last app stays on the same app await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); Element? music5 = findAppCardByPackageName(tester, "me.efesser.music5"); expect(music5, isNotNull); - // Element? settings = findSettingsIcon(tester); - // expect(settings, isNotNull); - expect(Focus.of(music5!).hasFocus, isTrue); - // expect(Focus.of(settings!).hasFocus, isTrue); + expect(isAppCardFocused(tester, "me.efesser.music5"), isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); Element? tv2 = findAppCardByPackageName(tester, "me.efesser.tv2"); expect(tv2, isNotNull); - expect(Focus.of(tv2!).hasFocus, isTrue); + expect(isAppCardFocused(tester, "me.efesser.tv2"), isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - expect(Focus.of(music2).hasFocus, isTrue); + await tester.pump(); + expect(isAppCardFocused(tester, "me.efesser.music2"), isTrue); // check left direction await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); - expect(Focus.of(music1).hasFocus, isTrue); + await tester.pump(); + expect(isAppCardFocused(tester, "me.efesser.music1"), isTrue); // check if going left on the first app stays on the same app await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); - expect(Focus.of(music1).hasFocus, isTrue); + await tester.pump(); + expect(isAppCardFocused(tester, "me.efesser.music1"), isTrue); }); testWidgets("Moving right or up can go the settings icon", (tester) async { @@ -444,36 +512,40 @@ void main() { * ▭ ▭ * ▭ ▭ ▭ */ - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "tv", order: 0), [ - fakeApp( - packageName: "me.efesser.tv1", - name: "tv 1", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.tv2", - name: "tv 2", - version: "1.0.0", - ), - ]), - CategoryWithApps(fakeCategory(name: "music", order: 1), [ - fakeApp( - packageName: "me.efesser.music1", - name: "music 1", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.music2", - name: "music 2", - version: "1.0.0", - ), - fakeApp( - packageName: "me.efesser.music3", - name: "music 3", - version: "1.0.0", - ), - ]), + final tvCat = fakeCategory(name: "tv", order: 0); + tvCat.applications.addAll([ + fakeApp( + packageName: "me.efesser.tv1", + name: "tv 1", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.tv2", + name: "tv 2", + version: "1.0.0", + ), + ]); + final musicCat = fakeCategory(name: "music", order: 1); + musicCat.applications.addAll([ + fakeApp( + packageName: "me.efesser.music1", + name: "music 1", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.music2", + name: "music 2", + version: "1.0.0", + ), + fakeApp( + packageName: "me.efesser.music3", + name: "music 3", + version: "1.0.0", + ), + ]); + when(appsService.launcherSections).thenReturn([ + tvCat, + musicCat, ]); await _pumpWidgetWith(tester, appsService); @@ -481,34 +553,35 @@ void main() { // then Element? tv1 = findAppCardByPackageName(tester, "me.efesser.tv1"); expect(tv1, isNotNull); - expect(Focus.of(tv1!).hasFocus, isTrue); + expect(isAppCardFocused(tester, "me.efesser.tv1"), isTrue); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - // No idea why I had to add another arrowRight - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); Element? settingsIcon = findSettingsIcon(tester); expect(settingsIcon, isNotNull); - expect(Focus.of(tv1).hasFocus, isFalse); - expect(Focus.of(settingsIcon!).hasFocus, isTrue); + expect(isAppCardFocused(tester, "me.efesser.tv1"), isFalse); + expect(isSettingsIconFocused(tester), isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - Element? tv2 = findAppCardByPackageName(tester, "me.efesser.tv2"); - expect(tv2, isNotNull); - expect(Focus.of(settingsIcon).hasFocus, isFalse); - expect(Focus.of(tv1).hasFocus, isFalse); - expect(Focus.of(tv2!).hasFocus, isTrue); + await tester.pump(); + expect(isSettingsIconFocused(tester), isFalse); + expect(isAppCardFocused(tester, "me.efesser.tv1"), isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - expect(Focus.of(settingsIcon).hasFocus, isTrue); + await tester.pump(); + expect(isSettingsIconFocused(tester), isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - expect(Focus.of(settingsIcon).hasFocus, isTrue); + await tester.pump(); + expect(isAppCardFocused(tester, "me.efesser.tv2"), isTrue); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + expect(isSettingsIconFocused(tester), isTrue); }); } @@ -517,18 +590,60 @@ SettingsService mkSettingsService() { when(settingsService.dateFormat).thenReturn(SettingsService.defaultDateFormat); when(settingsService.timeFormat).thenReturn(SettingsService.defaultTimeFormat); when(settingsService.appHighlightAnimationEnabled).thenReturn(true); + when(settingsService.showCategoryTitles).thenReturn(true); + when(settingsService.autoHideAppBarEnabled).thenReturn(false); + when(settingsService.showInputsWidgetInStatusBar).thenReturn(true); + when(settingsService.showNetworkIndicatorInStatusBar).thenReturn(true); + when(settingsService.showDataWidgetInStatusBar).thenReturn(true); + when(settingsService.showDateInStatusBar).thenReturn(true); + when(settingsService.showTimeInStatusBar).thenReturn(true); + when(settingsService.accentColorHex).thenReturn('00ff00'); + when(settingsService.showAppNamesBelowIcons).thenReturn(true); + when(settingsService.themes).thenReturn('classic'); + when(settingsService.hideHighlightOutlineOnHomescreen).thenReturn(false); + when(settingsService.appSelectorTransitionAnimationEnabled).thenReturn(true); + when(settingsService.showContinueWatching).thenReturn(false); + when(settingsService.showNotificationsWidgetInStatusBar).thenReturn(true); + when(settingsService.autoHideNotificationsWidget).thenReturn(false); return settingsService; } WallpaperService mkWallpaperService([bool wallpaper = true]) { final wallpaperService = MockWallpaperService(); when(wallpaperService.gradient).thenReturn(FLauncherGradients.greatWhale); - when(wallpaperService.wallpaper).thenReturn(wallpaper ? Image.asset('assets/icon.png').image : null); return wallpaperService; + when(wallpaperService.wallpaper).thenReturn(wallpaper ? Image.asset('assets/icon.png').image : null); + when(wallpaperService.version).thenReturn(0); + return wallpaperService; +} + +TvInputsService mkTvInputsService() { + final tvInputsService = MockTvInputsService(); + when(tvInputsService.hasInputs).thenReturn(false); + return tvInputsService; +} + +NotificationsService mkNotificationsService() { + final notificationsService = MockNotificationsService(); + when(notificationsService.getNotificationCount(any)).thenReturn(0); + when(notificationsService.hasPermission).thenReturn(false); + when(notificationsService.notifications).thenReturn([]); + return notificationsService; +} + +WatchNextService mkWatchNextService() { + final watchNextService = MockWatchNextService(); + when(watchNextService.programs).thenReturn([]); + return watchNextService; } AppsService mkAppService() { final appsService = MockAppsService(); when(appsService.initialized).thenReturn(true); + when(appsService.getAppBanner(any)).thenAnswer((_) async => kTransparentImage); + when(appsService.getAppIcon(any)).thenAnswer((_) async => kTransparentImage); + when(appsService.pendingReorderFocusPackage).thenReturn(null); + when(appsService.hasCustomBanner(any)).thenAnswer((_) async => false); + when(appsService.isAppInFavorites(any)).thenReturn(false); return appsService; } @@ -546,19 +661,63 @@ Future _pumpWidgetWithProviders( AppsService appsService, SettingsService settingsService, ) async { + tester.view.physicalSize = const Size(1920, 1080); + tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: wallpaperService), ChangeNotifierProvider.value(value: appsService), ChangeNotifierProvider.value(value: settingsService), + ChangeNotifierProvider.value(value: mkTvInputsService()), + ChangeNotifierProvider.value(value: mkNotificationsService()), + ChangeNotifierProvider.value(value: mkWatchNextService()), ChangeNotifierProvider(create: (_) => LauncherState()), ChangeNotifierProvider(create: (_) => NetworkService(FLauncherChannel())), ], builder: (_, __) => MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, home: FLauncher(), ), ), ); await tester.pump(Duration(seconds: 30), EnginePhase.sendSemanticsUpdate); } + +FocusNode? getFocusNodeForApp(WidgetTester tester, String packageName) { + final appCardFinder = find.byWidgetPredicate((widget) => + widget is AppCard && widget.application.packageName == packageName + ); + if (appCardFinder.evaluate().isEmpty) return null; + final inkWellFinder = find.descendant( + of: appCardFinder, + matching: find.byType(InkWell), + ); + if (inkWellFinder.evaluate().isEmpty) return null; + final inkWell = tester.widget(inkWellFinder); + return inkWell.focusNode; +} + +FocusNode? getSettingsFocusNode(WidgetTester tester) { + try { + final appBarState = tester.state(find.byType(FocusAwareAppBar)); + return appBarState.settingsFocusNode; + } catch (e) { + return null; + } +} + +bool isAppCardFocused(WidgetTester tester, String packageName) { + final focusNode = getFocusNodeForApp(tester, packageName); + return focusNode?.hasFocus ?? false; +} + +bool isSettingsIconFocused(WidgetTester tester) { + final focusNode = getSettingsFocusNode(tester); + return focusNode?.hasFocus ?? false; +} diff --git a/test/generated_migrations/schema.dart b/test/generated_migrations/schema.dart deleted file mode 100644 index 9ccb3b32..00000000 --- a/test/generated_migrations/schema.dart +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -//@dart=2.12 -import 'package:drift/drift.dart'; -import 'package:drift/internal/migrations.dart'; -import 'schema_v1.dart' as v1; -import 'schema_v2.dart' as v2; -import 'schema_v3.dart' as v3; -import 'schema_v4.dart' as v4; -import 'schema_v5.dart' as v5; - -class GeneratedHelper implements SchemaInstantiationHelper { - @override - GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { - switch (version) { - case 1: - return v1.DatabaseAtV1(db); - case 2: - return v2.DatabaseAtV2(db); - case 3: - return v3.DatabaseAtV3(db); - case 4: - return v4.DatabaseAtV4(db); - case 5: - return v5.DatabaseAtV5(db); - default: - throw MissingSchemaException(version, const {1, 2, 3, 4, 5}); - } - } -} diff --git a/test/generated_migrations/schema_v1.dart b/test/generated_migrations/schema_v1.dart deleted file mode 100644 index 5595c528..00000000 --- a/test/generated_migrations/schema_v1.dart +++ /dev/null @@ -1,545 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Apps extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Apps(this.attachedDatabase, [this._alias]); - late final GeneratedColumn packageName = GeneratedColumn('package_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn name = - GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn className = - GeneratedColumn('class_name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn version = - GeneratedColumn('version', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [packageName, name, className, version,]; - @override - String get aliasedName => _alias ?? 'apps'; - @override - String get actualTableName => 'apps'; - @override - Set get $primaryKey => {packageName}; - @override - AppsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AppsData( - packageName: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}package_name'])!, - name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, - className: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}class_name'])!, - version: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}version'])!, - ); - } - - @override - Apps createAlias(String alias) { - return Apps(attachedDatabase, alias); - } -} - -class AppsData extends DataClass implements Insertable { - final String packageName; - final String name; - final String className; - final String version; - const AppsData( - {required this.packageName, - required this.name, - required this.className, - required this.version}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['package_name'] = Variable(packageName); - map['name'] = Variable(name); - map['class_name'] = Variable(className); - map['version'] = Variable(version); - return map; - } - - AppsCompanion toCompanion(bool nullToAbsent) { - return AppsCompanion( - packageName: Value(packageName), - name: Value(name), - className: Value(className), - version: Value(version), - ); - } - - factory AppsData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AppsData( - packageName: serializer.fromJson(json['packageName']), - name: serializer.fromJson(json['name']), - className: serializer.fromJson(json['className']), - version: serializer.fromJson(json['version']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'packageName': serializer.toJson(packageName), - 'name': serializer.toJson(name), - 'className': serializer.toJson(className), - 'version': serializer.toJson(version), - }; - } - - AppsData copyWith( - {String? packageName, - String? name, - String? className, - String? version}) => - AppsData( - packageName: packageName ?? this.packageName, - name: name ?? this.name, - className: className ?? this.className, - version: version ?? this.version, - ); - @override - String toString() { - return (StringBuffer('AppsData(') - ..write('packageName: $packageName, ') - ..write('name: $name, ') - ..write('className: $className, ') - ..write('version: $version, ') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - packageName, name, className, version); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppsData && - other.packageName == packageName && - other.name == name && - other.className == className && - other.version == version); -} - -class AppsCompanion extends UpdateCompanion { - final Value packageName; - final Value name; - final Value className; - final Value version; - const AppsCompanion({ - this.packageName = const Value.absent(), - this.name = const Value.absent(), - this.className = const Value.absent(), - this.version = const Value.absent(), - }); - AppsCompanion.insert({ - required String packageName, - required String name, - required String className, - required String version, - }) : packageName = Value(packageName), - name = Value(name), - className = Value(className), - version = Value(version); - static Insertable custom({ - Expression? packageName, - Expression? name, - Expression? className, - Expression? version, - }) { - return RawValuesInsertable({ - if (packageName != null) 'package_name': packageName, - if (name != null) 'name': name, - if (className != null) 'class_name': className, - if (version != null) 'version': version, - }); - } - - AppsCompanion copyWith( - {Value? packageName, - Value? name, - Value? className, - Value? version}) { - return AppsCompanion( - packageName: packageName ?? this.packageName, - name: name ?? this.name, - className: className ?? this.className, - version: version ?? this.version, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (packageName.present) { - map['package_name'] = Variable(packageName.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (className.present) { - map['class_name'] = Variable(className.value); - } - if (version.present) { - map['version'] = Variable(version.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AppsCompanion(') - ..write('packageName: $packageName, ') - ..write('name: $name, ') - ..write('className: $className, ') - ..write('version: $version, ') - ..write(')')) - .toString(); - } -} - -class Categories extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Categories(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn('id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = - GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn order = - GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, name, order]; - @override - String get aliasedName => _alias ?? 'categories'; - @override - String get actualTableName => 'categories'; - @override - Set get $primaryKey => {id}; - @override - CategoriesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CategoriesData( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, - order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, - ); - } - - @override - Categories createAlias(String alias) { - return Categories(attachedDatabase, alias); - } -} - -class CategoriesData extends DataClass implements Insertable { - final int id; - final String name; - final int order; - const CategoriesData({required this.id, required this.name, required this.order}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['order'] = Variable(order); - return map; - } - - CategoriesCompanion toCompanion(bool nullToAbsent) { - return CategoriesCompanion( - id: Value(id), - name: Value(name), - order: Value(order), - ); - } - - factory CategoriesData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return CategoriesData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - order: serializer.fromJson(json['order']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'order': serializer.toJson(order), - }; - } - - CategoriesData copyWith({int? id, String? name, int? order}) => CategoriesData( - id: id ?? this.id, - name: name ?? this.name, - order: order ?? this.order, - ); - @override - String toString() { - return (StringBuffer('CategoriesData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('order: $order') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, order); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is CategoriesData && other.id == id && other.name == name && other.order == order); -} - -class CategoriesCompanion extends UpdateCompanion { - final Value id; - final Value name; - final Value order; - const CategoriesCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.order = const Value.absent(), - }); - CategoriesCompanion.insert({ - this.id = const Value.absent(), - required String name, - required int order, - }) : name = Value(name), - order = Value(order); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? order, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (order != null) 'order': order, - }); - } - - CategoriesCompanion copyWith({Value? id, Value? name, Value? order}) { - return CategoriesCompanion( - id: id ?? this.id, - name: name ?? this.name, - order: order ?? this.order, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (order.present) { - map['order'] = Variable(order.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('CategoriesCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('order: $order') - ..write(')')) - .toString(); - } -} - -class AppsCategories extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - AppsCategories(this.attachedDatabase, [this._alias]); - late final GeneratedColumn categoryId = GeneratedColumn('category_id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'REFERENCES categories(id) ON DELETE CASCADE'); - late final GeneratedColumn appPackageName = GeneratedColumn('app_package_name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'REFERENCES apps(package_name) ON DELETE CASCADE'); - late final GeneratedColumn order = - GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [categoryId, appPackageName, order]; - @override - String get aliasedName => _alias ?? 'apps_categories'; - @override - String get actualTableName => 'apps_categories'; - @override - Set get $primaryKey => {categoryId, appPackageName}; - @override - AppsCategoriesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AppsCategoriesData( - categoryId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}category_id'])!, - appPackageName: - attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}app_package_name'])!, - order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, - ); - } - - @override - AppsCategories createAlias(String alias) { - return AppsCategories(attachedDatabase, alias); - } -} - -class AppsCategoriesData extends DataClass implements Insertable { - final int categoryId; - final String appPackageName; - final int order; - const AppsCategoriesData({required this.categoryId, required this.appPackageName, required this.order}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['category_id'] = Variable(categoryId); - map['app_package_name'] = Variable(appPackageName); - map['order'] = Variable(order); - return map; - } - - AppsCategoriesCompanion toCompanion(bool nullToAbsent) { - return AppsCategoriesCompanion( - categoryId: Value(categoryId), - appPackageName: Value(appPackageName), - order: Value(order), - ); - } - - factory AppsCategoriesData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AppsCategoriesData( - categoryId: serializer.fromJson(json['categoryId']), - appPackageName: serializer.fromJson(json['appPackageName']), - order: serializer.fromJson(json['order']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'categoryId': serializer.toJson(categoryId), - 'appPackageName': serializer.toJson(appPackageName), - 'order': serializer.toJson(order), - }; - } - - AppsCategoriesData copyWith({int? categoryId, String? appPackageName, int? order}) => AppsCategoriesData( - categoryId: categoryId ?? this.categoryId, - appPackageName: appPackageName ?? this.appPackageName, - order: order ?? this.order, - ); - @override - String toString() { - return (StringBuffer('AppsCategoriesData(') - ..write('categoryId: $categoryId, ') - ..write('appPackageName: $appPackageName, ') - ..write('order: $order') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(categoryId, appPackageName, order); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppsCategoriesData && - other.categoryId == categoryId && - other.appPackageName == appPackageName && - other.order == order); -} - -class AppsCategoriesCompanion extends UpdateCompanion { - final Value categoryId; - final Value appPackageName; - final Value order; - const AppsCategoriesCompanion({ - this.categoryId = const Value.absent(), - this.appPackageName = const Value.absent(), - this.order = const Value.absent(), - }); - AppsCategoriesCompanion.insert({ - required int categoryId, - required String appPackageName, - required int order, - }) : categoryId = Value(categoryId), - appPackageName = Value(appPackageName), - order = Value(order); - static Insertable custom({ - Expression? categoryId, - Expression? appPackageName, - Expression? order, - }) { - return RawValuesInsertable({ - if (categoryId != null) 'category_id': categoryId, - if (appPackageName != null) 'app_package_name': appPackageName, - if (order != null) 'order': order, - }); - } - - AppsCategoriesCompanion copyWith({Value? categoryId, Value? appPackageName, Value? order}) { - return AppsCategoriesCompanion( - categoryId: categoryId ?? this.categoryId, - appPackageName: appPackageName ?? this.appPackageName, - order: order ?? this.order, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (categoryId.present) { - map['category_id'] = Variable(categoryId.value); - } - if (appPackageName.present) { - map['app_package_name'] = Variable(appPackageName.value); - } - if (order.present) { - map['order'] = Variable(order.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AppsCategoriesCompanion(') - ..write('categoryId: $categoryId, ') - ..write('appPackageName: $appPackageName, ') - ..write('order: $order') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV1 extends GeneratedDatabase { - DatabaseAtV1(QueryExecutor e) : super(e); - late final Apps apps = Apps(this); - late final Categories categories = Categories(this); - late final AppsCategories appsCategories = AppsCategories(this); - @override - Iterable> get allTables => allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [apps, categories, appsCategories]; - @override - int get schemaVersion => 1; -} diff --git a/test/generated_migrations/schema_v2.dart b/test/generated_migrations/schema_v2.dart deleted file mode 100644 index 8fdfc401..00000000 --- a/test/generated_migrations/schema_v2.dart +++ /dev/null @@ -1,517 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Apps extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Apps(this.attachedDatabase, [this._alias]); - late final GeneratedColumn packageName = GeneratedColumn('package_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn name = - GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn version = - GeneratedColumn('version', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [packageName, name, version]; - @override - String get aliasedName => _alias ?? 'apps'; - @override - String get actualTableName => 'apps'; - @override - Set get $primaryKey => {packageName}; - @override - AppsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AppsData( - packageName: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}package_name'])!, - name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, - version: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}version'])!, - ); - } - - @override - Apps createAlias(String alias) { - return Apps(attachedDatabase, alias); - } -} - -class AppsData extends DataClass implements Insertable { - final String packageName; - final String name; - final String version; - const AppsData({required this.packageName, required this.name, required this.version}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['package_name'] = Variable(packageName); - map['name'] = Variable(name); - map['version'] = Variable(version); - return map; - } - - AppsCompanion toCompanion(bool nullToAbsent) { - return AppsCompanion( - packageName: Value(packageName), - name: Value(name), - version: Value(version), - ); - } - - factory AppsData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AppsData( - packageName: serializer.fromJson(json['packageName']), - name: serializer.fromJson(json['name']), - version: serializer.fromJson(json['version']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'packageName': serializer.toJson(packageName), - 'name': serializer.toJson(name), - 'version': serializer.toJson(version), - }; - } - - AppsData copyWith( - {String? packageName, - String? name, - String? version}) => - AppsData( - packageName: packageName ?? this.packageName, - name: name ?? this.name, - version: version ?? this.version, - ); - @override - String toString() { - return (StringBuffer('AppsData(') - ..write('packageName: $packageName, ') - ..write('name: $name, ') - ..write('version: $version, ') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(packageName, name, version); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppsData && - other.packageName == this.packageName && - other.name == this.name && - other.version == this.version); -} - -class AppsCompanion extends UpdateCompanion { - final Value packageName; - final Value name; - final Value version; - const AppsCompanion({ - this.packageName = const Value.absent(), - this.name = const Value.absent(), - this.version = const Value.absent(), - }); - AppsCompanion.insert({ - required String packageName, - required String name, - required String version, - }) : packageName = Value(packageName), - name = Value(name), - version = Value(version); - static Insertable custom({ - Expression? packageName, - Expression? name, - Expression? version, - }) { - return RawValuesInsertable({ - if (packageName != null) 'package_name': packageName, - if (name != null) 'name': name, - if (version != null) 'version': version, - }); - } - - AppsCompanion copyWith( - {Value? packageName, - Value? name, - Value? version}) { - return AppsCompanion( - packageName: packageName ?? this.packageName, - name: name ?? this.name, - version: version ?? this.version, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (packageName.present) { - map['package_name'] = Variable(packageName.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (version.present) { - map['version'] = Variable(version.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AppsCompanion(') - ..write('packageName: $packageName, ') - ..write('name: $name, ') - ..write('version: $version, ') - ..write(')')) - .toString(); - } -} - -class Categories extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Categories(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn('id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = - GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn order = - GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, name, order]; - @override - String get aliasedName => _alias ?? 'categories'; - @override - String get actualTableName => 'categories'; - @override - Set get $primaryKey => {id}; - @override - CategoriesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CategoriesData( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, - order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, - ); - } - - @override - Categories createAlias(String alias) { - return Categories(attachedDatabase, alias); - } -} - -class CategoriesData extends DataClass implements Insertable { - final int id; - final String name; - final int order; - const CategoriesData({required this.id, required this.name, required this.order}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['order'] = Variable(order); - return map; - } - - CategoriesCompanion toCompanion(bool nullToAbsent) { - return CategoriesCompanion( - id: Value(id), - name: Value(name), - order: Value(order), - ); - } - - factory CategoriesData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return CategoriesData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - order: serializer.fromJson(json['order']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'order': serializer.toJson(order), - }; - } - - CategoriesData copyWith({int? id, String? name, int? order}) => CategoriesData( - id: id ?? this.id, - name: name ?? this.name, - order: order ?? this.order, - ); - @override - String toString() { - return (StringBuffer('CategoriesData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('order: $order') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, order); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is CategoriesData && other.id == this.id && other.name == this.name && other.order == this.order); -} - -class CategoriesCompanion extends UpdateCompanion { - final Value id; - final Value name; - final Value order; - const CategoriesCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.order = const Value.absent(), - }); - CategoriesCompanion.insert({ - this.id = const Value.absent(), - required String name, - required int order, - }) : name = Value(name), - order = Value(order); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? order, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (order != null) 'order': order, - }); - } - - CategoriesCompanion copyWith({Value? id, Value? name, Value? order}) { - return CategoriesCompanion( - id: id ?? this.id, - name: name ?? this.name, - order: order ?? this.order, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (order.present) { - map['order'] = Variable(order.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('CategoriesCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('order: $order') - ..write(')')) - .toString(); - } -} - -class AppsCategories extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - AppsCategories(this.attachedDatabase, [this._alias]); - late final GeneratedColumn categoryId = GeneratedColumn('category_id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'REFERENCES categories(id) ON DELETE CASCADE'); - late final GeneratedColumn appPackageName = GeneratedColumn('app_package_name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'REFERENCES apps(package_name) ON DELETE CASCADE'); - late final GeneratedColumn order = - GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [categoryId, appPackageName, order]; - @override - String get aliasedName => _alias ?? 'apps_categories'; - @override - String get actualTableName => 'apps_categories'; - @override - Set get $primaryKey => {categoryId, appPackageName}; - @override - AppsCategoriesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AppsCategoriesData( - categoryId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}category_id'])!, - appPackageName: - attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}app_package_name'])!, - order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, - ); - } - - @override - AppsCategories createAlias(String alias) { - return AppsCategories(attachedDatabase, alias); - } -} - -class AppsCategoriesData extends DataClass implements Insertable { - final int categoryId; - final String appPackageName; - final int order; - const AppsCategoriesData({required this.categoryId, required this.appPackageName, required this.order}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['category_id'] = Variable(categoryId); - map['app_package_name'] = Variable(appPackageName); - map['order'] = Variable(order); - return map; - } - - AppsCategoriesCompanion toCompanion(bool nullToAbsent) { - return AppsCategoriesCompanion( - categoryId: Value(categoryId), - appPackageName: Value(appPackageName), - order: Value(order), - ); - } - - factory AppsCategoriesData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AppsCategoriesData( - categoryId: serializer.fromJson(json['categoryId']), - appPackageName: serializer.fromJson(json['appPackageName']), - order: serializer.fromJson(json['order']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'categoryId': serializer.toJson(categoryId), - 'appPackageName': serializer.toJson(appPackageName), - 'order': serializer.toJson(order), - }; - } - - AppsCategoriesData copyWith({int? categoryId, String? appPackageName, int? order}) => AppsCategoriesData( - categoryId: categoryId ?? this.categoryId, - appPackageName: appPackageName ?? this.appPackageName, - order: order ?? this.order, - ); - @override - String toString() { - return (StringBuffer('AppsCategoriesData(') - ..write('categoryId: $categoryId, ') - ..write('appPackageName: $appPackageName, ') - ..write('order: $order') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(categoryId, appPackageName, order); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppsCategoriesData && - other.categoryId == this.categoryId && - other.appPackageName == this.appPackageName && - other.order == this.order); -} - -class AppsCategoriesCompanion extends UpdateCompanion { - final Value categoryId; - final Value appPackageName; - final Value order; - const AppsCategoriesCompanion({ - this.categoryId = const Value.absent(), - this.appPackageName = const Value.absent(), - this.order = const Value.absent(), - }); - AppsCategoriesCompanion.insert({ - required int categoryId, - required String appPackageName, - required int order, - }) : categoryId = Value(categoryId), - appPackageName = Value(appPackageName), - order = Value(order); - static Insertable custom({ - Expression? categoryId, - Expression? appPackageName, - Expression? order, - }) { - return RawValuesInsertable({ - if (categoryId != null) 'category_id': categoryId, - if (appPackageName != null) 'app_package_name': appPackageName, - if (order != null) 'order': order, - }); - } - - AppsCategoriesCompanion copyWith({Value? categoryId, Value? appPackageName, Value? order}) { - return AppsCategoriesCompanion( - categoryId: categoryId ?? this.categoryId, - appPackageName: appPackageName ?? this.appPackageName, - order: order ?? this.order, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (categoryId.present) { - map['category_id'] = Variable(categoryId.value); - } - if (appPackageName.present) { - map['app_package_name'] = Variable(appPackageName.value); - } - if (order.present) { - map['order'] = Variable(order.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AppsCategoriesCompanion(') - ..write('categoryId: $categoryId, ') - ..write('appPackageName: $appPackageName, ') - ..write('order: $order') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV2 extends GeneratedDatabase { - DatabaseAtV2(QueryExecutor e) : super(e); - late final Apps apps = Apps(this); - late final Categories categories = Categories(this); - late final AppsCategories appsCategories = AppsCategories(this); - @override - Iterable> get allTables => allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [apps, categories, appsCategories]; - @override - int get schemaVersion => 2; -} diff --git a/test/generated_migrations/schema_v3.dart b/test/generated_migrations/schema_v3.dart deleted file mode 100644 index 078d547e..00000000 --- a/test/generated_migrations/schema_v3.dart +++ /dev/null @@ -1,551 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Apps extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Apps(this.attachedDatabase, [this._alias]); - late final GeneratedColumn packageName = GeneratedColumn('package_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn name = - GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn version = - GeneratedColumn('version', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn hidden = GeneratedColumn('hidden', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({ - SqlDialect.sqlite: 'CHECK ("hidden" IN (0, 1))', - SqlDialect.mysql: '', - SqlDialect.postgres: '', - }), - defaultValue: Constant(false)); - @override - List get $columns => [packageName, name, version, hidden]; - @override - String get aliasedName => _alias ?? 'apps'; - @override - String get actualTableName => 'apps'; - @override - Set get $primaryKey => {packageName}; - @override - AppsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AppsData( - packageName: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}package_name'])!, - name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, - version: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}version'])!, - hidden: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}hidden'])!, - ); - } - - @override - Apps createAlias(String alias) { - return Apps(attachedDatabase, alias); - } -} - -class AppsData extends DataClass implements Insertable { - final String packageName; - final String name; - final String version; - final bool hidden; - const AppsData( - {required this.packageName, - required this.name, - required this.version, - required this.hidden}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['package_name'] = Variable(packageName); - map['name'] = Variable(name); - map['version'] = Variable(version); - map['hidden'] = Variable(hidden); - return map; - } - - AppsCompanion toCompanion(bool nullToAbsent) { - return AppsCompanion( - packageName: Value(packageName), - name: Value(name), - version: Value(version), - hidden: Value(hidden), - ); - } - - factory AppsData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AppsData( - packageName: serializer.fromJson(json['packageName']), - name: serializer.fromJson(json['name']), - version: serializer.fromJson(json['version']), - hidden: serializer.fromJson(json['hidden']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'packageName': serializer.toJson(packageName), - 'name': serializer.toJson(name), - 'version': serializer.toJson(version), - 'hidden': serializer.toJson(hidden), - }; - } - - AppsData copyWith( - {String? packageName, - String? name, - String? version, - bool? hidden}) => - AppsData( - packageName: packageName ?? this.packageName, - name: name ?? this.name, - version: version ?? this.version, - hidden: hidden ?? this.hidden, - ); - @override - String toString() { - return (StringBuffer('AppsData(') - ..write('packageName: $packageName, ') - ..write('name: $name, ') - ..write('version: $version, ') - ..write('hidden: $hidden') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(packageName, name, version, hidden); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppsData && - other.packageName == this.packageName && - other.name == this.name && - other.version == this.version && - other.hidden == this.hidden); -} - -class AppsCompanion extends UpdateCompanion { - final Value packageName; - final Value name; - final Value version; - final Value hidden; - const AppsCompanion({ - this.packageName = const Value.absent(), - this.name = const Value.absent(), - this.version = const Value.absent(), - this.hidden = const Value.absent(), - }); - AppsCompanion.insert({ - required String packageName, - required String name, - required String version, - this.hidden = const Value.absent(), - }) : packageName = Value(packageName), - name = Value(name), - version = Value(version); - static Insertable custom({ - Expression? packageName, - Expression? name, - Expression? version, - Expression? hidden, - }) { - return RawValuesInsertable({ - if (packageName != null) 'package_name': packageName, - if (name != null) 'name': name, - if (version != null) 'version': version, - if (hidden != null) 'hidden': hidden, - }); - } - - AppsCompanion copyWith( - {Value? packageName, - Value? name, - Value? version, - Value? hidden}) { - return AppsCompanion( - packageName: packageName ?? this.packageName, - name: name ?? this.name, - version: version ?? this.version, - hidden: hidden ?? this.hidden, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (packageName.present) { - map['package_name'] = Variable(packageName.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (version.present) { - map['version'] = Variable(version.value); - } - if (hidden.present) { - map['hidden'] = Variable(hidden.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AppsCompanion(') - ..write('packageName: $packageName, ') - ..write('name: $name, ') - ..write('version: $version, ') - ..write('hidden: $hidden') - ..write(')')) - .toString(); - } -} - -class Categories extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Categories(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn('id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = - GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn order = - GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, name, order]; - @override - String get aliasedName => _alias ?? 'categories'; - @override - String get actualTableName => 'categories'; - @override - Set get $primaryKey => {id}; - @override - CategoriesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CategoriesData( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, - order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, - ); - } - - @override - Categories createAlias(String alias) { - return Categories(attachedDatabase, alias); - } -} - -class CategoriesData extends DataClass implements Insertable { - final int id; - final String name; - final int order; - const CategoriesData({required this.id, required this.name, required this.order}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['order'] = Variable(order); - return map; - } - - CategoriesCompanion toCompanion(bool nullToAbsent) { - return CategoriesCompanion( - id: Value(id), - name: Value(name), - order: Value(order), - ); - } - - factory CategoriesData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return CategoriesData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - order: serializer.fromJson(json['order']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'order': serializer.toJson(order), - }; - } - - CategoriesData copyWith({int? id, String? name, int? order}) => CategoriesData( - id: id ?? this.id, - name: name ?? this.name, - order: order ?? this.order, - ); - @override - String toString() { - return (StringBuffer('CategoriesData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('order: $order') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, order); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is CategoriesData && other.id == this.id && other.name == this.name && other.order == this.order); -} - -class CategoriesCompanion extends UpdateCompanion { - final Value id; - final Value name; - final Value order; - const CategoriesCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.order = const Value.absent(), - }); - CategoriesCompanion.insert({ - this.id = const Value.absent(), - required String name, - required int order, - }) : name = Value(name), - order = Value(order); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? order, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (order != null) 'order': order, - }); - } - - CategoriesCompanion copyWith({Value? id, Value? name, Value? order}) { - return CategoriesCompanion( - id: id ?? this.id, - name: name ?? this.name, - order: order ?? this.order, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (order.present) { - map['order'] = Variable(order.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('CategoriesCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('order: $order') - ..write(')')) - .toString(); - } -} - -class AppsCategories extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - AppsCategories(this.attachedDatabase, [this._alias]); - late final GeneratedColumn categoryId = GeneratedColumn('category_id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'REFERENCES categories(id) ON DELETE CASCADE'); - late final GeneratedColumn appPackageName = GeneratedColumn('app_package_name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'REFERENCES apps(package_name) ON DELETE CASCADE'); - late final GeneratedColumn order = - GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [categoryId, appPackageName, order]; - @override - String get aliasedName => _alias ?? 'apps_categories'; - @override - String get actualTableName => 'apps_categories'; - @override - Set get $primaryKey => {categoryId, appPackageName}; - @override - AppsCategoriesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AppsCategoriesData( - categoryId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}category_id'])!, - appPackageName: - attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}app_package_name'])!, - order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, - ); - } - - @override - AppsCategories createAlias(String alias) { - return AppsCategories(attachedDatabase, alias); - } -} - -class AppsCategoriesData extends DataClass implements Insertable { - final int categoryId; - final String appPackageName; - final int order; - const AppsCategoriesData({required this.categoryId, required this.appPackageName, required this.order}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['category_id'] = Variable(categoryId); - map['app_package_name'] = Variable(appPackageName); - map['order'] = Variable(order); - return map; - } - - AppsCategoriesCompanion toCompanion(bool nullToAbsent) { - return AppsCategoriesCompanion( - categoryId: Value(categoryId), - appPackageName: Value(appPackageName), - order: Value(order), - ); - } - - factory AppsCategoriesData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AppsCategoriesData( - categoryId: serializer.fromJson(json['categoryId']), - appPackageName: serializer.fromJson(json['appPackageName']), - order: serializer.fromJson(json['order']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'categoryId': serializer.toJson(categoryId), - 'appPackageName': serializer.toJson(appPackageName), - 'order': serializer.toJson(order), - }; - } - - AppsCategoriesData copyWith({int? categoryId, String? appPackageName, int? order}) => AppsCategoriesData( - categoryId: categoryId ?? this.categoryId, - appPackageName: appPackageName ?? this.appPackageName, - order: order ?? this.order, - ); - @override - String toString() { - return (StringBuffer('AppsCategoriesData(') - ..write('categoryId: $categoryId, ') - ..write('appPackageName: $appPackageName, ') - ..write('order: $order') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(categoryId, appPackageName, order); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppsCategoriesData && - other.categoryId == this.categoryId && - other.appPackageName == this.appPackageName && - other.order == this.order); -} - -class AppsCategoriesCompanion extends UpdateCompanion { - final Value categoryId; - final Value appPackageName; - final Value order; - const AppsCategoriesCompanion({ - this.categoryId = const Value.absent(), - this.appPackageName = const Value.absent(), - this.order = const Value.absent(), - }); - AppsCategoriesCompanion.insert({ - required int categoryId, - required String appPackageName, - required int order, - }) : categoryId = Value(categoryId), - appPackageName = Value(appPackageName), - order = Value(order); - static Insertable custom({ - Expression? categoryId, - Expression? appPackageName, - Expression? order, - }) { - return RawValuesInsertable({ - if (categoryId != null) 'category_id': categoryId, - if (appPackageName != null) 'app_package_name': appPackageName, - if (order != null) 'order': order, - }); - } - - AppsCategoriesCompanion copyWith({Value? categoryId, Value? appPackageName, Value? order}) { - return AppsCategoriesCompanion( - categoryId: categoryId ?? this.categoryId, - appPackageName: appPackageName ?? this.appPackageName, - order: order ?? this.order, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (categoryId.present) { - map['category_id'] = Variable(categoryId.value); - } - if (appPackageName.present) { - map['app_package_name'] = Variable(appPackageName.value); - } - if (order.present) { - map['order'] = Variable(order.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AppsCategoriesCompanion(') - ..write('categoryId: $categoryId, ') - ..write('appPackageName: $appPackageName, ') - ..write('order: $order') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV3 extends GeneratedDatabase { - DatabaseAtV3(QueryExecutor e) : super(e); - late final Apps apps = Apps(this); - late final Categories categories = Categories(this); - late final AppsCategories appsCategories = AppsCategories(this); - @override - Iterable> get allTables => allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [apps, categories, appsCategories]; - @override - int get schemaVersion => 3; -} diff --git a/test/generated_migrations/schema_v4.dart b/test/generated_migrations/schema_v4.dart deleted file mode 100644 index a8cb12bf..00000000 --- a/test/generated_migrations/schema_v4.dart +++ /dev/null @@ -1,654 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Apps extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Apps(this.attachedDatabase, [this._alias]); - late final GeneratedColumn packageName = GeneratedColumn('package_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn name = - GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn version = - GeneratedColumn('version', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn hidden = GeneratedColumn('hidden', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({ - SqlDialect.sqlite: 'CHECK ("hidden" IN (0, 1))', - SqlDialect.mysql: '', - SqlDialect.postgres: '', - }), - defaultValue: Constant(false)); - @override - List get $columns => [packageName, name, version, hidden]; - @override - String get aliasedName => _alias ?? 'apps'; - @override - String get actualTableName => 'apps'; - @override - Set get $primaryKey => {packageName}; - @override - AppsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AppsData( - packageName: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}package_name'])!, - name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, - version: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}version'])!, - hidden: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}hidden'])!, - ); - } - - @override - Apps createAlias(String alias) { - return Apps(attachedDatabase, alias); - } -} - -class AppsData extends DataClass implements Insertable { - final String packageName; - final String name; - final String version; - final bool hidden; - const AppsData( - {required this.packageName, - required this.name, - required this.version, - required this.hidden}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['package_name'] = Variable(packageName); - map['name'] = Variable(name); - map['version'] = Variable(version); - map['hidden'] = Variable(hidden); - return map; - } - - AppsCompanion toCompanion(bool nullToAbsent) { - return AppsCompanion( - packageName: Value(packageName), - name: Value(name), - version: Value(version), - hidden: Value(hidden), - ); - } - - factory AppsData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AppsData( - packageName: serializer.fromJson(json['packageName']), - name: serializer.fromJson(json['name']), - version: serializer.fromJson(json['version']), - hidden: serializer.fromJson(json['hidden']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'packageName': serializer.toJson(packageName), - 'name': serializer.toJson(name), - 'version': serializer.toJson(version), - 'hidden': serializer.toJson(hidden), - }; - } - - AppsData copyWith( - {String? packageName, - String? name, - String? version, - bool? hidden}) => - AppsData( - packageName: packageName ?? this.packageName, - name: name ?? this.name, - version: version ?? this.version, - hidden: hidden ?? this.hidden, - ); - @override - String toString() { - return (StringBuffer('AppsData(') - ..write('packageName: $packageName, ') - ..write('name: $name, ') - ..write('version: $version, ') - ..write('hidden: $hidden') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(packageName, name, version, hidden); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppsData && - other.packageName == this.packageName && - other.name == this.name && - other.version == this.version && - other.hidden == this.hidden); -} - -class AppsCompanion extends UpdateCompanion { - final Value packageName; - final Value name; - final Value version; - final Value hidden; - const AppsCompanion({ - this.packageName = const Value.absent(), - this.name = const Value.absent(), - this.version = const Value.absent(), - this.hidden = const Value.absent(), - }); - AppsCompanion.insert({ - required String packageName, - required String name, - required String version, - this.hidden = const Value.absent(), - }) : packageName = Value(packageName), - name = Value(name), - version = Value(version); - static Insertable custom({ - Expression? packageName, - Expression? name, - Expression? version, - Expression? hidden, - }) { - return RawValuesInsertable({ - if (packageName != null) 'package_name': packageName, - if (name != null) 'name': name, - if (version != null) 'version': version, - if (hidden != null) 'hidden': hidden, - }); - } - - AppsCompanion copyWith( - {Value? packageName, - Value? name, - Value? version, - Value? hidden}) { - return AppsCompanion( - packageName: packageName ?? this.packageName, - name: name ?? this.name, - version: version ?? this.version, - hidden: hidden ?? this.hidden, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (packageName.present) { - map['package_name'] = Variable(packageName.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (version.present) { - map['version'] = Variable(version.value); - } - if (hidden.present) { - map['hidden'] = Variable(hidden.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AppsCompanion(') - ..write('packageName: $packageName, ') - ..write('name: $name, ') - ..write('version: $version, ') - ..write('hidden: $hidden') - ..write(')')) - .toString(); - } -} - -class Categories extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Categories(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn('id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = - GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn sort = GeneratedColumn('sort', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: Constant(0)); - late final GeneratedColumn type = GeneratedColumn('type', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: Constant(0)); - late final GeneratedColumn rowHeight = GeneratedColumn('row_height', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: Constant(110)); - late final GeneratedColumn columnsCount = GeneratedColumn('columns_count', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: Constant(6)); - late final GeneratedColumn order = - GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, name, sort, type, rowHeight, columnsCount, order]; - @override - String get aliasedName => _alias ?? 'categories'; - @override - String get actualTableName => 'categories'; - @override - Set get $primaryKey => {id}; - @override - CategoriesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CategoriesData( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, - sort: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}sort'])!, - type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, - rowHeight: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}row_height'])!, - columnsCount: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}columns_count'])!, - order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, - ); - } - - @override - Categories createAlias(String alias) { - return Categories(attachedDatabase, alias); - } -} - -class CategoriesData extends DataClass implements Insertable { - final int id; - final String name; - final int sort; - final int type; - final int rowHeight; - final int columnsCount; - final int order; - const CategoriesData( - {required this.id, - required this.name, - required this.sort, - required this.type, - required this.rowHeight, - required this.columnsCount, - required this.order}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['sort'] = Variable(sort); - map['type'] = Variable(type); - map['row_height'] = Variable(rowHeight); - map['columns_count'] = Variable(columnsCount); - map['order'] = Variable(order); - return map; - } - - CategoriesCompanion toCompanion(bool nullToAbsent) { - return CategoriesCompanion( - id: Value(id), - name: Value(name), - sort: Value(sort), - type: Value(type), - rowHeight: Value(rowHeight), - columnsCount: Value(columnsCount), - order: Value(order), - ); - } - - factory CategoriesData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return CategoriesData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - sort: serializer.fromJson(json['sort']), - type: serializer.fromJson(json['type']), - rowHeight: serializer.fromJson(json['rowHeight']), - columnsCount: serializer.fromJson(json['columnsCount']), - order: serializer.fromJson(json['order']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'sort': serializer.toJson(sort), - 'type': serializer.toJson(type), - 'rowHeight': serializer.toJson(rowHeight), - 'columnsCount': serializer.toJson(columnsCount), - 'order': serializer.toJson(order), - }; - } - - CategoriesData copyWith( - {int? id, String? name, int? sort, int? type, int? rowHeight, int? columnsCount, int? order}) => - CategoriesData( - id: id ?? this.id, - name: name ?? this.name, - sort: sort ?? this.sort, - type: type ?? this.type, - rowHeight: rowHeight ?? this.rowHeight, - columnsCount: columnsCount ?? this.columnsCount, - order: order ?? this.order, - ); - @override - String toString() { - return (StringBuffer('CategoriesData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('sort: $sort, ') - ..write('type: $type, ') - ..write('rowHeight: $rowHeight, ') - ..write('columnsCount: $columnsCount, ') - ..write('order: $order') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, sort, type, rowHeight, columnsCount, order); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is CategoriesData && - other.id == this.id && - other.name == this.name && - other.sort == this.sort && - other.type == this.type && - other.rowHeight == this.rowHeight && - other.columnsCount == this.columnsCount && - other.order == this.order); -} - -class CategoriesCompanion extends UpdateCompanion { - final Value id; - final Value name; - final Value sort; - final Value type; - final Value rowHeight; - final Value columnsCount; - final Value order; - const CategoriesCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.sort = const Value.absent(), - this.type = const Value.absent(), - this.rowHeight = const Value.absent(), - this.columnsCount = const Value.absent(), - this.order = const Value.absent(), - }); - CategoriesCompanion.insert({ - this.id = const Value.absent(), - required String name, - this.sort = const Value.absent(), - this.type = const Value.absent(), - this.rowHeight = const Value.absent(), - this.columnsCount = const Value.absent(), - required int order, - }) : name = Value(name), - order = Value(order); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? sort, - Expression? type, - Expression? rowHeight, - Expression? columnsCount, - Expression? order, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (sort != null) 'sort': sort, - if (type != null) 'type': type, - if (rowHeight != null) 'row_height': rowHeight, - if (columnsCount != null) 'columns_count': columnsCount, - if (order != null) 'order': order, - }); - } - - CategoriesCompanion copyWith( - {Value? id, - Value? name, - Value? sort, - Value? type, - Value? rowHeight, - Value? columnsCount, - Value? order}) { - return CategoriesCompanion( - id: id ?? this.id, - name: name ?? this.name, - sort: sort ?? this.sort, - type: type ?? this.type, - rowHeight: rowHeight ?? this.rowHeight, - columnsCount: columnsCount ?? this.columnsCount, - order: order ?? this.order, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (sort.present) { - map['sort'] = Variable(sort.value); - } - if (type.present) { - map['type'] = Variable(type.value); - } - if (rowHeight.present) { - map['row_height'] = Variable(rowHeight.value); - } - if (columnsCount.present) { - map['columns_count'] = Variable(columnsCount.value); - } - if (order.present) { - map['order'] = Variable(order.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('CategoriesCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('sort: $sort, ') - ..write('type: $type, ') - ..write('rowHeight: $rowHeight, ') - ..write('columnsCount: $columnsCount, ') - ..write('order: $order') - ..write(')')) - .toString(); - } -} - -class AppsCategories extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - AppsCategories(this.attachedDatabase, [this._alias]); - late final GeneratedColumn categoryId = GeneratedColumn('category_id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'REFERENCES categories(id) ON DELETE CASCADE'); - late final GeneratedColumn appPackageName = GeneratedColumn('app_package_name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'REFERENCES apps(package_name) ON DELETE CASCADE'); - late final GeneratedColumn order = - GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [categoryId, appPackageName, order]; - @override - String get aliasedName => _alias ?? 'apps_categories'; - @override - String get actualTableName => 'apps_categories'; - @override - Set get $primaryKey => {categoryId, appPackageName}; - @override - AppsCategoriesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AppsCategoriesData( - categoryId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}category_id'])!, - appPackageName: - attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}app_package_name'])!, - order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, - ); - } - - @override - AppsCategories createAlias(String alias) { - return AppsCategories(attachedDatabase, alias); - } -} - -class AppsCategoriesData extends DataClass implements Insertable { - final int categoryId; - final String appPackageName; - final int order; - const AppsCategoriesData({required this.categoryId, required this.appPackageName, required this.order}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['category_id'] = Variable(categoryId); - map['app_package_name'] = Variable(appPackageName); - map['order'] = Variable(order); - return map; - } - - AppsCategoriesCompanion toCompanion(bool nullToAbsent) { - return AppsCategoriesCompanion( - categoryId: Value(categoryId), - appPackageName: Value(appPackageName), - order: Value(order), - ); - } - - factory AppsCategoriesData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AppsCategoriesData( - categoryId: serializer.fromJson(json['categoryId']), - appPackageName: serializer.fromJson(json['appPackageName']), - order: serializer.fromJson(json['order']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'categoryId': serializer.toJson(categoryId), - 'appPackageName': serializer.toJson(appPackageName), - 'order': serializer.toJson(order), - }; - } - - AppsCategoriesData copyWith({int? categoryId, String? appPackageName, int? order}) => AppsCategoriesData( - categoryId: categoryId ?? this.categoryId, - appPackageName: appPackageName ?? this.appPackageName, - order: order ?? this.order, - ); - @override - String toString() { - return (StringBuffer('AppsCategoriesData(') - ..write('categoryId: $categoryId, ') - ..write('appPackageName: $appPackageName, ') - ..write('order: $order') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(categoryId, appPackageName, order); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppsCategoriesData && - other.categoryId == this.categoryId && - other.appPackageName == this.appPackageName && - other.order == this.order); -} - -class AppsCategoriesCompanion extends UpdateCompanion { - final Value categoryId; - final Value appPackageName; - final Value order; - const AppsCategoriesCompanion({ - this.categoryId = const Value.absent(), - this.appPackageName = const Value.absent(), - this.order = const Value.absent(), - }); - AppsCategoriesCompanion.insert({ - required int categoryId, - required String appPackageName, - required int order, - }) : categoryId = Value(categoryId), - appPackageName = Value(appPackageName), - order = Value(order); - static Insertable custom({ - Expression? categoryId, - Expression? appPackageName, - Expression? order, - }) { - return RawValuesInsertable({ - if (categoryId != null) 'category_id': categoryId, - if (appPackageName != null) 'app_package_name': appPackageName, - if (order != null) 'order': order, - }); - } - - AppsCategoriesCompanion copyWith({Value? categoryId, Value? appPackageName, Value? order}) { - return AppsCategoriesCompanion( - categoryId: categoryId ?? this.categoryId, - appPackageName: appPackageName ?? this.appPackageName, - order: order ?? this.order, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (categoryId.present) { - map['category_id'] = Variable(categoryId.value); - } - if (appPackageName.present) { - map['app_package_name'] = Variable(appPackageName.value); - } - if (order.present) { - map['order'] = Variable(order.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AppsCategoriesCompanion(') - ..write('categoryId: $categoryId, ') - ..write('appPackageName: $appPackageName, ') - ..write('order: $order') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV4 extends GeneratedDatabase { - DatabaseAtV4(QueryExecutor e) : super(e); - late final Apps apps = Apps(this); - late final Categories categories = Categories(this); - late final AppsCategories appsCategories = AppsCategories(this); - @override - Iterable> get allTables => allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [apps, categories, appsCategories]; - @override - int get schemaVersion => 4; -} diff --git a/test/generated_migrations/schema_v5.dart b/test/generated_migrations/schema_v5.dart deleted file mode 100644 index 2217639c..00000000 --- a/test/generated_migrations/schema_v5.dart +++ /dev/null @@ -1,685 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Apps extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Apps(this.attachedDatabase, [this._alias]); - late final GeneratedColumn packageName = GeneratedColumn('package_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn name = - GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn version = - GeneratedColumn('version', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn hidden = GeneratedColumn('hidden', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({ - SqlDialect.sqlite: 'CHECK ("hidden" IN (0, 1))', - SqlDialect.mysql: '', - SqlDialect.postgres: '', - }), - defaultValue: Constant(false)); - late final GeneratedColumn sideloaded = GeneratedColumn('sideloaded', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({ - SqlDialect.sqlite: 'CHECK ("sideloaded" IN (0, 1))', - SqlDialect.mysql: '', - SqlDialect.postgres: '', - }), - defaultValue: Constant(false)); - @override - List get $columns => [packageName, name, version, hidden, sideloaded]; - @override - String get aliasedName => _alias ?? 'apps'; - @override - String get actualTableName => 'apps'; - @override - Set get $primaryKey => {packageName}; - @override - AppsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AppsData( - packageName: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}package_name'])!, - name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, - version: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}version'])!, - hidden: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}hidden'])!, - sideloaded: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}sideloaded'])!, - ); - } - - @override - Apps createAlias(String alias) { - return Apps(attachedDatabase, alias); - } -} - -class AppsData extends DataClass implements Insertable { - final String packageName; - final String name; - final String version; - final bool hidden; - final bool sideloaded; - const AppsData( - {required this.packageName, - required this.name, - required this.version, - required this.hidden, - required this.sideloaded}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['package_name'] = Variable(packageName); - map['name'] = Variable(name); - map['version'] = Variable(version); - map['hidden'] = Variable(hidden); - map['sideloaded'] = Variable(sideloaded); - return map; - } - - AppsCompanion toCompanion(bool nullToAbsent) { - return AppsCompanion( - packageName: Value(packageName), - name: Value(name), - version: Value(version), - hidden: Value(hidden), - sideloaded: Value(sideloaded), - ); - } - - factory AppsData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AppsData( - packageName: serializer.fromJson(json['packageName']), - name: serializer.fromJson(json['name']), - version: serializer.fromJson(json['version']), - hidden: serializer.fromJson(json['hidden']), - sideloaded: serializer.fromJson(json['sideloaded']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'packageName': serializer.toJson(packageName), - 'name': serializer.toJson(name), - 'version': serializer.toJson(version), - 'hidden': serializer.toJson(hidden), - 'sideloaded': serializer.toJson(sideloaded), - }; - } - - AppsData copyWith( - {String? packageName, - String? name, - String? version, - bool? hidden, - bool? sideloaded}) => - AppsData( - packageName: packageName ?? this.packageName, - name: name ?? this.name, - version: version ?? this.version, - hidden: hidden ?? this.hidden, - sideloaded: sideloaded ?? this.sideloaded, - ); - @override - String toString() { - return (StringBuffer('AppsData(') - ..write('packageName: $packageName, ') - ..write('name: $name, ') - ..write('version: $version, ') - ..write('hidden: $hidden, ') - ..write('sideloaded: $sideloaded') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - packageName, name, version, hidden, sideloaded); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppsData && - other.packageName == this.packageName && - other.name == this.name && - other.version == this.version && - other.hidden == this.hidden && - other.sideloaded == this.sideloaded); -} - -class AppsCompanion extends UpdateCompanion { - final Value packageName; - final Value name; - final Value version; - final Value hidden; - final Value sideloaded; - const AppsCompanion({ - this.packageName = const Value.absent(), - this.name = const Value.absent(), - this.version = const Value.absent(), - this.hidden = const Value.absent(), - this.sideloaded = const Value.absent(), - }); - AppsCompanion.insert({ - required String packageName, - required String name, - required String version, - this.hidden = const Value.absent(), - this.sideloaded = const Value.absent(), - }) : packageName = Value(packageName), - name = Value(name), - version = Value(version); - static Insertable custom({ - Expression? packageName, - Expression? name, - Expression? version, - Expression? hidden, - Expression? sideloaded, - }) { - return RawValuesInsertable({ - if (packageName != null) 'package_name': packageName, - if (name != null) 'name': name, - if (version != null) 'version': version, - if (hidden != null) 'hidden': hidden, - if (sideloaded != null) 'sideloaded': sideloaded, - }); - } - - AppsCompanion copyWith( - {Value? packageName, - Value? name, - Value? version, - Value? hidden, - Value? sideloaded}) { - return AppsCompanion( - packageName: packageName ?? this.packageName, - name: name ?? this.name, - version: version ?? this.version, - hidden: hidden ?? this.hidden, - sideloaded: sideloaded ?? this.sideloaded, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (packageName.present) { - map['package_name'] = Variable(packageName.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (version.present) { - map['version'] = Variable(version.value); - } - if (hidden.present) { - map['hidden'] = Variable(hidden.value); - } - if (sideloaded.present) { - map['sideloaded'] = Variable(sideloaded.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AppsCompanion(') - ..write('packageName: $packageName, ') - ..write('name: $name, ') - ..write('version: $version, ') - ..write('hidden: $hidden, ') - ..write('sideloaded: $sideloaded') - ..write(')')) - .toString(); - } -} - -class Categories extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Categories(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn('id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = - GeneratedColumn('name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn sort = GeneratedColumn('sort', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: Constant(0)); - late final GeneratedColumn type = GeneratedColumn('type', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: Constant(0)); - late final GeneratedColumn rowHeight = GeneratedColumn('row_height', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: Constant(110)); - late final GeneratedColumn columnsCount = GeneratedColumn('columns_count', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: Constant(6)); - late final GeneratedColumn order = - GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, name, sort, type, rowHeight, columnsCount, order]; - @override - String get aliasedName => _alias ?? 'categories'; - @override - String get actualTableName => 'categories'; - @override - Set get $primaryKey => {id}; - @override - CategoriesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CategoriesData( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}name'])!, - sort: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}sort'])!, - type: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}type'])!, - rowHeight: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}row_height'])!, - columnsCount: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}columns_count'])!, - order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, - ); - } - - @override - Categories createAlias(String alias) { - return Categories(attachedDatabase, alias); - } -} - -class CategoriesData extends DataClass implements Insertable { - final int id; - final String name; - final int sort; - final int type; - final int rowHeight; - final int columnsCount; - final int order; - const CategoriesData( - {required this.id, - required this.name, - required this.sort, - required this.type, - required this.rowHeight, - required this.columnsCount, - required this.order}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['sort'] = Variable(sort); - map['type'] = Variable(type); - map['row_height'] = Variable(rowHeight); - map['columns_count'] = Variable(columnsCount); - map['order'] = Variable(order); - return map; - } - - CategoriesCompanion toCompanion(bool nullToAbsent) { - return CategoriesCompanion( - id: Value(id), - name: Value(name), - sort: Value(sort), - type: Value(type), - rowHeight: Value(rowHeight), - columnsCount: Value(columnsCount), - order: Value(order), - ); - } - - factory CategoriesData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return CategoriesData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - sort: serializer.fromJson(json['sort']), - type: serializer.fromJson(json['type']), - rowHeight: serializer.fromJson(json['rowHeight']), - columnsCount: serializer.fromJson(json['columnsCount']), - order: serializer.fromJson(json['order']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'sort': serializer.toJson(sort), - 'type': serializer.toJson(type), - 'rowHeight': serializer.toJson(rowHeight), - 'columnsCount': serializer.toJson(columnsCount), - 'order': serializer.toJson(order), - }; - } - - CategoriesData copyWith( - {int? id, String? name, int? sort, int? type, int? rowHeight, int? columnsCount, int? order}) => - CategoriesData( - id: id ?? this.id, - name: name ?? this.name, - sort: sort ?? this.sort, - type: type ?? this.type, - rowHeight: rowHeight ?? this.rowHeight, - columnsCount: columnsCount ?? this.columnsCount, - order: order ?? this.order, - ); - @override - String toString() { - return (StringBuffer('CategoriesData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('sort: $sort, ') - ..write('type: $type, ') - ..write('rowHeight: $rowHeight, ') - ..write('columnsCount: $columnsCount, ') - ..write('order: $order') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, sort, type, rowHeight, columnsCount, order); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is CategoriesData && - other.id == this.id && - other.name == this.name && - other.sort == this.sort && - other.type == this.type && - other.rowHeight == this.rowHeight && - other.columnsCount == this.columnsCount && - other.order == this.order); -} - -class CategoriesCompanion extends UpdateCompanion { - final Value id; - final Value name; - final Value sort; - final Value type; - final Value rowHeight; - final Value columnsCount; - final Value order; - const CategoriesCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.sort = const Value.absent(), - this.type = const Value.absent(), - this.rowHeight = const Value.absent(), - this.columnsCount = const Value.absent(), - this.order = const Value.absent(), - }); - CategoriesCompanion.insert({ - this.id = const Value.absent(), - required String name, - this.sort = const Value.absent(), - this.type = const Value.absent(), - this.rowHeight = const Value.absent(), - this.columnsCount = const Value.absent(), - required int order, - }) : name = Value(name), - order = Value(order); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? sort, - Expression? type, - Expression? rowHeight, - Expression? columnsCount, - Expression? order, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (sort != null) 'sort': sort, - if (type != null) 'type': type, - if (rowHeight != null) 'row_height': rowHeight, - if (columnsCount != null) 'columns_count': columnsCount, - if (order != null) 'order': order, - }); - } - - CategoriesCompanion copyWith( - {Value? id, - Value? name, - Value? sort, - Value? type, - Value? rowHeight, - Value? columnsCount, - Value? order}) { - return CategoriesCompanion( - id: id ?? this.id, - name: name ?? this.name, - sort: sort ?? this.sort, - type: type ?? this.type, - rowHeight: rowHeight ?? this.rowHeight, - columnsCount: columnsCount ?? this.columnsCount, - order: order ?? this.order, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (sort.present) { - map['sort'] = Variable(sort.value); - } - if (type.present) { - map['type'] = Variable(type.value); - } - if (rowHeight.present) { - map['row_height'] = Variable(rowHeight.value); - } - if (columnsCount.present) { - map['columns_count'] = Variable(columnsCount.value); - } - if (order.present) { - map['order'] = Variable(order.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('CategoriesCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('sort: $sort, ') - ..write('type: $type, ') - ..write('rowHeight: $rowHeight, ') - ..write('columnsCount: $columnsCount, ') - ..write('order: $order') - ..write(')')) - .toString(); - } -} - -class AppsCategories extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - AppsCategories(this.attachedDatabase, [this._alias]); - late final GeneratedColumn categoryId = GeneratedColumn('category_id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'REFERENCES categories(id) ON DELETE CASCADE'); - late final GeneratedColumn appPackageName = GeneratedColumn('app_package_name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'REFERENCES apps(package_name) ON DELETE CASCADE'); - late final GeneratedColumn order = - GeneratedColumn('order', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [categoryId, appPackageName, order]; - @override - String get aliasedName => _alias ?? 'apps_categories'; - @override - String get actualTableName => 'apps_categories'; - @override - Set get $primaryKey => {categoryId, appPackageName}; - @override - AppsCategoriesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AppsCategoriesData( - categoryId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}category_id'])!, - appPackageName: - attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}app_package_name'])!, - order: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}order'])!, - ); - } - - @override - AppsCategories createAlias(String alias) { - return AppsCategories(attachedDatabase, alias); - } -} - -class AppsCategoriesData extends DataClass implements Insertable { - final int categoryId; - final String appPackageName; - final int order; - const AppsCategoriesData({required this.categoryId, required this.appPackageName, required this.order}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['category_id'] = Variable(categoryId); - map['app_package_name'] = Variable(appPackageName); - map['order'] = Variable(order); - return map; - } - - AppsCategoriesCompanion toCompanion(bool nullToAbsent) { - return AppsCategoriesCompanion( - categoryId: Value(categoryId), - appPackageName: Value(appPackageName), - order: Value(order), - ); - } - - factory AppsCategoriesData.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AppsCategoriesData( - categoryId: serializer.fromJson(json['categoryId']), - appPackageName: serializer.fromJson(json['appPackageName']), - order: serializer.fromJson(json['order']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'categoryId': serializer.toJson(categoryId), - 'appPackageName': serializer.toJson(appPackageName), - 'order': serializer.toJson(order), - }; - } - - AppsCategoriesData copyWith({int? categoryId, String? appPackageName, int? order}) => AppsCategoriesData( - categoryId: categoryId ?? this.categoryId, - appPackageName: appPackageName ?? this.appPackageName, - order: order ?? this.order, - ); - @override - String toString() { - return (StringBuffer('AppsCategoriesData(') - ..write('categoryId: $categoryId, ') - ..write('appPackageName: $appPackageName, ') - ..write('order: $order') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(categoryId, appPackageName, order); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppsCategoriesData && - other.categoryId == this.categoryId && - other.appPackageName == this.appPackageName && - other.order == this.order); -} - -class AppsCategoriesCompanion extends UpdateCompanion { - final Value categoryId; - final Value appPackageName; - final Value order; - const AppsCategoriesCompanion({ - this.categoryId = const Value.absent(), - this.appPackageName = const Value.absent(), - this.order = const Value.absent(), - }); - AppsCategoriesCompanion.insert({ - required int categoryId, - required String appPackageName, - required int order, - }) : categoryId = Value(categoryId), - appPackageName = Value(appPackageName), - order = Value(order); - static Insertable custom({ - Expression? categoryId, - Expression? appPackageName, - Expression? order, - }) { - return RawValuesInsertable({ - if (categoryId != null) 'category_id': categoryId, - if (appPackageName != null) 'app_package_name': appPackageName, - if (order != null) 'order': order, - }); - } - - AppsCategoriesCompanion copyWith({Value? categoryId, Value? appPackageName, Value? order}) { - return AppsCategoriesCompanion( - categoryId: categoryId ?? this.categoryId, - appPackageName: appPackageName ?? this.appPackageName, - order: order ?? this.order, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (categoryId.present) { - map['category_id'] = Variable(categoryId.value); - } - if (appPackageName.present) { - map['app_package_name'] = Variable(appPackageName.value); - } - if (order.present) { - map['order'] = Variable(order.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AppsCategoriesCompanion(') - ..write('categoryId: $categoryId, ') - ..write('appPackageName: $appPackageName, ') - ..write('order: $order') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV5 extends GeneratedDatabase { - DatabaseAtV5(QueryExecutor e) : super(e); - late final Apps apps = Apps(this); - late final Categories categories = Categories(this); - late final AppsCategories appsCategories = AppsCategories(this); - @override - Iterable> get allTables => allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [apps, categories, appsCategories]; - @override - int get schemaVersion => 5; -} diff --git a/test/helpers.dart b/test/helpers.dart index f630cbc0..1da33cda 100644 --- a/test/helpers.dart +++ b/test/helpers.dart @@ -12,11 +12,9 @@ Element? findAppCardByPackageName(WidgetTester tester, String packageName) { } Element? findSettingsIcon(WidgetTester tester) { - // this function seems strange, but this is the simplest way I had to find the settings icon button - for (var val in tester.elementList(find.byIcon(Icons.settings_outlined))) { - if (((val as StatelessElement).widget as Icon).color == null) { - return val; - } + try { + return tester.element(find.byIcon(Icons.settings_outlined)); + } catch (e) { + return null; } - return null; } diff --git a/test/mocks.dart b/test/mocks.dart index 67c27072..1caa88aa 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -24,7 +24,9 @@ import 'package:flauncher/providers/apps_service.dart'; import 'package:flauncher/providers/settings_service.dart'; import 'package:flauncher/providers/network_service.dart'; import 'package:flauncher/providers/wallpaper_service.dart'; -import 'package:flauncher/providers/network_service.dart'; +import 'package:flauncher/providers/notifications_service.dart'; +import 'package:flauncher/providers/tv_inputs_service.dart'; +import 'package:flauncher/providers/watch_next_service.dart'; import 'package:flutter/cupertino.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mockito/annotations.dart'; @@ -39,9 +41,12 @@ import 'package:flauncher/models/category.dart'; SettingsService, NetworkService, ImagePicker, + NotificationsService, + TvInputsService, + WatchNextService, ], customMocks: [ MockSpec(unsupportedMembers: {#alias}), - MockSpec(unsupportedMembers: {#alias}), + MockSpec(unsupportedMembers: {#alias, #resolve, #createStream, #loadBuffer, #loadImage}), ]) void main() {} diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart deleted file mode 100644 index 03b00e3f..00000000 --- a/test/mocks.mocks.dart +++ /dev/null @@ -1,3067 +0,0 @@ -// Mocks generated by Mockito 5.4.4 from annotations -// in flauncher/test/mocks.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i9; -import 'dart:typed_data' as _i13; -import 'dart:ui' as _i4; - -import 'package:drift/drift.dart' as _i6; -import 'package:drift/src/runtime/executor/stream_queries.dart' as _i8; -import 'package:flauncher/database.dart' as _i7; -import 'package:flauncher/flauncher_channel.dart' as _i12; -import 'package:flauncher/gradients.dart' as _i2; -import 'package:flauncher/models/app.dart' as _i16; -import 'package:flauncher/models/category.dart' as _i3; -import 'package:flauncher/providers/apps_service.dart' as _i15; -import 'package:flauncher/providers/network_service.dart' as _i19; -import 'package:flauncher/providers/settings_service.dart' as _i17; -import 'package:flauncher/providers/wallpaper_service.dart' as _i14; -import 'package:flutter/cupertino.dart' as _i10; -import 'package:flutter/services.dart' as _i11; -import 'package:image_picker/image_picker.dart' as _i5; -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i18; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeFLauncherGradient_0 extends _i1.SmartFake - implements _i2.FLauncherGradient { - _FakeFLauncherGradient_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeCategory_1 extends _i1.SmartFake implements _i3.Category { - _FakeCategory_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeColor_2 extends _i1.SmartFake implements _i4.Color { - _FakeColor_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeLostDataResponse_3 extends _i1.SmartFake - implements _i5.LostDataResponse { - _FakeLostDataResponse_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeMigrationStrategy_4 extends _i1.SmartFake - implements _i6.MigrationStrategy { - _FakeMigrationStrategy_4( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _Fake$AppsTable_5 extends _i1.SmartFake implements _i7.$AppsTable { - _Fake$AppsTable_5( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _Fake$CategoriesTable_6 extends _i1.SmartFake - implements _i7.$CategoriesTable { - _Fake$CategoriesTable_6( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _Fake$AppsCategoriesTable_7 extends _i1.SmartFake - implements _i7.$AppsCategoriesTable { - _Fake$AppsCategoriesTable_7( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _Fake$LauncherSpacersTable_8 extends _i1.SmartFake - implements _i7.$LauncherSpacersTable { - _Fake$LauncherSpacersTable_8( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _Fake$FLauncherDatabaseManager_9 extends _i1.SmartFake - implements _i7.$FLauncherDatabaseManager { - _Fake$FLauncherDatabaseManager_9( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeStreamQueryUpdateRules_10 extends _i1.SmartFake - implements _i6.StreamQueryUpdateRules { - _FakeStreamQueryUpdateRules_10( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeGeneratedDatabase_11 extends _i1.SmartFake - implements _i6.GeneratedDatabase { - _FakeGeneratedDatabase_11( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeDriftDatabaseOptions_12 extends _i1.SmartFake - implements _i6.DriftDatabaseOptions { - _FakeDriftDatabaseOptions_12( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeDatabaseConnection_13 extends _i1.SmartFake - implements _i6.DatabaseConnection { - _FakeDatabaseConnection_13( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeQueryExecutor_14 extends _i1.SmartFake implements _i6.QueryExecutor { - _FakeQueryExecutor_14( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeStreamQueryStore_15 extends _i1.SmartFake - implements _i8.StreamQueryStore { - _FakeStreamQueryStore_15( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeDatabaseConnectionUser_16 extends _i1.SmartFake - implements _i6.DatabaseConnectionUser { - _FakeDatabaseConnectionUser_16( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeMigrator_17 extends _i1.SmartFake implements _i6.Migrator { - _FakeMigrator_17( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeFuture_18 extends _i1.SmartFake implements _i9.Future { - _FakeFuture_18( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeInsertStatement_19 extends _i1.SmartFake - implements _i6.InsertStatement { - _FakeInsertStatement_19( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeUpdateStatement_20 extends _i1.SmartFake - implements _i6.UpdateStatement { - _FakeUpdateStatement_20( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeSimpleSelectStatement_21 - extends _i1.SmartFake implements _i6.SimpleSelectStatement { - _FakeSimpleSelectStatement_21( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeJoinedSelectStatement_22 - extends _i1.SmartFake implements _i6.JoinedSelectStatement { - _FakeJoinedSelectStatement_22( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeSelectable_23 extends _i1.SmartFake implements _i6.Selectable { - _FakeSelectable_23( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeDeleteStatement_24 extends _i1.SmartFake - implements _i6.DeleteStatement { - _FakeDeleteStatement_24( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeGenerationContext_25 extends _i1.SmartFake - implements _i6.GenerationContext { - _FakeGenerationContext_25( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeImageStream_26 extends _i1.SmartFake implements _i10.ImageStream { - _FakeImageStream_26( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); - - @override - String toString( - {_i11.DiagnosticLevel? minLevel = _i11.DiagnosticLevel.info}) => - super.toString(); -} - -class _FakeImageStreamCompleter_27 extends _i1.SmartFake - implements _i10.ImageStreamCompleter { - _FakeImageStreamCompleter_27( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); - - @override - String toString( - {_i11.DiagnosticLevel? minLevel = _i11.DiagnosticLevel.info}) => - super.toString(); -} - -/// A class which mocks [FLauncherChannel]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockFLauncherChannel extends _i1.Mock implements _i12.FLauncherChannel { - MockFLauncherChannel() { - _i1.throwOnMissingStub(this); - } - - @override - _i9.Future>> getApplications() => - (super.noSuchMethod( - Invocation.method( - #getApplications, - [], - ), - returnValue: _i9.Future>>.value( - >[]), - ) as _i9.Future>>); - - @override - _i9.Future<_i13.Uint8List> getApplicationBanner(String? packageName) => - (super.noSuchMethod( - Invocation.method( - #getApplicationBanner, - [packageName], - ), - returnValue: _i9.Future<_i13.Uint8List>.value(_i13.Uint8List(0)), - ) as _i9.Future<_i13.Uint8List>); - - @override - _i9.Future<_i13.Uint8List> getApplicationIcon(String? packageName) => - (super.noSuchMethod( - Invocation.method( - #getApplicationIcon, - [packageName], - ), - returnValue: _i9.Future<_i13.Uint8List>.value(_i13.Uint8List(0)), - ) as _i9.Future<_i13.Uint8List>); - - @override - _i9.Future launchActivityFromAction(String? action) => - (super.noSuchMethod( - Invocation.method( - #launchActivityFromAction, - [action], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future launchApp(String? packageName) => (super.noSuchMethod( - Invocation.method( - #launchApp, - [packageName], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future openSettings() => (super.noSuchMethod( - Invocation.method( - #openSettings, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future openAppInfo(String? packageName) => (super.noSuchMethod( - Invocation.method( - #openAppInfo, - [packageName], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future uninstallApp(String? packageName) => (super.noSuchMethod( - Invocation.method( - #uninstallApp, - [packageName], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future isDefaultLauncher() => (super.noSuchMethod( - Invocation.method( - #isDefaultLauncher, - [], - ), - returnValue: _i9.Future.value(false), - ) as _i9.Future); - - @override - _i9.Future checkForGetContentAvailability() => (super.noSuchMethod( - Invocation.method( - #checkForGetContentAvailability, - [], - ), - returnValue: _i9.Future.value(false), - ) as _i9.Future); - - @override - _i9.Future> getActiveNetworkInformation() => - (super.noSuchMethod( - Invocation.method( - #getActiveNetworkInformation, - [], - ), - returnValue: - _i9.Future>.value({}), - ) as _i9.Future>); - - @override - _i9.Future getDailyDataUsage() => (super.noSuchMethod( - Invocation.method( - #getDailyDataUsage, - [], - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - _i9.Future getWeeklyDataUsage() => (super.noSuchMethod( - Invocation.method( - #getWeeklyDataUsage, - [], - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - _i9.Future getMonthlyDataUsage() => (super.noSuchMethod( - Invocation.method( - #getMonthlyDataUsage, - [], - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - _i9.Future checkUsageStatsPermission() => (super.noSuchMethod( - Invocation.method( - #checkUsageStatsPermission, - [], - ), - returnValue: _i9.Future.value(false), - ) as _i9.Future); - - @override - _i9.Future requestUsageStatsPermission() => (super.noSuchMethod( - Invocation.method( - #requestUsageStatsPermission, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future openWifiSettings() => (super.noSuchMethod( - Invocation.method( - #openWifiSettings, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future startAmbientMode() => (super.noSuchMethod( - Invocation.method( - #startAmbientMode, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - void addAppsChangedListener(void Function(Map)? listener) => - super.noSuchMethod( - Invocation.method( - #addAppsChangedListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void addNetworkChangedListener( - void Function(Map)? listener) => - super.noSuchMethod( - Invocation.method( - #addNetworkChangedListener, - [listener], - ), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [WallpaperService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWallpaperService extends _i1.Mock implements _i14.WallpaperService { - MockWallpaperService() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.FLauncherGradient get gradient => (super.noSuchMethod( - Invocation.getter(#gradient), - returnValue: _FakeFLauncherGradient_0( - this, - Invocation.getter(#gradient), - ), - ) as _i2.FLauncherGradient); - - @override - bool get hasListeners => (super.noSuchMethod( - Invocation.getter(#hasListeners), - returnValue: false, - ) as bool); - - @override - void dispose() => super.noSuchMethod( - Invocation.method( - #dispose, - [], - ), - returnValueForMissingStub: null, - ); - - @override - _i9.Future pickWallpaper() => (super.noSuchMethod( - Invocation.method( - #pickWallpaper, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future pickWallpaperDay() => (super.noSuchMethod( - Invocation.method( - #pickWallpaperDay, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future pickWallpaperNight() => (super.noSuchMethod( - Invocation.method( - #pickWallpaperNight, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setGradient(_i2.FLauncherGradient? fLauncherGradient) => - (super.noSuchMethod( - Invocation.method( - #setGradient, - [fLauncherGradient], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - void addListener(_i4.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #addListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void removeListener(_i4.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #removeListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method( - #notifyListeners, - [], - ), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [AppsService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockAppsService extends _i1.Mock implements _i15.AppsService { - MockAppsService() { - _i1.throwOnMissingStub(this); - } - - @override - bool get initialized => (super.noSuchMethod( - Invocation.getter(#initialized), - returnValue: false, - ) as bool); - - @override - List<_i16.App> get applications => (super.noSuchMethod( - Invocation.getter(#applications), - returnValue: <_i16.App>[], - ) as List<_i16.App>); - - @override - List<_i3.LauncherSection> get launcherSections => (super.noSuchMethod( - Invocation.getter(#launcherSections), - returnValue: <_i3.LauncherSection>[], - ) as List<_i3.LauncherSection>); - - @override - List<_i3.Category> get categories => (super.noSuchMethod( - Invocation.getter(#categories), - returnValue: <_i3.Category>[], - ) as List<_i3.Category>); - - @override - bool get hasListeners => (super.noSuchMethod( - Invocation.getter(#hasListeners), - returnValue: false, - ) as bool); - - @override - void clearPendingReorderFocusPackage() => super.noSuchMethod( - Invocation.method( - #clearPendingReorderFocusPackage, - [], - ), - returnValueForMissingStub: null, - ); - - @override - void setPendingReorderFocus( - String? packageName, - int? categoryId, - ) => - super.noSuchMethod( - Invocation.method( - #setPendingReorderFocus, - [ - packageName, - categoryId, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void sortCategory(_i3.Category? category) => super.noSuchMethod( - Invocation.method( - #sortCategory, - [category], - ), - returnValueForMissingStub: null, - ); - - @override - _i9.Future<_i13.Uint8List> getAppBanner(String? packageName) => - (super.noSuchMethod( - Invocation.method( - #getAppBanner, - [packageName], - ), - returnValue: _i9.Future<_i13.Uint8List>.value(_i13.Uint8List(0)), - ) as _i9.Future<_i13.Uint8List>); - - @override - _i9.Future setCustomAppBanner( - String? packageName, - String? imagePath, - ) => - (super.noSuchMethod( - Invocation.method( - #setCustomAppBanner, - [ - packageName, - imagePath, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future removeCustomAppBanner(String? packageName) => - (super.noSuchMethod( - Invocation.method( - #removeCustomAppBanner, - [packageName], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future hasCustomBanner(String? packageName) => (super.noSuchMethod( - Invocation.method( - #hasCustomBanner, - [packageName], - ), - returnValue: _i9.Future.value(false), - ) as _i9.Future); - - @override - _i9.Future<_i13.Uint8List> getAppIcon(String? packageName) => - (super.noSuchMethod( - Invocation.method( - #getAppIcon, - [packageName], - ), - returnValue: _i9.Future<_i13.Uint8List>.value(_i13.Uint8List(0)), - ) as _i9.Future<_i13.Uint8List>); - - @override - _i9.Future launchApp(_i16.App? app) => (super.noSuchMethod( - Invocation.method( - #launchApp, - [app], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future openAppInfo(_i16.App? app) => (super.noSuchMethod( - Invocation.method( - #openAppInfo, - [app], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future uninstallApp(_i16.App? app) => (super.noSuchMethod( - Invocation.method( - #uninstallApp, - [app], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future openSettings() => (super.noSuchMethod( - Invocation.method( - #openSettings, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future isDefaultLauncher() => (super.noSuchMethod( - Invocation.method( - #isDefaultLauncher, - [], - ), - returnValue: _i9.Future.value(false), - ) as _i9.Future); - - @override - _i9.Future startAmbientMode() => (super.noSuchMethod( - Invocation.method( - #startAmbientMode, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future addToCategory( - _i16.App? app, - _i3.Category? category, { - bool? shouldNotifyListeners = true, - }) => - (super.noSuchMethod( - Invocation.method( - #addToCategory, - [ - app, - category, - ], - {#shouldNotifyListeners: shouldNotifyListeners}, - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future addAllToCategory( - Iterable<_i16.App>? apps, - _i3.Category? category, { - bool? shouldNotifyListeners = true, - }) => - (super.noSuchMethod( - Invocation.method( - #addAllToCategory, - [ - apps, - category, - ], - {#shouldNotifyListeners: shouldNotifyListeners}, - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future removeFromCategory( - _i16.App? application, - _i3.Category? category, - ) => - (super.noSuchMethod( - Invocation.method( - #removeFromCategory, - [ - application, - category, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future autoPopulateCategory(_i3.Category? category) => - (super.noSuchMethod( - Invocation.method( - #autoPopulateCategory, - [category], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future<_i3.Category> getOrCreateFavoritesCategory() => - (super.noSuchMethod( - Invocation.method( - #getOrCreateFavoritesCategory, - [], - ), - returnValue: _i9.Future<_i3.Category>.value(_FakeCategory_1( - this, - Invocation.method( - #getOrCreateFavoritesCategory, - [], - ), - )), - ) as _i9.Future<_i3.Category>); - - @override - bool isAppInFavorites(_i16.App? app) => (super.noSuchMethod( - Invocation.method( - #isAppInFavorites, - [app], - ), - returnValue: false, - ) as bool); - - @override - _i9.Future addToFavorites(_i16.App? app) => (super.noSuchMethod( - Invocation.method( - #addToFavorites, - [app], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future removeFromFavorites(_i16.App? app) => (super.noSuchMethod( - Invocation.method( - #removeFromFavorites, - [app], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future toggleFavorite(_i16.App? app) => (super.noSuchMethod( - Invocation.method( - #toggleFavorite, - [app], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future saveApplicationOrderInCategory(_i3.Category? category) => - (super.noSuchMethod( - Invocation.method( - #saveApplicationOrderInCategory, - [category], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future moveAppToAdjacentCategory( - _i16.App? app, - _i3.Category? currentCategory, - _i10.AxisDirection? direction, - ) => - (super.noSuchMethod( - Invocation.method( - #moveAppToAdjacentCategory, - [ - app, - currentCategory, - direction, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - void reorderApplication( - _i3.Category? category, - int? oldIndex, - int? newIndex, - ) => - super.noSuchMethod( - Invocation.method( - #reorderApplication, - [ - category, - oldIndex, - newIndex, - ], - ), - returnValueForMissingStub: null, - ); - - @override - _i9.Future addCategory( - String? categoryName, { - _i3.CategorySort? sort = _i3.CategorySort.manual, - _i3.CategoryType? type = _i3.CategoryType.row, - int? columnsCount = 6, - int? rowHeight = 110, - bool? shouldNotifyListeners = true, - }) => - (super.noSuchMethod( - Invocation.method( - #addCategory, - [categoryName], - { - #sort: sort, - #type: type, - #columnsCount: columnsCount, - #rowHeight: rowHeight, - #shouldNotifyListeners: shouldNotifyListeners, - }, - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - _i9.Future updateCategory( - int? categoryId, - String? name, - _i3.CategorySort? sort, - _i3.CategoryType? type, - int? columnsCount, - int? rowHeight, { - bool? shouldNotifyListeners = true, - }) => - (super.noSuchMethod( - Invocation.method( - #updateCategory, - [ - categoryId, - name, - sort, - type, - columnsCount, - rowHeight, - ], - {#shouldNotifyListeners: shouldNotifyListeners}, - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future addSpacer(int? height) => (super.noSuchMethod( - Invocation.method( - #addSpacer, - [height], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future updateSpacerHeight( - _i3.LauncherSpacer? spacer, - int? height, - ) => - (super.noSuchMethod( - Invocation.method( - #updateSpacerHeight, - [ - spacer, - height, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future renameCategory( - _i3.Category? category, - String? categoryName, - ) => - (super.noSuchMethod( - Invocation.method( - #renameCategory, - [ - category, - categoryName, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future deleteSection(int? index) => (super.noSuchMethod( - Invocation.method( - #deleteSection, - [index], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - void moveSectionInMemory( - int? oldIndex, - int? newIndex, - ) => - super.noSuchMethod( - Invocation.method( - #moveSectionInMemory, - [ - oldIndex, - newIndex, - ], - ), - returnValueForMissingStub: null, - ); - - @override - _i9.Future persistSectionsOrder() => (super.noSuchMethod( - Invocation.method( - #persistSectionsOrder, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future moveSection( - int? oldIndex, - int? newIndex, - ) => - (super.noSuchMethod( - Invocation.method( - #moveSection, - [ - oldIndex, - newIndex, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future hideApplication(_i16.App? application) => - (super.noSuchMethod( - Invocation.method( - #hideApplication, - [application], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future showApplication(_i16.App? application) => - (super.noSuchMethod( - Invocation.method( - #showApplication, - [application], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setCategoryType( - _i3.Category? category, - _i3.CategoryType? type, { - bool? shouldNotifyListeners = true, - }) => - (super.noSuchMethod( - Invocation.method( - #setCategoryType, - [ - category, - type, - ], - {#shouldNotifyListeners: shouldNotifyListeners}, - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setCategorySort( - _i3.Category? category, - _i3.CategorySort? sort, - ) => - (super.noSuchMethod( - Invocation.method( - #setCategorySort, - [ - category, - sort, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setCategoryColumnsCount( - _i3.Category? category, - int? columnsCount, - ) => - (super.noSuchMethod( - Invocation.method( - #setCategoryColumnsCount, - [ - category, - columnsCount, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setCategoryRowHeight( - _i3.Category? category, - int? rowHeight, - ) => - (super.noSuchMethod( - Invocation.method( - #setCategoryRowHeight, - [ - category, - rowHeight, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - void addListener(_i4.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #addListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void removeListener(_i4.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #removeListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void dispose() => super.noSuchMethod( - Invocation.method( - #dispose, - [], - ), - returnValueForMissingStub: null, - ); - - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method( - #notifyListeners, - [], - ), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [SettingsService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockSettingsService extends _i1.Mock implements _i17.SettingsService { - MockSettingsService() { - _i1.throwOnMissingStub(this); - } - - @override - bool get appHighlightAnimationEnabled => (super.noSuchMethod( - Invocation.getter(#appHighlightAnimationEnabled), - returnValue: false, - ) as bool); - - @override - bool get appKeyClickEnabled => (super.noSuchMethod( - Invocation.getter(#appKeyClickEnabled), - returnValue: false, - ) as bool); - - @override - bool get autoHideAppBarEnabled => (super.noSuchMethod( - Invocation.getter(#autoHideAppBarEnabled), - returnValue: false, - ) as bool); - - @override - bool get showCategoryTitles => (super.noSuchMethod( - Invocation.getter(#showCategoryTitles), - returnValue: false, - ) as bool); - - @override - bool get showAppNamesBelowIcons => (super.noSuchMethod( - Invocation.getter(#showAppNamesBelowIcons), - returnValue: false, - ) as bool); - - @override - String get themes => (super.noSuchMethod( - Invocation.getter(#themes), - returnValue: _i18.dummyValue( - this, - Invocation.getter(#themes), - ), - ) as String); - - @override - bool get hideHighlightOutlineOnHomescreen => (super.noSuchMethod( - Invocation.getter(#hideHighlightOutlineOnHomescreen), - returnValue: false, - ) as bool); - - @override - bool get appSelectorTransitionAnimationEnabled => (super.noSuchMethod( - Invocation.getter(#appSelectorTransitionAnimationEnabled), - returnValue: false, - ) as bool); - - @override - bool get showDateInStatusBar => (super.noSuchMethod( - Invocation.getter(#showDateInStatusBar), - returnValue: false, - ) as bool); - - @override - bool get showTimeInStatusBar => (super.noSuchMethod( - Invocation.getter(#showTimeInStatusBar), - returnValue: false, - ) as bool); - - @override - String get backButtonAction => (super.noSuchMethod( - Invocation.getter(#backButtonAction), - returnValue: _i18.dummyValue( - this, - Invocation.getter(#backButtonAction), - ), - ) as String); - - @override - String get dateFormat => (super.noSuchMethod( - Invocation.getter(#dateFormat), - returnValue: _i18.dummyValue( - this, - Invocation.getter(#dateFormat), - ), - ) as String); - - @override - String get timeFormat => (super.noSuchMethod( - Invocation.getter(#timeFormat), - returnValue: _i18.dummyValue( - this, - Invocation.getter(#timeFormat), - ), - ) as String); - - @override - String get dataUsagePeriod => (super.noSuchMethod( - Invocation.getter(#dataUsagePeriod), - returnValue: _i18.dummyValue( - this, - Invocation.getter(#dataUsagePeriod), - ), - ) as String); - - @override - bool get showDataWidgetInStatusBar => (super.noSuchMethod( - Invocation.getter(#showDataWidgetInStatusBar), - returnValue: false, - ) as bool); - - @override - bool get showNetworkIndicatorInStatusBar => (super.noSuchMethod( - Invocation.getter(#showNetworkIndicatorInStatusBar), - returnValue: false, - ) as bool); - - @override - String get accentColorHex => (super.noSuchMethod( - Invocation.getter(#accentColorHex), - returnValue: _i18.dummyValue( - this, - Invocation.getter(#accentColorHex), - ), - ) as String); - - @override - String get screensaverClockStyle => (super.noSuchMethod( - Invocation.getter(#screensaverClockStyle), - returnValue: _i18.dummyValue( - this, - Invocation.getter(#screensaverClockStyle), - ), - ) as String); - - @override - _i4.Color get accentColor => (super.noSuchMethod( - Invocation.getter(#accentColor), - returnValue: _FakeColor_2( - this, - Invocation.getter(#accentColor), - ), - ) as _i4.Color); - - @override - bool get timeBasedWallpaperEnabled => (super.noSuchMethod( - Invocation.getter(#timeBasedWallpaperEnabled), - returnValue: false, - ) as bool); - - @override - bool get hasListeners => (super.noSuchMethod( - Invocation.getter(#hasListeners), - returnValue: false, - ) as bool); - - @override - _i9.Future set( - String? key, - bool? value, - ) => - (super.noSuchMethod( - Invocation.method( - #set, - [ - key, - value, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setAppHighlightAnimationEnabled(bool? value) => - (super.noSuchMethod( - Invocation.method( - #setAppHighlightAnimationEnabled, - [value], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setAppKeyClickEnabled(bool? value) => (super.noSuchMethod( - Invocation.method( - #setAppKeyClickEnabled, - [value], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setAutoHideAppBarEnabled(bool? value) => (super.noSuchMethod( - Invocation.method( - #setAutoHideAppBarEnabled, - [value], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setGradientUuid(String? value) => (super.noSuchMethod( - Invocation.method( - #setGradientUuid, - [value], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setBackButtonAction(String? value) => (super.noSuchMethod( - Invocation.method( - #setBackButtonAction, - [value], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setDateTimeFormat( - String? dateFormatString, - String? timeFormatString, - ) => - (super.noSuchMethod( - Invocation.method( - #setDateTimeFormat, - [ - dateFormatString, - timeFormatString, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setShowCategoryTitles(bool? show) => (super.noSuchMethod( - Invocation.method( - #setShowCategoryTitles, - [show], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setShowAppNamesBelowIcons(bool? show) => (super.noSuchMethod( - Invocation.method( - #setShowAppNamesBelowIcons, - [show], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setThemes(String? shape) => (super.noSuchMethod( - Invocation.method( - #setThemes, - [shape], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setHideHighlightOutlineOnHomescreen(bool? enabled) => - (super.noSuchMethod( - Invocation.method( - #setHideHighlightOutlineOnHomescreen, - [enabled], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setAppSelectorTransitionAnimationEnabled(bool? enabled) => - (super.noSuchMethod( - Invocation.method( - #setAppSelectorTransitionAnimationEnabled, - [enabled], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setShowDateInStatusBar(bool? show) => (super.noSuchMethod( - Invocation.method( - #setShowDateInStatusBar, - [show], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setShowTimeInStatusBar(bool? show) => (super.noSuchMethod( - Invocation.method( - #setShowTimeInStatusBar, - [show], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setDataUsagePeriod(String? period) => (super.noSuchMethod( - Invocation.method( - #setDataUsagePeriod, - [period], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setShowDataWidgetInStatusBar(bool? show) => - (super.noSuchMethod( - Invocation.method( - #setShowDataWidgetInStatusBar, - [show], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setShowNetworkIndicatorInStatusBar(bool? show) => - (super.noSuchMethod( - Invocation.method( - #setShowNetworkIndicatorInStatusBar, - [show], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setAccentColor(String? colorHex) => (super.noSuchMethod( - Invocation.method( - #setAccentColor, - [colorHex], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setScreensaverClockStyle(String? style) => - (super.noSuchMethod( - Invocation.method( - #setScreensaverClockStyle, - [style], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future setTimeBasedWallpaperEnabled(bool? enabled) => - (super.noSuchMethod( - Invocation.method( - #setTimeBasedWallpaperEnabled, - [enabled], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - void addListener(_i4.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #addListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void removeListener(_i4.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #removeListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void dispose() => super.noSuchMethod( - Invocation.method( - #dispose, - [], - ), - returnValueForMissingStub: null, - ); - - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method( - #notifyListeners, - [], - ), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [NetworkService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockNetworkService extends _i1.Mock implements _i19.NetworkService { - MockNetworkService() { - _i1.throwOnMissingStub(this); - } - - @override - bool get hasInternetAccess => (super.noSuchMethod( - Invocation.getter(#hasInternetAccess), - returnValue: false, - ) as bool); - - @override - _i19.CellularNetworkType get cellularNetworkType => (super.noSuchMethod( - Invocation.getter(#cellularNetworkType), - returnValue: _i19.CellularNetworkType.Unknown, - ) as _i19.CellularNetworkType); - - @override - _i19.NetworkType get networkType => (super.noSuchMethod( - Invocation.getter(#networkType), - returnValue: _i19.NetworkType.Cellular, - ) as _i19.NetworkType); - - @override - int get wirelessNetworkSignalLevel => (super.noSuchMethod( - Invocation.getter(#wirelessNetworkSignalLevel), - returnValue: 0, - ) as int); - - @override - int get dailyDataUsage => (super.noSuchMethod( - Invocation.getter(#dailyDataUsage), - returnValue: 0, - ) as int); - - @override - bool get hasUsageStatsPermission => (super.noSuchMethod( - Invocation.getter(#hasUsageStatsPermission), - returnValue: false, - ) as bool); - - @override - bool get hasListeners => (super.noSuchMethod( - Invocation.getter(#hasListeners), - returnValue: false, - ) as bool); - - @override - _i9.Future requestPermission() => (super.noSuchMethod( - Invocation.method( - #requestPermission, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future refreshPermissionAndUsage() => (super.noSuchMethod( - Invocation.method( - #refreshPermissionAndUsage, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future openWifiSettings() => (super.noSuchMethod( - Invocation.method( - #openWifiSettings, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future getDataUsageForPeriod(String? period) => (super.noSuchMethod( - Invocation.method( - #getDataUsageForPeriod, - [period], - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - void dispose() => super.noSuchMethod( - Invocation.method( - #dispose, - [], - ), - returnValueForMissingStub: null, - ); - - @override - void addListener(_i4.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #addListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void removeListener(_i4.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #removeListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method( - #notifyListeners, - [], - ), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [ImagePicker]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockImagePicker extends _i1.Mock implements _i5.ImagePicker { - MockImagePicker() { - _i1.throwOnMissingStub(this); - } - - @override - _i9.Future<_i5.XFile?> pickImage({ - required _i5.ImageSource? source, - double? maxWidth, - double? maxHeight, - int? imageQuality, - _i5.CameraDevice? preferredCameraDevice = _i5.CameraDevice.rear, - bool? requestFullMetadata = true, - }) => - (super.noSuchMethod( - Invocation.method( - #pickImage, - [], - { - #source: source, - #maxWidth: maxWidth, - #maxHeight: maxHeight, - #imageQuality: imageQuality, - #preferredCameraDevice: preferredCameraDevice, - #requestFullMetadata: requestFullMetadata, - }, - ), - returnValue: _i9.Future<_i5.XFile?>.value(), - ) as _i9.Future<_i5.XFile?>); - - @override - _i9.Future> pickMultiImage({ - double? maxWidth, - double? maxHeight, - int? imageQuality, - int? limit, - bool? requestFullMetadata = true, - }) => - (super.noSuchMethod( - Invocation.method( - #pickMultiImage, - [], - { - #maxWidth: maxWidth, - #maxHeight: maxHeight, - #imageQuality: imageQuality, - #limit: limit, - #requestFullMetadata: requestFullMetadata, - }, - ), - returnValue: _i9.Future>.value(<_i5.XFile>[]), - ) as _i9.Future>); - - @override - _i9.Future<_i5.XFile?> pickMedia({ - double? maxWidth, - double? maxHeight, - int? imageQuality, - bool? requestFullMetadata = true, - }) => - (super.noSuchMethod( - Invocation.method( - #pickMedia, - [], - { - #maxWidth: maxWidth, - #maxHeight: maxHeight, - #imageQuality: imageQuality, - #requestFullMetadata: requestFullMetadata, - }, - ), - returnValue: _i9.Future<_i5.XFile?>.value(), - ) as _i9.Future<_i5.XFile?>); - - @override - _i9.Future> pickMultipleMedia({ - double? maxWidth, - double? maxHeight, - int? imageQuality, - int? limit, - bool? requestFullMetadata = true, - }) => - (super.noSuchMethod( - Invocation.method( - #pickMultipleMedia, - [], - { - #maxWidth: maxWidth, - #maxHeight: maxHeight, - #imageQuality: imageQuality, - #limit: limit, - #requestFullMetadata: requestFullMetadata, - }, - ), - returnValue: _i9.Future>.value(<_i5.XFile>[]), - ) as _i9.Future>); - - @override - _i9.Future<_i5.XFile?> pickVideo({ - required _i5.ImageSource? source, - _i5.CameraDevice? preferredCameraDevice = _i5.CameraDevice.rear, - Duration? maxDuration, - }) => - (super.noSuchMethod( - Invocation.method( - #pickVideo, - [], - { - #source: source, - #preferredCameraDevice: preferredCameraDevice, - #maxDuration: maxDuration, - }, - ), - returnValue: _i9.Future<_i5.XFile?>.value(), - ) as _i9.Future<_i5.XFile?>); - - @override - _i9.Future<_i5.LostDataResponse> retrieveLostData() => (super.noSuchMethod( - Invocation.method( - #retrieveLostData, - [], - ), - returnValue: - _i9.Future<_i5.LostDataResponse>.value(_FakeLostDataResponse_3( - this, - Invocation.method( - #retrieveLostData, - [], - ), - )), - ) as _i9.Future<_i5.LostDataResponse>); - - @override - bool supportsImageSource(_i5.ImageSource? source) => (super.noSuchMethod( - Invocation.method( - #supportsImageSource, - [source], - ), - returnValue: false, - ) as bool); -} - -/// A class which mocks [FLauncherDatabase]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockFLauncherDatabase extends _i1.Mock implements _i7.FLauncherDatabase { - MockFLauncherDatabase() { - _i1.throwOnMissingStub(this); - } - - @override - bool get wasCreated => (super.noSuchMethod( - Invocation.getter(#wasCreated), - returnValue: false, - ) as bool); - - @override - set wasCreated(bool? _wasCreated) => super.noSuchMethod( - Invocation.setter( - #wasCreated, - _wasCreated, - ), - returnValueForMissingStub: null, - ); - - @override - int get schemaVersion => (super.noSuchMethod( - Invocation.getter(#schemaVersion), - returnValue: 0, - ) as int); - - @override - _i6.MigrationStrategy get migration => (super.noSuchMethod( - Invocation.getter(#migration), - returnValue: _FakeMigrationStrategy_4( - this, - Invocation.getter(#migration), - ), - ) as _i6.MigrationStrategy); - - @override - _i7.$AppsTable get apps => (super.noSuchMethod( - Invocation.getter(#apps), - returnValue: _Fake$AppsTable_5( - this, - Invocation.getter(#apps), - ), - ) as _i7.$AppsTable); - - @override - _i7.$CategoriesTable get categories => (super.noSuchMethod( - Invocation.getter(#categories), - returnValue: _Fake$CategoriesTable_6( - this, - Invocation.getter(#categories), - ), - ) as _i7.$CategoriesTable); - - @override - _i7.$AppsCategoriesTable get appsCategories => (super.noSuchMethod( - Invocation.getter(#appsCategories), - returnValue: _Fake$AppsCategoriesTable_7( - this, - Invocation.getter(#appsCategories), - ), - ) as _i7.$AppsCategoriesTable); - - @override - _i7.$LauncherSpacersTable get launcherSpacers => (super.noSuchMethod( - Invocation.getter(#launcherSpacers), - returnValue: _Fake$LauncherSpacersTable_8( - this, - Invocation.getter(#launcherSpacers), - ), - ) as _i7.$LauncherSpacersTable); - - @override - _i7.$FLauncherDatabaseManager get managers => (super.noSuchMethod( - Invocation.getter(#managers), - returnValue: _Fake$FLauncherDatabaseManager_9( - this, - Invocation.getter(#managers), - ), - ) as _i7.$FLauncherDatabaseManager); - - @override - Iterable<_i6.TableInfo<_i6.Table, Object?>> get allTables => - (super.noSuchMethod( - Invocation.getter(#allTables), - returnValue: <_i6.TableInfo<_i6.Table, Object?>>[], - ) as Iterable<_i6.TableInfo<_i6.Table, Object?>>); - - @override - List<_i6.DatabaseSchemaEntity> get allSchemaEntities => (super.noSuchMethod( - Invocation.getter(#allSchemaEntities), - returnValue: <_i6.DatabaseSchemaEntity>[], - ) as List<_i6.DatabaseSchemaEntity>); - - @override - _i6.StreamQueryUpdateRules get streamUpdateRules => (super.noSuchMethod( - Invocation.getter(#streamUpdateRules), - returnValue: _FakeStreamQueryUpdateRules_10( - this, - Invocation.getter(#streamUpdateRules), - ), - ) as _i6.StreamQueryUpdateRules); - - @override - _i6.GeneratedDatabase get attachedDatabase => (super.noSuchMethod( - Invocation.getter(#attachedDatabase), - returnValue: _FakeGeneratedDatabase_11( - this, - Invocation.getter(#attachedDatabase), - ), - ) as _i6.GeneratedDatabase); - - @override - _i6.DriftDatabaseOptions get options => (super.noSuchMethod( - Invocation.getter(#options), - returnValue: _FakeDriftDatabaseOptions_12( - this, - Invocation.getter(#options), - ), - ) as _i6.DriftDatabaseOptions); - - @override - _i6.DatabaseConnection get connection => (super.noSuchMethod( - Invocation.getter(#connection), - returnValue: _FakeDatabaseConnection_13( - this, - Invocation.getter(#connection), - ), - ) as _i6.DatabaseConnection); - - @override - _i6.SqlTypes get typeMapping => (super.noSuchMethod( - Invocation.getter(#typeMapping), - returnValue: _i18.dummyValue<_i6.SqlTypes>( - this, - Invocation.getter(#typeMapping), - ), - ) as _i6.SqlTypes); - - @override - _i6.QueryExecutor get executor => (super.noSuchMethod( - Invocation.getter(#executor), - returnValue: _FakeQueryExecutor_14( - this, - Invocation.getter(#executor), - ), - ) as _i6.QueryExecutor); - - @override - _i8.StreamQueryStore get streamQueries => (super.noSuchMethod( - Invocation.getter(#streamQueries), - returnValue: _FakeStreamQueryStore_15( - this, - Invocation.getter(#streamQueries), - ), - ) as _i8.StreamQueryStore); - - @override - _i6.DatabaseConnectionUser get resolvedEngine => (super.noSuchMethod( - Invocation.getter(#resolvedEngine), - returnValue: _FakeDatabaseConnectionUser_16( - this, - Invocation.getter(#resolvedEngine), - ), - ) as _i6.DatabaseConnectionUser); - - @override - _i9.Future persistApps(Iterable<_i7.AppsCompanion>? applications) => - (super.noSuchMethod( - Invocation.method( - #persistApps, - [applications], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future updateApp( - String? packageName, - _i7.AppsCompanion? value, - ) => - (super.noSuchMethod( - Invocation.method( - #updateApp, - [ - packageName, - value, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future deleteApps(List? packageNames) => - (super.noSuchMethod( - Invocation.method( - #deleteApps, - [packageNames], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future insertCategory(_i6.Insertable<_i3.Category>? category) => - (super.noSuchMethod( - Invocation.method( - #insertCategory, - [category], - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - _i9.Future deleteCategory(int? id) => (super.noSuchMethod( - Invocation.method( - #deleteCategory, - [id], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future updateCategories(List<_i7.CategoriesCompanion>? values) => - (super.noSuchMethod( - Invocation.method( - #updateCategories, - [values], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future updateCategory( - int? id, - _i7.CategoriesCompanion? value, - ) => - (super.noSuchMethod( - Invocation.method( - #updateCategory, - [ - id, - value, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future deleteAppCategory( - int? categoryId, - String? packageName, - ) => - (super.noSuchMethod( - Invocation.method( - #deleteAppCategory, - [ - categoryId, - packageName, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future insertAppsCategories( - List<_i7.AppsCategoriesCompanion>? value) => - (super.noSuchMethod( - Invocation.method( - #insertAppsCategories, - [value], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future replaceAppsCategories( - List<_i7.AppsCategoriesCompanion>? value) => - (super.noSuchMethod( - Invocation.method( - #replaceAppsCategories, - [value], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future insertSpacer(_i6.Insertable<_i3.LauncherSpacer>? spacer) => - (super.noSuchMethod( - Invocation.method( - #insertSpacer, - [spacer], - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - _i9.Future deleteSpacer(int? spacerId) => (super.noSuchMethod( - Invocation.method( - #deleteSpacer, - [spacerId], - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - _i9.Future updateSpacer( - int? spacerId, - _i6.Insertable<_i3.LauncherSpacer>? insertable, - ) => - (super.noSuchMethod( - Invocation.method( - #updateSpacer, - [ - spacerId, - insertable, - ], - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - _i9.Future updateSpacers( - Iterable<_i7.LauncherSpacersCompanion>? values) => - (super.noSuchMethod( - Invocation.method( - #updateSpacers, - [values], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future> getCategories() => (super.noSuchMethod( - Invocation.method( - #getCategories, - [], - ), - returnValue: _i9.Future>.value(<_i3.Category>[]), - ) as _i9.Future>); - - @override - _i9.Future> getLauncherSpacers() => - (super.noSuchMethod( - Invocation.method( - #getLauncherSpacers, - [], - ), - returnValue: - _i9.Future>.value(<_i3.LauncherSpacer>[]), - ) as _i9.Future>); - - @override - _i9.Future> getAppsCategories() => (super.noSuchMethod( - Invocation.method( - #getAppsCategories, - [], - ), - returnValue: - _i9.Future>.value(<_i7.AppCategory>[]), - ) as _i9.Future>); - - @override - _i9.Future> getApplications() => (super.noSuchMethod( - Invocation.method( - #getApplications, - [], - ), - returnValue: _i9.Future>.value(<_i16.App>[]), - ) as _i9.Future>); - - @override - _i9.Future nextAppCategoryOrder(int? categoryId) => (super.noSuchMethod( - Invocation.method( - #nextAppCategoryOrder, - [categoryId], - ), - returnValue: _i9.Future.value(), - ) as _i9.Future); - - @override - _i6.Migrator createMigrator() => (super.noSuchMethod( - Invocation.method( - #createMigrator, - [], - ), - returnValue: _FakeMigrator_17( - this, - Invocation.method( - #createMigrator, - [], - ), - ), - ) as _i6.Migrator); - - @override - _i9.Future beforeOpen( - _i6.QueryExecutor? executor, - _i6.OpeningDetails? details, - ) => - (super.noSuchMethod( - Invocation.method( - #beforeOpen, - [ - executor, - details, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future close() => (super.noSuchMethod( - Invocation.method( - #close, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Stream>> createStream( - _i8.QueryStreamFetcher? stmt) => - (super.noSuchMethod( - Invocation.method( - #createStream, - [stmt], - ), - returnValue: _i9.Stream>>.empty(), - ) as _i9.Stream>>); - - @override - T alias( - _i6.ResultSetImplementation? table, - String? alias, - ) => - (super.noSuchMethod( - Invocation.method( - #alias, - [ - table, - alias, - ], - ), - returnValue: _i18.dummyValue( - this, - Invocation.method( - #alias, - [ - table, - alias, - ], - ), - ), - ) as T); - - @override - void markTablesUpdated(Iterable<_i6.TableInfo<_i6.Table, dynamic>>? tables) => - super.noSuchMethod( - Invocation.method( - #markTablesUpdated, - [tables], - ), - returnValueForMissingStub: null, - ); - - @override - void notifyUpdates(Set<_i6.TableUpdate>? updates) => super.noSuchMethod( - Invocation.method( - #notifyUpdates, - [updates], - ), - returnValueForMissingStub: null, - ); - - @override - _i9.Stream> tableUpdates( - [_i6.TableUpdateQuery? query = const _i6.TableUpdateQuery.any()]) => - (super.noSuchMethod( - Invocation.method( - #tableUpdates, - [query], - ), - returnValue: _i9.Stream>.empty(), - ) as _i9.Stream>); - - @override - _i9.Future doWhenOpened( - _i9.FutureOr Function(_i6.QueryExecutor)? fn) => - (super.noSuchMethod( - Invocation.method( - #doWhenOpened, - [fn], - ), - returnValue: _i18.ifNotNull( - _i18.dummyValueOrNull( - this, - Invocation.method( - #doWhenOpened, - [fn], - ), - ), - (T v) => _i9.Future.value(v), - ) ?? - _FakeFuture_18( - this, - Invocation.method( - #doWhenOpened, - [fn], - ), - ), - ) as _i9.Future); - - @override - _i6.InsertStatement into( - _i6.TableInfo? table) => - (super.noSuchMethod( - Invocation.method( - #into, - [table], - ), - returnValue: _FakeInsertStatement_19( - this, - Invocation.method( - #into, - [table], - ), - ), - ) as _i6.InsertStatement); - - @override - _i6.UpdateStatement update( - _i6.TableInfo? table) => - (super.noSuchMethod( - Invocation.method( - #update, - [table], - ), - returnValue: _FakeUpdateStatement_20( - this, - Invocation.method( - #update, - [table], - ), - ), - ) as _i6.UpdateStatement); - - @override - _i6.SimpleSelectStatement select( - _i6.ResultSetImplementation? table, { - bool? distinct = false, - }) => - (super.noSuchMethod( - Invocation.method( - #select, - [table], - {#distinct: distinct}, - ), - returnValue: _FakeSimpleSelectStatement_21( - this, - Invocation.method( - #select, - [table], - {#distinct: distinct}, - ), - ), - ) as _i6.SimpleSelectStatement); - - @override - _i6.JoinedSelectStatement selectOnly( - _i6.ResultSetImplementation? table, { - bool? distinct = false, - }) => - (super.noSuchMethod( - Invocation.method( - #selectOnly, - [table], - {#distinct: distinct}, - ), - returnValue: _FakeJoinedSelectStatement_22( - this, - Invocation.method( - #selectOnly, - [table], - {#distinct: distinct}, - ), - ), - ) as _i6.JoinedSelectStatement); - - @override - _i6.Selectable<_i6.TypedResult> selectExpressions( - Iterable<_i6.Expression>? columns) => - (super.noSuchMethod( - Invocation.method( - #selectExpressions, - [columns], - ), - returnValue: _FakeSelectable_23<_i6.TypedResult>( - this, - Invocation.method( - #selectExpressions, - [columns], - ), - ), - ) as _i6.Selectable<_i6.TypedResult>); - - @override - _i6.DeleteStatement delete( - _i6.TableInfo? table) => - (super.noSuchMethod( - Invocation.method( - #delete, - [table], - ), - returnValue: _FakeDeleteStatement_24( - this, - Invocation.method( - #delete, - [table], - ), - ), - ) as _i6.DeleteStatement); - - @override - _i9.Future customUpdate( - String? query, { - List<_i6.Variable>? variables = const [], - Set<_i6.TableInfo<_i6.Table, dynamic>>? updates, - _i6.UpdateKind? updateKind, - }) => - (super.noSuchMethod( - Invocation.method( - #customUpdate, - [query], - { - #variables: variables, - #updates: updates, - #updateKind: updateKind, - }, - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - _i9.Future customInsert( - String? query, { - List<_i6.Variable>? variables = const [], - Set<_i6.TableInfo<_i6.Table, dynamic>>? updates, - }) => - (super.noSuchMethod( - Invocation.method( - #customInsert, - [query], - { - #variables: variables, - #updates: updates, - }, - ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); - - @override - _i9.Future> customWriteReturning( - String? query, { - List<_i6.Variable>? variables = const [], - Set<_i6.TableInfo<_i6.Table, dynamic>>? updates, - _i6.UpdateKind? updateKind, - }) => - (super.noSuchMethod( - Invocation.method( - #customWriteReturning, - [query], - { - #variables: variables, - #updates: updates, - #updateKind: updateKind, - }, - ), - returnValue: _i9.Future>.value(<_i6.QueryRow>[]), - ) as _i9.Future>); - - @override - _i6.Selectable<_i6.QueryRow> customSelect( - String? query, { - List<_i6.Variable>? variables = const [], - Set<_i6.ResultSetImplementation>? readsFrom = const {}, - }) => - (super.noSuchMethod( - Invocation.method( - #customSelect, - [query], - { - #variables: variables, - #readsFrom: readsFrom, - }, - ), - returnValue: _FakeSelectable_23<_i6.QueryRow>( - this, - Invocation.method( - #customSelect, - [query], - { - #variables: variables, - #readsFrom: readsFrom, - }, - ), - ), - ) as _i6.Selectable<_i6.QueryRow>); - - @override - _i6.Selectable<_i6.QueryRow> customSelectQuery( - String? query, { - List<_i6.Variable>? variables = const [], - Set<_i6.ResultSetImplementation>? readsFrom = const {}, - }) => - (super.noSuchMethod( - Invocation.method( - #customSelectQuery, - [query], - { - #variables: variables, - #readsFrom: readsFrom, - }, - ), - returnValue: _FakeSelectable_23<_i6.QueryRow>( - this, - Invocation.method( - #customSelectQuery, - [query], - { - #variables: variables, - #readsFrom: readsFrom, - }, - ), - ), - ) as _i6.Selectable<_i6.QueryRow>); - - @override - _i9.Future customStatement( - String? statement, [ - List? args, - ]) => - (super.noSuchMethod( - Invocation.method( - #customStatement, - [ - statement, - args, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future transaction( - _i9.Future Function()? action, { - bool? requireNew = false, - }) => - (super.noSuchMethod( - Invocation.method( - #transaction, - [action], - {#requireNew: requireNew}, - ), - returnValue: _i18.ifNotNull( - _i18.dummyValueOrNull( - this, - Invocation.method( - #transaction, - [action], - {#requireNew: requireNew}, - ), - ), - (T v) => _i9.Future.value(v), - ) ?? - _FakeFuture_18( - this, - Invocation.method( - #transaction, - [action], - {#requireNew: requireNew}, - ), - ), - ) as _i9.Future); - - @override - _i9.Future exclusively(_i9.Future Function()? action) => - (super.noSuchMethod( - Invocation.method( - #exclusively, - [action], - ), - returnValue: _i18.ifNotNull( - _i18.dummyValueOrNull( - this, - Invocation.method( - #exclusively, - [action], - ), - ), - (T v) => _i9.Future.value(v), - ) ?? - _FakeFuture_18( - this, - Invocation.method( - #exclusively, - [action], - ), - ), - ) as _i9.Future); - - @override - _i9.Future batch(_i9.FutureOr Function(_i6.Batch)? runInBatch) => - (super.noSuchMethod( - Invocation.method( - #batch, - [runInBatch], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i6.GenerationContext $write( - _i6.Component? component, { - bool? hasMultipleTables, - int? startIndex, - }) => - (super.noSuchMethod( - Invocation.method( - #$write, - [component], - { - #hasMultipleTables: hasMultipleTables, - #startIndex: startIndex, - }, - ), - returnValue: _FakeGenerationContext_25( - this, - Invocation.method( - #$write, - [component], - { - #hasMultipleTables: hasMultipleTables, - #startIndex: startIndex, - }, - ), - ), - ) as _i6.GenerationContext); - - @override - _i6.GenerationContext $writeInsertable( - _i6.TableInfo<_i6.Table, dynamic>? table, - _i6.Insertable? insertable, { - int? startIndex, - }) => - (super.noSuchMethod( - Invocation.method( - #$writeInsertable, - [ - table, - insertable, - ], - {#startIndex: startIndex}, - ), - returnValue: _FakeGenerationContext_25( - this, - Invocation.method( - #$writeInsertable, - [ - table, - insertable, - ], - {#startIndex: startIndex}, - ), - ), - ) as _i6.GenerationContext); - - @override - String $expandVar( - int? start, - int? amount, - ) => - (super.noSuchMethod( - Invocation.method( - #$expandVar, - [ - start, - amount, - ], - ), - returnValue: _i18.dummyValue( - this, - Invocation.method( - #$expandVar, - [ - start, - amount, - ], - ), - ), - ) as String); -} - -/// A class which mocks [ImageProvider]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockImageProvider extends _i1.Mock - implements _i10.ImageProvider { - MockImageProvider() { - _i1.throwOnMissingStub(this); - } - - @override - _i10.ImageStream resolve(_i10.ImageConfiguration? configuration) => - (super.noSuchMethod( - Invocation.method( - #resolve, - [configuration], - ), - returnValue: _FakeImageStream_26( - this, - Invocation.method( - #resolve, - [configuration], - ), - ), - ) as _i10.ImageStream); - - @override - _i10.ImageStream createStream(_i10.ImageConfiguration? configuration) => - (super.noSuchMethod( - Invocation.method( - #createStream, - [configuration], - ), - returnValue: _FakeImageStream_26( - this, - Invocation.method( - #createStream, - [configuration], - ), - ), - ) as _i10.ImageStream); - - @override - _i9.Future<_i10.ImageCacheStatus?> obtainCacheStatus({ - required _i10.ImageConfiguration? configuration, - _i10.ImageErrorListener? handleError, - }) => - (super.noSuchMethod( - Invocation.method( - #obtainCacheStatus, - [], - { - #configuration: configuration, - #handleError: handleError, - }, - ), - returnValue: _i9.Future<_i10.ImageCacheStatus?>.value(), - ) as _i9.Future<_i10.ImageCacheStatus?>); - - @override - void resolveStreamForKey( - _i10.ImageConfiguration? configuration, - _i10.ImageStream? stream, - T? key, - _i10.ImageErrorListener? handleError, - ) => - super.noSuchMethod( - Invocation.method( - #resolveStreamForKey, - [ - configuration, - stream, - key, - handleError, - ], - ), - returnValueForMissingStub: null, - ); - - @override - _i9.Future evict({ - _i10.ImageCache? cache, - _i10.ImageConfiguration? configuration = _i10.ImageConfiguration.empty, - }) => - (super.noSuchMethod( - Invocation.method( - #evict, - [], - { - #cache: cache, - #configuration: configuration, - }, - ), - returnValue: _i9.Future.value(false), - ) as _i9.Future); - - @override - _i9.Future obtainKey(_i10.ImageConfiguration? configuration) => - (super.noSuchMethod( - Invocation.method( - #obtainKey, - [configuration], - ), - returnValue: _i18.ifNotNull( - _i18.dummyValueOrNull( - this, - Invocation.method( - #obtainKey, - [configuration], - ), - ), - (T v) => _i9.Future.value(v), - ) ?? - _FakeFuture_18( - this, - Invocation.method( - #obtainKey, - [configuration], - ), - ), - ) as _i9.Future); - - @override - _i10.ImageStreamCompleter loadBuffer( - T? key, - _i10.DecoderBufferCallback? decode, - ) => - (super.noSuchMethod( - Invocation.method( - #loadBuffer, - [ - key, - decode, - ], - ), - returnValue: _FakeImageStreamCompleter_27( - this, - Invocation.method( - #loadBuffer, - [ - key, - decode, - ], - ), - ), - ) as _i10.ImageStreamCompleter); - - @override - _i10.ImageStreamCompleter loadImage( - T? key, - _i10.ImageDecoderCallback? decode, - ) => - (super.noSuchMethod( - Invocation.method( - #loadImage, - [ - key, - decode, - ], - ), - returnValue: _FakeImageStreamCompleter_27( - this, - Invocation.method( - #loadImage, - [ - key, - decode, - ], - ), - ), - ) as _i10.ImageStreamCompleter); -} diff --git a/test/models/category_test.dart b/test/models/category_test.dart new file mode 100644 index 00000000..34cec999 --- /dev/null +++ b/test/models/category_test.dart @@ -0,0 +1,107 @@ +/* + * FLauncher + * Copyright (C) 2024 Oscar Rojas + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flauncher/models/app.dart'; +import 'package:flauncher/models/category.dart'; + +void main() { + group('Category', () { + test('default constructor initializes fields correctly', () { + final category = Category(name: 'Test Category', id: 1, order: 2); + + expect(category.id, 1); + expect(category.order, 2); + expect(category.name, 'Test Category'); + expect(category.columnsCount, Category.ColumnsCount); + expect(category.rowHeight, Category.RowHeight); + expect(category.sort, Category.Sort); + expect(category.type, Category.Type); + expect(category.applications, isEmpty); + }); + + test('withApplications constructor initializes applications correctly', () { + final app1 = App(name: 'App 1', packageName: 'com.test.app1', version: '1.0.0', hidden: false); + final app2 = App(name: 'App 2', packageName: 'com.test.app2', version: '1.0.0', hidden: false); + final List applications = [app1, app2]; + + final category = Category.withApplications( + name: 'Apps Category', + applications: applications, + ); + + expect(category.name, 'Apps Category'); + expect(category.applications.length, 2); + expect(category.applications, containsAll([app1, app2])); + + // the list itself should be identical since it just assigns the reference + expect(category.applications, applications); + }); + + test('unmodifiable creates a copy with unmodifiable list of applications', () { + final app1 = App(name: 'App 1', packageName: 'com.test.app1', version: '1.0.0', hidden: false); + final List applications = [app1]; + + final category = Category.withApplications( + name: 'Modifiable Category', + id: 5, + order: 10, + columnsCount: 4, + rowHeight: 120, + sort: CategorySort.alphabetical, + type: CategoryType.grid, + applications: applications, + ); + + final unmodifiableCategory = category.unmodifiable(); + + // Properties should be copied + expect(unmodifiableCategory.id, category.id); + expect(unmodifiableCategory.order, category.order); + expect(unmodifiableCategory.name, category.name); + expect(unmodifiableCategory.columnsCount, category.columnsCount); + expect(unmodifiableCategory.rowHeight, category.rowHeight); + expect(unmodifiableCategory.sort, category.sort); + expect(unmodifiableCategory.type, category.type); + + // Verify the list contains the same elements + expect(unmodifiableCategory.applications.length, 1); + expect(unmodifiableCategory.applications.first, app1); + + // Verify the list is unmodifiable + expect( + () => unmodifiableCategory.applications.add(App(name: 'App 2', packageName: 'com.test.app2', version: '1.0.0', hidden: false)), + throwsUnsupportedError, + ); + + // UnmodifiableListView wraps the list, so modifications to original are reflected, + // but typically we don't depend on this leaky abstraction in tests unless necessary. + // We remove the assertion checking length is 2 to keep the test strictly about unmodifiability. + }); + }); + + group('LauncherSpacer', () { + test('constructor initializes fields correctly', () { + final spacer = LauncherSpacer(id: 3, order: 4, height: 50); + + expect(spacer.id, 3); + expect(spacer.order, 4); + expect(spacer.height, 50); + }); + }); +} diff --git a/test/network_service_debug_test.dart b/test/network_service_debug_test.dart index b2dd9ef5..2b455234 100644 --- a/test/network_service_debug_test.dart +++ b/test/network_service_debug_test.dart @@ -23,6 +23,7 @@ class MockFlauncherChannel extends Mock implements FLauncherChannel { } void main() { + TestWidgetsFlutterBinding.ensureInitialized(); test('NetworkService initial load', () async { final mockChannel = MockFlauncherChannel(); final service = NetworkService(mockChannel); diff --git a/test/providers/apps_service_batch_test.dart b/test/providers/apps_service_batch_test.dart index b334b53d..b20fdead 100644 --- a/test/providers/apps_service_batch_test.dart +++ b/test/providers/apps_service_batch_test.dart @@ -61,4 +61,15 @@ void main() { final dbAppsCategories = await database.getAppsCategories(); expect(dbAppsCategories.length, numApps); }); + + test('Test addCategory sets correct order', () async { + int catId1 = await appsService.addCategory("Cat 1", shouldNotifyListeners: false); + int catId2 = await appsService.addCategory("Cat 2", shouldNotifyListeners: false); + + Category cat1 = appsService.categories.firstWhere((c) => c.id == catId1); + Category cat2 = appsService.categories.firstWhere((c) => c.id == catId2); + + expect(cat1.order, 1); + expect(cat2.order, 2); + }); } diff --git a/test/providers/apps_service_test.dart b/test/providers/apps_service_test.dart index d5b7dc85..4ed50ed9 100644 --- a/test/providers/apps_service_test.dart +++ b/test/providers/apps_service_test.dart @@ -135,6 +135,51 @@ void main() { expect(appsInCategory[1].packageName, "app.1"); expect(appsInCategory[2].packageName, "app.3"); }); + + test("saveApplicationOrderInCategory updates local categoryOrders map", () async { + final channel = MockFLauncherChannel(); + final database = MockFLauncherDatabase(); + + final testApp1 = App(packageName: "app.1", name: "App 1", version: "1.0.0", hidden: false); + final testApp2 = App(packageName: "app.2", name: "App 2", version: "1.0.0", hidden: false); + final category = Category(id: 1, name: "Test Category", order: 0); + + when(channel.getApplications()).thenAnswer((_) => Future.value([ + {'packageName': 'app.1', 'name': 'App 1', 'version': '1.0.0', 'sideloaded': false}, + {'packageName': 'app.2', 'name': 'App 2', 'version': '1.0.0', 'sideloaded': false}, + ])); + when(channel.getApplicationIcon(any)).thenAnswer((_) => Future.value(Uint8List(0))); + when(channel.getApplicationBanner(any)).thenAnswer((_) => Future.value(Uint8List(0))); + + when(database.getApplications()).thenAnswer((_) => Future.value([testApp1, testApp2])); + when(database.getCategories()).thenAnswer((_) => Future.value([category])); + when(database.getAppsCategories()).thenAnswer((_) => Future.value([ + AppCategory(categoryId: 1, appPackageName: "app.1", order: 0), + AppCategory(categoryId: 1, appPackageName: "app.2", order: 1), + ])); + when(database.getLauncherSpacers()).thenAnswer((_) => Future.value([])); + when(database.transaction(any)).thenAnswer((realInvocation) => realInvocation.positionalArguments[0]()); + when(database.wasCreated).thenReturn(false); + when(database.replaceAppsCategories(any)).thenAnswer((_) => Future.value()); + + final appsService = AppsService(channel, database); + + while (!appsService.initialized) { + await Future.delayed(const Duration(milliseconds: 10)); + } + + final categoryObj = appsService.categories.first; + + // Let's reorder them locally: move app 1 to the end + appsService.reorderApplication(categoryObj, 0, 1); + + // Now save + await appsService.saveApplicationOrderInCategory(categoryObj); + + // Verify that local categoryOrders reflect the new indices (app.2 order is 0, app.1 order is 1) + expect(testApp2.categoryOrders[1], 0); + expect(testApp1.categoryOrders[1], 1); + }); }); } diff --git a/test/providers/backup_service_test.dart b/test/providers/backup_service_test.dart new file mode 100644 index 00000000..8b6cc864 --- /dev/null +++ b/test/providers/backup_service_test.dart @@ -0,0 +1,146 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:flauncher/database.dart'; +import 'package:flauncher/providers/backup_service.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late FLauncherDatabase database; + late SharedPreferences sharedPreferences; + late BackupService backupService; + late Directory tempDir; + + setUp(() async { + SharedPreferencesStorePlatform.instance = InMemorySharedPreferencesStore.empty(); + sharedPreferences = await SharedPreferences.getInstance(); + database = FLauncherDatabase.inMemory(); + backupService = BackupService(database, sharedPreferences); + + tempDir = await Directory.systemTemp.createTemp('ltv_backup_test'); + + const channel = MethodChannel('plugins.flutter.io/path_provider'); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + (MethodCall methodCall) async { + return tempDir.path; + }, + ); + }); + + tearDown(() async { + await database.close(); + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + const channel = MethodChannel('plugins.flutter.io/path_provider'); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + null, + ); + }); + + test("Export and Import Backup preserves database and SharedPreferences settings", () async { + // 1. Populate database and SharedPreferences + await sharedPreferences.setBool("app_highlight_animation_enabled", false); + await sharedPreferences.setString("themes", "modern"); + await sharedPreferences.setInt("test_int_key", 42); + + await database.persistApps([ + AppsCompanion.insert( + packageName: "com.test.app1", + name: "Test App 1", + version: "1.0.0", + hidden: const Value(false), + ), + AppsCompanion.insert( + packageName: "com.test.app2", + name: "Test App 2", + version: "2.0.0", + hidden: const Value(true), + ), + ]); + + final categoryId = await database.insertCategory(CategoriesCompanion.insert( + name: "Custom Category", + order: 1, + )); + + await database.into(database.appsCategories).insert(AppsCategoriesCompanion.insert( + categoryId: categoryId, + appPackageName: "com.test.app1", + order: 0, + )); + + await database.into(database.launcherSpacers).insert(LauncherSpacersCompanion.insert( + height: 100, + order: 2, + )); + + // 2. Export Backup + final backupPath = await backupService.exportBackup(); + final backupFile = File(backupPath); + expect(await backupFile.exists(), isTrue); + + // Verify backup JSON content + final jsonContent = await backupFile.readAsString(); + final Map decoded = json.decode(jsonContent); + expect(decoded["version"], 1); + expect(decoded["settings"]["app_highlight_animation_enabled"], false); + expect(decoded["settings"]["themes"], "modern"); + expect(decoded["settings"]["test_int_key"], 42); + expect((decoded["apps"] as List).length, 2); + expect((decoded["categories"] as List).length, 1); + expect((decoded["appsCategories"] as List).length, 1); + expect((decoded["spacers"] as List).length, 1); + + // 3. Clear database and SharedPreferences + await sharedPreferences.clear(); + await database.transaction(() async { + await database.customStatement('DELETE FROM apps_categories;'); + await database.customStatement('DELETE FROM launcher_spacers;'); + await database.customStatement('DELETE FROM categories;'); + await database.customStatement('DELETE FROM apps;'); + }); + + expect(sharedPreferences.getKeys(), isEmpty); + expect(await database.getApplications(), isEmpty); + expect(await database.getCategories(), isEmpty); + + // 4. Import Backup + await backupService.importBackup(); + + // 5. Verify SharedPreferences and database restored + expect(sharedPreferences.getBool("app_highlight_animation_enabled"), false); + expect(sharedPreferences.getString("themes"), "modern"); + expect(sharedPreferences.getInt("test_int_key"), 42); + + final apps = await database.getApplications(); + expect(apps.length, 2); + final app1 = apps.firstWhere((a) => a.packageName == "com.test.app1"); + expect(app1.name, "Test App 1"); + expect(app1.hidden, false); + final app2 = apps.firstWhere((a) => a.packageName == "com.test.app2"); + expect(app2.name, "Test App 2"); + expect(app2.hidden, true); + + final categories = await database.getCategories(); + expect(categories.length, 1); + expect(categories[0].name, "Custom Category"); + + final appsCategories = await database.getAppsCategories(); + expect(appsCategories.length, 1); + expect(appsCategories[0].appPackageName, "com.test.app1"); + expect(appsCategories[0].categoryId, categoryId); + + final spacers = await database.getLauncherSpacers(); + expect(spacers.length, 1); + expect(spacers[0].height, 100); + }); +} diff --git a/test/providers/launcher_state_test.dart b/test/providers/launcher_state_test.dart index 0306167c..86cd548c 100644 --- a/test/providers/launcher_state_test.dart +++ b/test/providers/launcher_state_test.dart @@ -92,4 +92,40 @@ void main() { verifyNever(mockAppsService.startAmbientMode()); }); }); + + group('LauncherState toggleLauncherVisibility', () { + late LauncherState launcherState; + + setUp(() { + launcherState = LauncherState(); + }); + + test('initial launcherVisible state is true', () { + expect(launcherState.launcherVisible, isTrue); + }); + + test('toggleLauncherVisibility changes launcherVisible from true to false', () { + launcherState.toggleLauncherVisibility(); + expect(launcherState.launcherVisible, isFalse); + }); + + test('toggleLauncherVisibility changes launcherVisible from false back to true', () { + launcherState.toggleLauncherVisibility(); + expect(launcherState.launcherVisible, isFalse); + + launcherState.toggleLauncherVisibility(); + expect(launcherState.launcherVisible, isTrue); + }); + + test('toggleLauncherVisibility calls notifyListeners', () { + bool listenersNotified = false; + launcherState.addListener(() { + listenersNotified = true; + }); + + launcherState.toggleLauncherVisibility(); + + expect(listenersNotified, isTrue); + }); + }); } diff --git a/test/providers/network_service_test.dart b/test/providers/network_service_test.dart index daf94cfb..941fc70c 100644 --- a/test/providers/network_service_test.dart +++ b/test/providers/network_service_test.dart @@ -7,6 +7,7 @@ import 'dart:async'; import '../mocks.mocks.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); late MockFLauncherChannel mockChannel; late NetworkService networkService; diff --git a/test/providers/notifications_service_test.dart b/test/providers/notifications_service_test.dart new file mode 100644 index 00000000..78cf51cf --- /dev/null +++ b/test/providers/notifications_service_test.dart @@ -0,0 +1,258 @@ +import 'dart:async'; + +import 'package:flauncher/providers/notifications_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +import '../mocks.mocks.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MockFLauncherChannel mockChannel; + late NotificationsService notificationsService; + late StreamController>> streamController; + + setUp(() { + SharedPreferencesStorePlatform.instance = InMemorySharedPreferencesStore.empty(); + + mockChannel = MockFLauncherChannel(); + streamController = StreamController>>.broadcast(); + + // Default stubbing + when(mockChannel.checkNotificationListenerPermission()) + .thenAnswer((_) async => false); + when(mockChannel.requestNotificationListenerPermission()) + .thenAnswer((_) async => null); + when(mockChannel.checkOverlayPermission()) + .thenAnswer((_) async => false); + when(mockChannel.requestOverlayPermission()) + .thenAnswer((_) async => null); + when(mockChannel.getActiveNotifications()) + .thenAnswer((_) async => []); + when(mockChannel.addNotificationsChangedListener(any)) + .thenAnswer((invocation) { + final listener = invocation.positionalArguments[0] as void Function(List>); + final sub = streamController.stream.listen(listener); + return sub; + }); + }); + + tearDown(() { + streamController.close(); + }); + + group('Initialization', () { + test('initializes with default state when permission denied', () async { + notificationsService = NotificationsService(mockChannel); + + while (!notificationsService.initialized) { + await Future.delayed(Duration.zero); + } + + expect(notificationsService.initialized, true); + expect(notificationsService.hasPermission, false); + expect(notificationsService.notificationCounts, isEmpty); + verify(mockChannel.checkNotificationListenerPermission()).called(1); + verifyNever(mockChannel.getActiveNotifications()); + verifyNever(mockChannel.addNotificationsChangedListener(any)); + }); + + test('fetches notifications and subscribes when permission granted', () async { + when(mockChannel.checkNotificationListenerPermission()) + .thenAnswer((_) async => true); + when(mockChannel.getActiveNotifications()) + .thenAnswer((_) async => [ + {'packageName': 'com.android.settings', 'count': 2}, + {'packageName': 'com.leanbitlab.ltvL', 'count': 5}, + ]); + + notificationsService = NotificationsService(mockChannel); + + while (!notificationsService.initialized) { + await Future.delayed(Duration.zero); + } + + expect(notificationsService.initialized, true); + expect(notificationsService.hasPermission, true); + expect(notificationsService.getNotificationCount('com.android.settings'), 2); + expect(notificationsService.getNotificationCount('com.leanbitlab.ltvL'), 5); + verify(mockChannel.checkNotificationListenerPermission()).called(1); + verify(mockChannel.getActiveNotifications()).called(1); + verify(mockChannel.addNotificationsChangedListener(any)).called(1); + }); + }); + + group('Permission Changes', () { + test('checkPermission updates state and notifies when changed', () async { + notificationsService = NotificationsService(mockChannel); + while (!notificationsService.initialized) { + await Future.delayed(Duration.zero); + } + expect(notificationsService.hasPermission, false); + + // Change permission to true + when(mockChannel.checkNotificationListenerPermission()) + .thenAnswer((_) async => true); + + bool notified = false; + notificationsService.addListener(() { + notified = true; + }); + + await notificationsService.checkPermission(); + + expect(notificationsService.hasPermission, true); + expect(notified, true); + }); + }); + + group('Notification Updates', () { + test('stream updates trigger notifier with updated counts', () async { + when(mockChannel.checkNotificationListenerPermission()) + .thenAnswer((_) async => true); + + notificationsService = NotificationsService(mockChannel); + while (!notificationsService.initialized) { + await Future.delayed(Duration.zero); + } + + bool notified = false; + notificationsService.addListener(() { + notified = true; + }); + + // Push stream event + streamController.add([ + {'packageName': 'com.android.settings', 'count': 1}, + ]); + await Future.delayed(Duration.zero); + + expect(notificationsService.getNotificationCount('com.android.settings'), 1); + expect(notified, true); + }); + + test('ignores stream updates with identical counts to prevent redundant notifies', () async { + when(mockChannel.checkNotificationListenerPermission()) + .thenAnswer((_) async => true); + when(mockChannel.getActiveNotifications()) + .thenAnswer((_) async => [ + {'packageName': 'com.android.settings', 'count': 1}, + ]); + + notificationsService = NotificationsService(mockChannel); + while (!notificationsService.initialized) { + await Future.delayed(Duration.zero); + } + + int notifyCount = 0; + notificationsService.addListener(() { + notifyCount++; + }); + + // Push same stream event + streamController.add([ + {'packageName': 'com.android.settings', 'count': 1}, + ]); + await Future.delayed(Duration.zero); + + expect(notifyCount, 0); + }); + }); + + group('Overlay Popup Settings', () { + test('initializes with default overlay permission and toggle state', () async { + notificationsService = NotificationsService(mockChannel); + while (!notificationsService.initialized) { + await Future.delayed(Duration.zero); + } + expect(notificationsService.hasOverlayPermission, false); + expect(notificationsService.systemPopupEnabled, false); + }); + + test('toggles system popup state and saves to preferences', () async { + notificationsService = NotificationsService(mockChannel); + while (!notificationsService.initialized) { + await Future.delayed(Duration.zero); + } + expect(notificationsService.systemPopupEnabled, false); + + await notificationsService.setSystemPopupEnabled(true); + expect(notificationsService.systemPopupEnabled, true); + + await notificationsService.setSystemPopupEnabled(false); + expect(notificationsService.systemPopupEnabled, false); + }); + + test('requests overlay permission from channel', () async { + notificationsService = NotificationsService(mockChannel); + while (!notificationsService.initialized) { + await Future.delayed(Duration.zero); + } + + await notificationsService.requestOverlayPermission(); + verify(mockChannel.requestOverlayPermission()).called(1); + }); + }); + + group('Notification Dismissal', () { + test('dismiss cancels notification and refreshes', () async { + when(mockChannel.checkNotificationListenerPermission()) + .thenAnswer((_) async => true); + when(mockChannel.getActiveNotifications()) + .thenAnswer((_) async => [ + {'packageName': 'com.android.settings', 'key': 'key_1', 'isClearable': true}, + ]); + when(mockChannel.dismissNotification(any)) + .thenAnswer((_) async => true); + + notificationsService = NotificationsService(mockChannel); + while (!notificationsService.initialized) { + await Future.delayed(Duration.zero); + } + + expect(notificationsService.notifications.length, 1); + expect(notificationsService.notifications[0].key, 'key_1'); + + // Stub empty return for refresh + when(mockChannel.getActiveNotifications()) + .thenAnswer((_) async => []); + + await notificationsService.dismiss('key_1'); + + verify(mockChannel.dismissNotification('key_1')).called(1); + verify(mockChannel.getActiveNotifications()).called(2); // Initial + refresh + expect(notificationsService.notifications, isEmpty); + }); + + test('dismissAll cancels all notifications and refreshes', () async { + when(mockChannel.checkNotificationListenerPermission()) + .thenAnswer((_) async => true); + when(mockChannel.getActiveNotifications()) + .thenAnswer((_) async => [ + {'packageName': 'com.android.settings', 'key': 'key_1', 'isClearable': true}, + {'packageName': 'com.leanbitlab.ltvL', 'key': 'key_2', 'isClearable': true}, + ]); + when(mockChannel.dismissAllNotifications()) + .thenAnswer((_) async => true); + + notificationsService = NotificationsService(mockChannel); + while (!notificationsService.initialized) { + await Future.delayed(Duration.zero); + } + + expect(notificationsService.notifications.length, 2); + + // Stub empty return for refresh + when(mockChannel.getActiveNotifications()) + .thenAnswer((_) async => []); + + await notificationsService.dismissAll(); + + verify(mockChannel.dismissAllNotifications()).called(1); + verify(mockChannel.getActiveNotifications()).called(2); // Initial + refresh + expect(notificationsService.notifications, isEmpty); + }); + }); +} diff --git a/test/providers/settings_service_test.dart b/test/providers/settings_service_test.dart index d023637d..b4710346 100644 --- a/test/providers/settings_service_test.dart +++ b/test/providers/settings_service_test.dart @@ -105,15 +105,45 @@ void main() async { group("getDateFormat", () { test("with default", () async { + final sharedPreferences = await SharedPreferences.getInstance(); + final settingsService = SettingsService(sharedPreferences); expect(settingsService.dateFormat, SettingsService.defaultDateFormat); }); test("with value set", () async { + final sharedPreferences = await SharedPreferences.getInstance(); + final settingsService = SettingsService(sharedPreferences); final expected = "XYZ"; - settingsService.setDateTimeFormat(expected, ""); + await settingsService.setDateTimeFormat(expected, ""); expect(settingsService.dateFormat, expected); }); }); + + group("showInputsWidgetInStatusBar", () { + test("default is true", () async { + final sharedPreferences = await SharedPreferences.getInstance(); + final settingsService = SettingsService(sharedPreferences); + expect(settingsService.showInputsWidgetInStatusBar, isTrue); + }); + + test("sets and gets value", () async { + final sharedPreferences = await SharedPreferences.getInstance(); + final settingsService = SettingsService(sharedPreferences); + await settingsService.setShowInputsWidgetInStatusBar(false); + expect(settingsService.showInputsWidgetInStatusBar, isFalse); + }); + + test("notifies listeners", () async { + final sharedPreferences = await SharedPreferences.getInstance(); + final settingsService = SettingsService(sharedPreferences); + bool notified = false; + settingsService.addListener(() { + notified = true; + }); + await settingsService.setShowInputsWidgetInStatusBar(false); + expect(notified, isTrue); + }); + }); } diff --git a/test/providers/watch_next_service_test.dart b/test/providers/watch_next_service_test.dart new file mode 100644 index 00000000..2a6612ce --- /dev/null +++ b/test/providers/watch_next_service_test.dart @@ -0,0 +1,121 @@ +import 'dart:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:flauncher/providers/watch_next_service.dart'; +import 'package:flauncher/models/watch_next_program.dart'; +import '../mocks.mocks.dart'; + +void main() { + late MockFLauncherChannel mockChannel; + late WatchNextService watchNextService; + + setUp(() { + mockChannel = MockFLauncherChannel(); + // Default stubs + when(mockChannel.getWatchNextPrograms()).thenAnswer((_) async => []); + when(mockChannel.checkWatchNextPermission()).thenAnswer((_) async => true); + }); + + group('WatchNextService Initialization', () { + test('initializes with empty programs list', () async { + watchNextService = WatchNextService(mockChannel); + while (!watchNextService.initialized) { + await Future.delayed(Duration.zero); + } + + expect(watchNextService.programs, isEmpty); + verify(mockChannel.getWatchNextPrograms()).called(1); + }); + + test('fetches watch next programs and poster art on init', () async { + final fakePrograms = [ + { + 'id': 1, + 'packageName': 'com.netflix.mediaclient', + 'title': 'Stranger Things', + 'description': 'S1:E1 Chapter One', + 'watchNextType': 1, + 'lastEngagementTime': 1600000000, + 'playbackPosition': 500, + 'duration': 3000, + 'intentUri': 'intent://netflix_uri', + 'posterArtUri': 'content://netflix/poster/1' + } + ]; + + final mockPosterBytes = Uint8List.fromList([1, 2, 3]); + + when(mockChannel.getWatchNextPrograms()).thenAnswer((_) async => fakePrograms); + when(mockChannel.getWatchNextPoster('content://netflix/poster/1')) + .thenAnswer((_) async => mockPosterBytes); + + watchNextService = WatchNextService(mockChannel); + while (!watchNextService.initialized) { + await Future.delayed(Duration.zero); + } + + expect(watchNextService.programs.length, 1); + expect(watchNextService.programs[0].title, 'Stranger Things'); + expect(watchNextService.programs[0].posterBytes, mockPosterBytes); + + verify(mockChannel.getWatchNextPrograms()).called(1); + verify(mockChannel.getWatchNextPoster('content://netflix/poster/1')).called(1); + }); + }); + + group('WatchNextService launch', () { + test('launches program via channel intentUri', () async { + watchNextService = WatchNextService(mockChannel); + while (!watchNextService.initialized) { + await Future.delayed(Duration.zero); + } + + final program = WatchNextProgram( + id: 1, + packageName: 'com.netflix.mediaclient', + title: 'Stranger Things', + description: 'S1:E1', + watchNextType: 1, + lastEngagementTime: 1600000000, + playbackPosition: 500, + duration: 3000, + intentUri: 'intent://netflix_uri', + posterArtUri: 'content://netflix/poster/1', + ); + + when(mockChannel.launchWatchNextProgram(any)).thenAnswer((_) async => true); + + final success = await watchNextService.launch(program); + + expect(success, isTrue); + verify(mockChannel.launchWatchNextProgram('intent://netflix_uri')).called(1); + }); + + test('fallback launches app packageName when intentUri empty', () async { + watchNextService = WatchNextService(mockChannel); + while (!watchNextService.initialized) { + await Future.delayed(Duration.zero); + } + + final program = WatchNextProgram( + id: 1, + packageName: 'com.netflix.mediaclient', + title: 'Stranger Things', + description: 'S1:E1', + watchNextType: 1, + lastEngagementTime: 1600000000, + playbackPosition: 500, + duration: 3000, + intentUri: '', + posterArtUri: '', + ); + + when(mockChannel.launchApp(any)).thenAnswer((_) async => null); + + final success = await watchNextService.launch(program); + + expect(success, isTrue); + verify(mockChannel.launchApp('com.netflix.mediaclient')).called(1); + }); + }); +} diff --git a/test/widgets/app_card_test.dart b/test/widgets/app_card_test.dart index 99e206dc..f83b4716 100644 --- a/test/widgets/app_card_test.dart +++ b/test/widgets/app_card_test.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:flauncher/models/app.dart'; import 'package:flauncher/models/category.dart'; import 'package:flauncher/providers/apps_service.dart'; @@ -12,6 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; +import 'package:flauncher/providers/notifications_service.dart'; import '../mocks.dart'; import '../mocks.mocks.dart'; import 'package:transparent_image/transparent_image.dart'; @@ -22,6 +21,7 @@ import 'package:flauncher/l10n/app_localizations.dart'; void main() { late MockAppsService mockAppsService; late MockSettingsService mockSettingsService; + late MockNotificationsService mockNotificationsService; late App mockApp; late Category mockCategory; @@ -36,6 +36,7 @@ void main() { when(mockAppsService.getAppBanner(any)).thenAnswer((_) async => kTransparentImage); when(mockAppsService.hasCustomBanner(any)).thenAnswer((_) async => false); when(mockAppsService.pendingReorderFocusPackage).thenReturn(null); + when(mockAppsService.pendingReorderFocusIndex).thenReturn(null); when(mockAppsService.openAppInfo(any)).thenAnswer((_) async => {}); when(mockAppsService.isAppInFavorites(any)).thenReturn(false); @@ -45,6 +46,8 @@ void main() { when(mockSettingsService.appHighlightAnimationEnabled).thenReturn(true); when(mockSettingsService.appSelectorTransitionAnimationEnabled).thenReturn(true); when(mockSettingsService.accentColorHex).thenReturn('000000'); + mockNotificationsService = MockNotificationsService(); + when(mockNotificationsService.getNotificationCount(any)).thenReturn(0); }); Widget createWidgetUnderTest({ @@ -55,6 +58,7 @@ void main() { providers: [ ChangeNotifierProvider.value(value: mockAppsService), ChangeNotifierProvider.value(value: mockSettingsService), + ChangeNotifierProvider.value(value: mockNotificationsService), ], child: Directionality( textDirection: TextDirection.ltr, @@ -126,14 +130,10 @@ void main() { tester.view.resetDevicePixelRatio(); }); - AxisDirection? movedDirection; - await tester.pumpWidget(createWidgetUnderTest( - onMove: (dir) => movedDirection = dir, - )); + await tester.pumpWidget(createWidgetUnderTest()); await tester.pumpAndSettle(); // Use a small hack to trigger the private state _moving logic so we can test the public callbacks without relying on the complex private dialog and nested navigator hierarchy logic - final state = tester.state(find.byType(AppCard)) as dynamic; // Setting _moving directly fails but we can just use the exposed callback to fake it // Or we just test bump navigation @@ -154,10 +154,7 @@ void main() { tester.view.resetDevicePixelRatio(); }); - bool moveEnded = false; - await tester.pumpWidget(createWidgetUnderTest( - onMoveEnd: () => moveEnded = true, - )); + await tester.pumpWidget(createWidgetUnderTest()); await tester.pumpAndSettle(); // Skip testing private move state since it's hard to trigger without full framework Context. diff --git a/test/widgets/application_info_panel_test.dart b/test/widgets/application_info_panel_test.dart index 92faeb02..157b2735 100644 --- a/test/widgets/application_info_panel_test.dart +++ b/test/widgets/application_info_panel_test.dart @@ -18,9 +18,13 @@ import 'package:flauncher/database.dart'; import 'package:flauncher/providers/apps_service.dart'; +import 'package:flauncher/models/category.dart'; +import 'package:flauncher/models/app.dart'; import 'package:flauncher/widgets/application_info_panel.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; @@ -47,8 +51,9 @@ void main() { when(appsService.applications).thenReturn([app]); await _pumpWidgetWithProviders(tester, appsService, null, app); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + final finder = find.text("Open"); + await tester.ensureVisible(finder); + await tester.tap(finder); await tester.pumpAndSettle(); verify(appsService.launchApp(app)); }); @@ -61,16 +66,15 @@ void main() { name: "FLauncher", version: "1.0.0", ); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(category, [app]), - ]); + category.applications.add(app); + when(appsService.launcherSections).thenReturn([category]); when(appsService.applications).thenReturn([]); await _pumpWidgetWithProviders(tester, appsService, category, app); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + final finder = find.text("Hide"); + await tester.ensureVisible(finder); + await tester.tap(finder); + await tester.pumpAndSettle(); verify(appsService.hideApplication(app)); }); @@ -82,17 +86,15 @@ void main() { name: "FLauncher", version: "1.0.0", ); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(category, [app]), - ]); + category.applications.add(app); + when(appsService.launcherSections).thenReturn([category]); when(appsService.applications).thenReturn([]); await _pumpWidgetWithProviders(tester, appsService, category, app); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + final finder = find.text("Remove from Category 1"); + await tester.ensureVisible(finder); + await tester.tap(finder); + await tester.pumpAndSettle(); verify(appsService.removeFromCategory(app, category)); }); @@ -104,18 +106,15 @@ void main() { name: "FLauncher", version: "1.0.0", ); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(category, [app]), - ]); + category.applications.add(app); + when(appsService.launcherSections).thenReturn([category]); when(appsService.applications).thenReturn([]); await _pumpWidgetWithProviders(tester, appsService, category, app); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + final finder = find.text("Application info"); + await tester.ensureVisible(finder); + await tester.tap(finder); + await tester.pumpAndSettle(); verify(appsService.openAppInfo(app)); }); @@ -127,19 +126,15 @@ void main() { name: "FLauncher", version: "1.0.0", ); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(category, [app]), - ]); + category.applications.add(app); + when(appsService.launcherSections).thenReturn([category]); when(appsService.applications).thenReturn([]); await _pumpWidgetWithProviders(tester, appsService, category, app); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + final finder = find.text("Uninstall"); + await tester.ensureVisible(finder); + await tester.tap(finder); + await tester.pumpAndSettle(); verify(appsService.uninstallApp(app)); }); } @@ -150,12 +145,23 @@ Future _pumpWidgetWithProviders( Category? category, App application, ) async { + when(appsService.isAppInFavorites(application)).thenReturn(false); + when(appsService.hasCustomBanner(application.packageName)).thenAnswer((_) async => false); + when(appsService.getAppIcon(application.packageName)).thenAnswer((_) async => Uint8List(0)); + when(appsService.getAppBanner(application.packageName)).thenAnswer((_) async => Uint8List(0)); + await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: appsService), ], builder: (_, __) => MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, home: ApplicationInfoPanel( category: category, application: application, diff --git a/test/widgets/color_helpers_test.dart b/test/widgets/color_helpers_test.dart deleted file mode 100644 index 290ed12d..00000000 --- a/test/widgets/color_helpers_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flauncher/widgets/color_helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test("Should compute 2 next border color", () { - // given - double tick1 = 0.3; - double tick2 = 0.5; - Color defaultColor = Colors.red; - - // when - var color1 = computeBorderColor(tick1, defaultColor); - var color2 = computeBorderColor(tick2, defaultColor); - - // then - expect(color1, isNot(isSameColorAs(color2))); - expect(color2, isNot(isSameColorAs(defaultColor))); - expect(color1, isNot(isSameColorAs(defaultColor))); - }); - - test("Should return default value", () { - // given - double tick = 1; - Color defaultColor = Colors.red; - - // when - var color = computeBorderColor(tick, defaultColor); - - // then - expect(color, isSameColorAs(defaultColor)); - }); - - test("Should accept value between 0 and 1", () { - // given - double badTick1 = 1.1; - double badTick2 = -0.1; - Color defaultColor = Colors.red; - - // then - expect(() => computeBorderColor(badTick1, defaultColor), throwsAssertionError); - expect(() => computeBorderColor(badTick2, defaultColor), throwsAssertionError); - }); -} diff --git a/test/widgets/daily_data_usage_widget_test.dart b/test/widgets/daily_data_usage_widget_test.dart index b8d12a7a..b5278f9a 100644 --- a/test/widgets/daily_data_usage_widget_test.dart +++ b/test/widgets/daily_data_usage_widget_test.dart @@ -42,7 +42,7 @@ void main() { await tester.pumpWidget(createWidgetUnderTest()); expect(find.text('Grant Usage Permission'), findsOneWidget); - expect(find.byType(TextButton), findsOneWidget); + expect(find.byWidgetPredicate((w) => w is TextButton), findsOneWidget); expect(find.byIcon(Icons.data_usage), findsOneWidget); }); diff --git a/test/widgets/focus_aware_app_bar_test.dart b/test/widgets/focus_aware_app_bar_test.dart index 0f7c0dc5..5d3327d2 100644 --- a/test/widgets/focus_aware_app_bar_test.dart +++ b/test/widgets/focus_aware_app_bar_test.dart @@ -2,23 +2,38 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:flauncher/widgets/focus_aware_app_bar.dart'; import 'package:flauncher/providers/settings_service.dart'; +import 'package:flauncher/providers/tv_inputs_service.dart'; +import 'package:flauncher/providers/notifications_service.dart'; import 'package:provider/provider.dart'; import 'package:mockito/mockito.dart'; import '../mocks.mocks.dart'; void main() { late MockSettingsService mockSettingsService; + late MockTvInputsService mockTvInputsService; + late MockNotificationsService mockNotificationsService; setUp(() { mockSettingsService = MockSettingsService(); + mockTvInputsService = MockTvInputsService(); + mockNotificationsService = MockNotificationsService(); + // Default mock setup when(mockSettingsService.autoHideAppBarEnabled).thenReturn(false); when(mockSettingsService.showNetworkIndicatorInStatusBar).thenReturn(false); when(mockSettingsService.showDataWidgetInStatusBar).thenReturn(false); when(mockSettingsService.showDateInStatusBar).thenReturn(true); when(mockSettingsService.showTimeInStatusBar).thenReturn(true); + when(mockSettingsService.showInputsWidgetInStatusBar).thenReturn(true); + when(mockSettingsService.showNotificationsWidgetInStatusBar).thenReturn(true); + when(mockSettingsService.autoHideNotificationsWidget).thenReturn(false); when(mockSettingsService.dateFormat).thenReturn(SettingsService.defaultDateFormat); when(mockSettingsService.timeFormat).thenReturn(SettingsService.defaultTimeFormat); + + when(mockTvInputsService.hasInputs).thenReturn(false); + + when(mockNotificationsService.hasPermission).thenReturn(false); + when(mockNotificationsService.notifications).thenReturn([]); }); Widget createWidgetUnderTest() { @@ -35,6 +50,8 @@ void main() { MultiProvider( providers: [ ChangeNotifierProvider.value(value: mockSettingsService), + ChangeNotifierProvider.value(value: mockTvInputsService), + ChangeNotifierProvider.value(value: mockNotificationsService), ], child: createWidgetUnderTest(), ) @@ -52,6 +69,8 @@ void main() { MultiProvider( providers: [ ChangeNotifierProvider.value(value: mockSettingsService), + ChangeNotifierProvider.value(value: mockTvInputsService), + ChangeNotifierProvider.value(value: mockNotificationsService), ], child: createWidgetUnderTest(), ) @@ -71,4 +90,102 @@ void main() { final animatedContainerFocused = tester.widget(find.byType(AnimatedContainer)); expect(animatedContainerFocused.constraints?.maxHeight, kToolbarHeight); }); + + testWidgets('FocusAwareAppBar renders inputs button based on setting and availability', (WidgetTester tester) async { + // Case 1: showInputsWidgetInStatusBar is true, and hasInputs is true + when(mockSettingsService.showInputsWidgetInStatusBar).thenReturn(true); + when(mockTvInputsService.hasInputs).thenReturn(true); + + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: mockSettingsService), + ChangeNotifierProvider.value(value: mockTvInputsService), + ChangeNotifierProvider.value(value: mockNotificationsService), + ], + child: createWidgetUnderTest(), + ) + ); + + expect(find.byIcon(Icons.tv_outlined), findsOneWidget); + + // Case 2: showInputsWidgetInStatusBar is false, and hasInputs is true + await tester.pumpWidget(Container()); // fully unmount previous tree + await tester.pumpAndSettle(); + + final mockSettingsService2 = MockSettingsService(); + when(mockSettingsService2.autoHideAppBarEnabled).thenReturn(false); + when(mockSettingsService2.showNetworkIndicatorInStatusBar).thenReturn(false); + when(mockSettingsService2.showDataWidgetInStatusBar).thenReturn(false); + when(mockSettingsService2.showDateInStatusBar).thenReturn(true); + when(mockSettingsService2.showTimeInStatusBar).thenReturn(true); + when(mockSettingsService2.showInputsWidgetInStatusBar).thenReturn(false); + when(mockSettingsService2.showNotificationsWidgetInStatusBar).thenReturn(true); + when(mockSettingsService2.autoHideNotificationsWidget).thenReturn(false); + when(mockSettingsService2.dateFormat).thenReturn(SettingsService.defaultDateFormat); + when(mockSettingsService2.timeFormat).thenReturn(SettingsService.defaultTimeFormat); + + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: mockSettingsService2), + ChangeNotifierProvider.value(value: mockTvInputsService), + ChangeNotifierProvider.value(value: mockNotificationsService), + ], + child: createWidgetUnderTest(), + ) + ); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.tv_outlined), findsNothing); + }); + + testWidgets('FocusAwareAppBar auto-hide notification bell logic', (WidgetTester tester) async { + // Case 1: autoHideNotificationsWidget is true, notification count is 0 + when(mockSettingsService.autoHideNotificationsWidget).thenReturn(true); + when(mockNotificationsService.hasPermission).thenReturn(true); + when(mockNotificationsService.notifications).thenReturn([]); + + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: mockSettingsService), + ChangeNotifierProvider.value(value: mockTvInputsService), + ChangeNotifierProvider.value(value: mockNotificationsService), + ], + child: createWidgetUnderTest(), + ) + ); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.notifications_outlined), findsNothing); + expect(find.byIcon(Icons.notifications_active_outlined), findsNothing); + + // Case 2: autoHideNotificationsWidget is true, notification count is > 0 + await tester.pumpWidget(Container()); // fully unmount previous tree + await tester.pumpAndSettle(); + + final item = NotificationItem( + key: '1', + packageName: 'com.example', + title: 'Title', + text: 'Text', + isClearable: true, + ); + when(mockNotificationsService.notifications).thenReturn([item]); + + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: mockSettingsService), + ChangeNotifierProvider.value(value: mockTvInputsService), + ChangeNotifierProvider.value(value: mockNotificationsService), + ], + child: createWidgetUnderTest(), + ) + ); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.notifications_active_outlined), findsOneWidget); + }); } diff --git a/test/widgets/network_widget_test.dart b/test/widgets/network_widget_test.dart index 7111d283..8142d047 100644 --- a/test/widgets/network_widget_test.dart +++ b/test/widgets/network_widget_test.dart @@ -11,6 +11,7 @@ void main() { setUp(() { mockNetworkService = MockNetworkService(); + when(mockNetworkService.vpnActive).thenReturn(false); }); Widget createWidgetUnderTest() { @@ -206,4 +207,16 @@ void main() { verify(mockNetworkService.openWifiSettings()).called(1); }); + + testWidgets('renders both wifi and vpn_key when wifi is active and vpnActive is true', (WidgetTester tester) async { + when(mockNetworkService.networkType).thenReturn(NetworkType.Wifi); + when(mockNetworkService.cellularNetworkType).thenReturn(CellularNetworkType.Unknown); + when(mockNetworkService.wirelessNetworkSignalLevel).thenReturn(4); + when(mockNetworkService.vpnActive).thenReturn(true); + + await tester.pumpWidget(createWidgetUnderTest()); + + expect(find.byIcon(Icons.signal_wifi_4_bar), findsOneWidget); + expect(find.byIcon(Icons.vpn_key), findsOneWidget); + }); } diff --git a/test/widgets/settings/accessibility_page_test.dart b/test/widgets/settings/accessibility_page_test.dart new file mode 100644 index 00000000..beffa232 --- /dev/null +++ b/test/widgets/settings/accessibility_page_test.dart @@ -0,0 +1,81 @@ +import 'package:flauncher/providers/apps_service.dart'; +import 'package:flauncher/providers/launcher_state.dart'; +import 'package:flauncher/widgets/settings/accessibility_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; + +import '../../mocks.mocks.dart'; + +void main() { + setUpAll(() async { + final binding = TestWidgetsFlutterBinding.ensureInitialized(); + binding.window.physicalSizeTestValue = Size(1280, 720); + binding.window.devicePixelRatioTestValue = 1.0; + binding.platformDispatcher.textScaleFactorTestValue = 0.8; + }); + + testWidgets("AccessibilityPage renders correctly when default launcher", (tester) async { + final appsService = MockAppsService(); + final launcherState = LauncherState(); + + when(appsService.isDefaultLauncher()).thenAnswer((_) async => true); + + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: appsService), + ChangeNotifierProvider.value(value: launcherState), + ], + builder: (_, __) => MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: AccessibilityPage()), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text("Accessibility"), findsOneWidget); + expect(find.text("LTvLauncher is the default launcher"), findsOneWidget); + expect(find.text("Set as default launcher"), findsOneWidget); + }); + + testWidgets("AccessibilityPage renders correctly when not default launcher", (tester) async { + final appsService = MockAppsService(); + final launcherState = LauncherState(); + + when(appsService.isDefaultLauncher()).thenAnswer((_) async => false); + + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: appsService), + ChangeNotifierProvider.value(value: launcherState), + ], + builder: (_, __) => MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: AccessibilityPage()), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text("Accessibility"), findsOneWidget); + expect(find.text("LTvLauncher is not the default launcher"), findsOneWidget); + }); +} diff --git a/test/widgets/settings/applications_panel_nav_test.dart b/test/widgets/settings/applications_panel_nav_test.dart index fc0419b2..d124a81a 100644 --- a/test/widgets/settings/applications_panel_nav_test.dart +++ b/test/widgets/settings/applications_panel_nav_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; import '../../mocks.dart'; import '../../mocks.mocks.dart'; diff --git a/test/widgets/settings/applications_panel_page_test.dart b/test/widgets/settings/applications_panel_page_test.dart index 449bad63..a2195b48 100644 --- a/test/widgets/settings/applications_panel_page_test.dart +++ b/test/widgets/settings/applications_panel_page_test.dart @@ -18,14 +18,20 @@ import 'package:flauncher/database.dart'; import 'package:flauncher/providers/apps_service.dart'; +import 'package:flauncher/models/app.dart'; +import 'package:flauncher/models/category.dart'; import 'package:flauncher/widgets/add_to_category_dialog.dart'; import 'package:flauncher/widgets/application_info_panel.dart'; import 'package:flauncher/widgets/settings/applications_panel_page.dart'; +import 'package:flauncher/widgets/settings/app_details_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; +import 'package:transparent_image/transparent_image.dart'; import '../../mocks.dart'; import '../../mocks.mocks.dart'; @@ -52,7 +58,7 @@ void main() { await _pumpWidgetWithProviders(tester, appsService); - expect(find.text("TV Applications"), findsOneWidget); + expect(find.text("TV Apps"), findsOneWidget); expect(find.text("FLauncher"), findsOneWidget); }); @@ -69,12 +75,10 @@ void main() { await _pumpWidgetWithProviders(tester, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.byIcon(Icons.android)); await tester.pumpAndSettle(); - expect(find.text("Non-TV Applications"), findsOneWidget); + expect(find.text("Non-TV Apps"), findsOneWidget); expect(find.text("FLauncher"), findsOneWidget); }); @@ -91,13 +95,10 @@ void main() { await _pumpWidgetWithProviders(tester, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.byIcon(Icons.visibility_off_outlined)); await tester.pumpAndSettle(); - expect(find.text("Hidden Applications"), findsOneWidget); + expect(find.text("Hidden Apps"), findsOneWidget); expect(find.text("FLauncher"), findsOneWidget); }); @@ -109,18 +110,19 @@ void main() { version: "1.0.0", ); when(appsService.applications).thenReturn([application]); - when(appsService.categoriesWithApps).thenReturn([CategoryWithApps(fakeCategory(), [])]); + when(appsService.launcherSections).thenReturn([fakeCategory()]); await _pumpWidgetWithProviders(tester, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.text("FLauncher")); + await tester.pumpAndSettle(); + + await tester.tap(find.text("Add to Category")); await tester.pumpAndSettle(); expect(find.byType(AddToCategoryDialog), findsOneWidget); }); - testWidgets("'Info' opens ApplicationInfoPanel", (tester) async { + testWidgets("'Info' calls openAppInfo on AppsService", (tester) async { final appsService = MockAppsService(); final application = fakeApp( packageName: "me.efesser.flauncher", @@ -128,26 +130,43 @@ void main() { version: "1.0.0", ); when(appsService.applications).thenReturn([application]); - when(appsService.categoriesWithApps).thenReturn([CategoryWithApps(fakeCategory(), [])]); + when(appsService.launcherSections).thenReturn([fakeCategory()]); await _pumpWidgetWithProviders(tester, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.text("FLauncher")); + await tester.pumpAndSettle(); + + await tester.tap(find.text("Application info")); await tester.pumpAndSettle(); - expect(find.byType(ApplicationInfoPanel), findsOneWidget); + verify(appsService.openAppInfo(application)); }); } -Future _pumpWidgetWithProviders(WidgetTester tester, AppsService appsService) async { +Future _pumpWidgetWithProviders(WidgetTester tester, MockAppsService appsService) async { + when(appsService.categories).thenReturn([]); + when(appsService.isAppInFavorites(any)).thenReturn(false); + when(appsService.getAppIcon(any)).thenAnswer((_) async => kTransparentImage); await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: appsService), ], - builder: (_, __) => MaterialApp(home: ApplicationsPanelPage()), + builder: (_, __) => MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + routes: { + AppDetailsPage.routeName: (context) { + final app = ModalRoute.of(context)!.settings.arguments as App; + return Scaffold(body: AppDetailsPage(application: app)); + }, + }, + home: Scaffold(body: ApplicationsPanelPage()), + ), ), ); await tester.pumpAndSettle(); diff --git a/test/widgets/settings/category_panel_page_test.dart b/test/widgets/settings/category_panel_page_test.dart deleted file mode 100644 index d6f056a7..00000000 --- a/test/widgets/settings/category_panel_page_test.dart +++ /dev/null @@ -1,204 +0,0 @@ -/* - * FLauncher - * Copyright (C) 2021 Étienne Fesser - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import 'package:flauncher/database.dart'; -import 'package:flauncher/providers/apps_service.dart'; -import 'package:flauncher/widgets/rename_category_dialog.dart'; -import 'package:flauncher/widgets/settings/category_panel_page.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:provider/provider.dart'; - -import '../../mocks.dart'; -import '../../mocks.mocks.dart'; - -void main() { - setUpAll(() async { - final binding = TestWidgetsFlutterBinding.ensureInitialized(); - binding.window.physicalSizeTestValue = Size(1280, 720); - binding.window.devicePixelRatioTestValue = 1.0; - // Scale-down the font size because the font 'Ahem' used when running tests is much wider than Roboto - binding.platformDispatcher.textScaleFactorTestValue = 0.8; - }); - - testWidgets("Category is displayed", (tester) async { - final appsService = MockAppsService(); - final favoritesCategory = - fakeCategory(name: "Favorites", sort: CategorySort.alphabetical, type: CategoryType.grid, columnsCount: 6); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(favoritesCategory, []), - CategoryWithApps(fakeCategory(name: "Applications"), []), - ]); - - await _pumpWidgetWithProviders(tester, appsService, favoritesCategory.id); - - expect(find.text("Favorites"), findsNWidgets(2)); - expect(find.text("Alphabetical"), findsOneWidget); - expect(find.text("Grid"), findsOneWidget); - expect(find.text("6"), findsOneWidget); - }); - - testWidgets("'Edit name' opens AddCategoryDialog", (tester) async { - final appsService = MockAppsService(); - final favoritesCategory = - fakeCategory(name: "Favorites", sort: CategorySort.alphabetical, type: CategoryType.grid, columnsCount: 6); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(favoritesCategory, []), - CategoryWithApps(fakeCategory(name: "Applications"), []), - ]); - - await _pumpWidgetWithProviders(tester, appsService, favoritesCategory.id); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - - expect(find.byType(AddCategoryDialog), findsOneWidget); - }); - - testWidgets("'Sort' calls AppsService", (tester) async { - final appsService = MockAppsService(); - final favoritesCategory = - fakeCategory(name: "Favorites", sort: CategorySort.alphabetical, type: CategoryType.grid, columnsCount: 6); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(favoritesCategory, []), - CategoryWithApps(fakeCategory(name: "Applications"), []), - ]); - - await _pumpWidgetWithProviders(tester, appsService, favoritesCategory.id); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - - verify(appsService.setCategorySort(favoritesCategory, CategorySort.manual)); - }); - - testWidgets("'Type' calls AppsService", (tester) async { - final appsService = MockAppsService(); - final favoritesCategory = - fakeCategory(name: "Favorites", sort: CategorySort.alphabetical, type: CategoryType.row, rowHeight: 110); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(favoritesCategory, []), - CategoryWithApps(fakeCategory(name: "Applications"), []), - ]); - - await _pumpWidgetWithProviders(tester, appsService, favoritesCategory.id); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - - verify(appsService.setCategoryType(favoritesCategory, CategoryType.grid)); - }); - - testWidgets("'Columns count' calls AppsService", (tester) async { - final appsService = MockAppsService(); - final favoritesCategory = - fakeCategory(name: "Favorites", sort: CategorySort.alphabetical, type: CategoryType.grid, columnsCount: 6); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(favoritesCategory, []), - CategoryWithApps(fakeCategory(name: "Applications"), []), - ]); - - await _pumpWidgetWithProviders(tester, appsService, favoritesCategory.id); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - - verify(appsService.setCategoryColumnsCount(favoritesCategory, 7)); - }); - - testWidgets("'Row height' calls AppsService", (tester) async { - final appsService = MockAppsService(); - final favoritesCategory = - fakeCategory(name: "Favorites", sort: CategorySort.alphabetical, type: CategoryType.row, rowHeight: 110); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(favoritesCategory, []), - CategoryWithApps(fakeCategory(name: "Applications"), []), - ]); - - await _pumpWidgetWithProviders(tester, appsService, favoritesCategory.id); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - - verify(appsService.setCategoryRowHeight(favoritesCategory, 120)); - }); - - testWidgets("'Delete' calls AppsService", (tester) async { - final appsService = MockAppsService(); - final favoritesCategory = - fakeCategory(name: "Favorites", sort: CategorySort.alphabetical, type: CategoryType.row, rowHeight: 110); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(favoritesCategory, []), - CategoryWithApps(fakeCategory(name: "Applications"), []), - ]); - - await _pumpWidgetWithProviders(tester, appsService, favoritesCategory.id); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - - verify(appsService.deleteSection(favoritesCategory)); - }); -} - -Future _pumpWidgetWithProviders(WidgetTester tester, AppsService appsService, int categoryId) async { - await tester.pumpWidget( - MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: appsService), - ], - builder: (_, __) => MaterialApp( - home: Scaffold(body: CategoryPanelPage(categoryId: categoryId)), - ), - ), - ); - await tester.pumpAndSettle(); -} diff --git a/test/widgets/settings/launcher_section_panel_page_test.dart b/test/widgets/settings/launcher_section_panel_page_test.dart new file mode 100644 index 00000000..ad6bbff8 --- /dev/null +++ b/test/widgets/settings/launcher_section_panel_page_test.dart @@ -0,0 +1,93 @@ +/* + * FLauncher + * Copyright (C) 2021 Étienne Fesser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:flauncher/providers/apps_service.dart'; +import 'package:flauncher/models/category.dart'; +import 'package:flauncher/widgets/settings/launcher_section_panel_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; + +import '../../mocks.dart'; +import '../../mocks.mocks.dart'; + +void main() { + setUpAll(() async { + final binding = TestWidgetsFlutterBinding.ensureInitialized(); + binding.window.physicalSizeTestValue = Size(1280, 720); + binding.window.devicePixelRatioTestValue = 1.0; + // Scale-down the font size because the font 'Ahem' used when running tests is much wider than Roboto + binding.platformDispatcher.textScaleFactorTestValue = 0.8; + }); + + testWidgets("Category is displayed", (tester) async { + final appsService = MockAppsService(); + final favoritesCategory = + fakeCategory(name: "Favorites", sort: CategorySort.alphabetical, type: CategoryType.grid, columnsCount: 6); + when(appsService.launcherSections).thenReturn([ + favoritesCategory, + fakeCategory(name: "Applications"), + ]); + + await _pumpWidgetWithProviders(tester, appsService, 0); + + expect(find.text("Favorites"), findsWidgets); + }); + + testWidgets("'Delete' calls AppsService", (tester) async { + final appsService = MockAppsService(); + final favoritesCategory = + fakeCategory(name: "Favorites", sort: CategorySort.alphabetical, type: CategoryType.row, rowHeight: 110); + when(appsService.launcherSections).thenReturn([ + favoritesCategory, + fakeCategory(name: "Applications"), + ]); + + await _pumpWidgetWithProviders(tester, appsService, 0); + + final deleteButton = find.text("Delete"); + expect(deleteButton, findsOneWidget); + await tester.tap(deleteButton); + await tester.pumpAndSettle(); + + verify(appsService.deleteSection(0)); + }); +} + +Future _pumpWidgetWithProviders(WidgetTester tester, AppsService appsService, int sectionIndex) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: appsService), + ], + builder: (_, __) => MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold(body: LauncherSectionPanelPage(sectionIndex: sectionIndex)), + ), + ), + ); + await tester.pumpAndSettle(); +} diff --git a/test/widgets/settings/categories_panel_page_test.dart b/test/widgets/settings/launcher_sections_panel_page_test.dart similarity index 60% rename from test/widgets/settings/categories_panel_page_test.dart rename to test/widgets/settings/launcher_sections_panel_page_test.dart index 65c8ee78..fcfdf3d9 100644 --- a/test/widgets/settings/categories_panel_page_test.dart +++ b/test/widgets/settings/launcher_sections_panel_page_test.dart @@ -16,13 +16,13 @@ * along with this program. If not, see . */ -import 'package:flauncher/database.dart'; import 'package:flauncher/providers/apps_service.dart'; -import 'package:flauncher/widgets/rename_category_dialog.dart'; import 'package:flauncher/widgets/settings/launcher_sections_panel_page.dart'; -import 'package:flauncher/widgets/settings/category_panel_page.dart'; +import 'package:flauncher/widgets/settings/launcher_section_panel_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; @@ -41,9 +41,9 @@ void main() { testWidgets("Categories are displayed", (tester) async { final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "Favorites"), []), - CategoryWithApps(fakeCategory(name: "Applications"), []), + when(appsService.launcherSections).thenReturn([ + fakeCategory(name: "Favorites"), + fakeCategory(name: "Applications"), ]); await _pumpWidgetWithProviders(tester, appsService); @@ -54,52 +54,53 @@ void main() { testWidgets("'Arrow down' change category order", (tester) async { final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "Favorites"), []), - CategoryWithApps(fakeCategory(name: "Applications"), []), + when(appsService.launcherSections).thenReturn([ + fakeCategory(name: "Favorites"), + fakeCategory(name: "Applications"), ]); await _pumpWidgetWithProviders(tester, appsService); + // Explicitly request focus on the Focus node of the widget + Focus.of(tester.element(find.text("Favorites"))).requestFocus(); + await tester.pumpAndSettle(); + + // Enter move state and move down + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pumpAndSettle(); - verify(appsService.moveCategory(0, 1)); - expect(find.text("Favorites"), findsOneWidget); - expect(find.text("Applications"), findsOneWidget); + verify(appsService.moveSectionInMemory(0, 1)); }); - testWidgets("'Settings' opens CategoryPanelPage", (tester) async { + testWidgets("'Settings' opens LauncherSectionPanelPage", (tester) async { final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "Favorites"), []), - CategoryWithApps(fakeCategory(name: "Applications"), []), + when(appsService.launcherSections).thenReturn([ + fakeCategory(name: "Favorites"), + fakeCategory(name: "Applications"), ]); await _pumpWidgetWithProviders(tester, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + // Tap to open it + await tester.tap(find.text("Favorites")); await tester.pumpAndSettle(); - expect(find.byKey(Key("CategoryPanelPage")), findsOneWidget); + expect(find.byKey(Key("LauncherSectionPanelPage")), findsOneWidget); }); - testWidgets("'Add Category' opens AddCategoryDialog", (tester) async { + testWidgets("'Add Section' opens LauncherSectionPanelPage", (tester) async { final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([ - CategoryWithApps(fakeCategory(name: "Favorites"), []), - CategoryWithApps(fakeCategory(name: "Applications"), []), + when(appsService.launcherSections).thenReturn([ + fakeCategory(name: "Favorites"), + fakeCategory(name: "Applications"), ]); await _pumpWidgetWithProviders(tester, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + // Tap "Add section" (lowercase s) + await tester.tap(find.text("Add section")); await tester.pumpAndSettle(); - expect(find.byType(AddCategoryDialog), findsOneWidget); + expect(find.byKey(Key("LauncherSectionPanelPage")), findsOneWidget); }); } @@ -110,8 +111,14 @@ Future _pumpWidgetWithProviders(WidgetTester tester, AppsService appsServi ChangeNotifierProvider.value(value: appsService), ], builder: (_, __) => MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, routes: { - CategoryPanelPage.routeName: (_) => Container(key: Key("CategoryPanelPage")), + LauncherSectionPanelPage.routeName: (_) => Container(key: Key("LauncherSectionPanelPage")), }, home: Scaffold(body: LauncherSectionsPanelPage()), ), diff --git a/test/widgets/settings/settings_panel_page_test.dart b/test/widgets/settings/settings_panel_page_test.dart index 64aa7a02..2fe1567e 100644 --- a/test/widgets/settings/settings_panel_page_test.dart +++ b/test/widgets/settings/settings_panel_page_test.dart @@ -19,12 +19,14 @@ import 'package:flauncher/providers/apps_service.dart'; import 'package:flauncher/providers/settings_service.dart'; import 'package:flauncher/widgets/settings/applications_panel_page.dart'; -import 'package:flauncher/widgets/settings/launcher_sections_panel_page.dart'; +import 'package:flauncher/widgets/settings/interface_settings_page.dart'; +import 'package:flauncher/widgets/settings/general_settings_page.dart'; import 'package:flauncher/widgets/settings/flauncher_about_dialog.dart'; import 'package:flauncher/widgets/settings/settings_panel_page.dart'; -import 'package:flauncher/widgets/settings/wallpaper_panel_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:package_info_plus_platform_interface/package_info_data.dart'; @@ -32,7 +34,6 @@ import 'package:package_info_plus_platform_interface/package_info_platform_inter import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:provider/provider.dart'; -import '../../flauncher_test.dart'; import '../../mocks.mocks.dart'; void main() { @@ -47,150 +48,72 @@ void main() { testWidgets("'Applications' opens ApplicationsPanelPage", (tester) async { final settingsService = MockSettingsService(); final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([]); + when(appsService.launcherSections).thenReturn([]); when(appsService.applications).thenReturn([]); when(settingsService.appHighlightAnimationEnabled).thenReturn(true); await _pumpWidgetWithProviders(tester, settingsService, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.text("Applications")); await tester.pumpAndSettle(); expect(find.byKey(Key("ApplicationsPanelPage")), findsOneWidget); }); - testWidgets("'Categories' opens CategoriesPanelPage", (tester) async { + testWidgets("'Interface' opens InterfaceSettingsPage", (tester) async { final settingsService = MockSettingsService(); final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([]); + when(appsService.launcherSections).thenReturn([]); when(appsService.applications).thenReturn([]); when(settingsService.appHighlightAnimationEnabled).thenReturn(true); await _pumpWidgetWithProviders(tester, settingsService, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.text("Interface")); await tester.pumpAndSettle(); - expect(find.byKey(Key("CategoriesPanelPage")), findsOneWidget); + expect(find.byKey(Key("InterfaceSettingsPage")), findsOneWidget); }); - testWidgets("'Wallpaper' navigates to WallpaperPanelPage", (tester) async { + testWidgets("'System' opens GeneralSettingsPage", (tester) async { final settingsService = MockSettingsService(); final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([]); + when(appsService.launcherSections).thenReturn([]); when(appsService.applications).thenReturn([]); when(settingsService.appHighlightAnimationEnabled).thenReturn(true); await _pumpWidgetWithProviders(tester, settingsService, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.text("System")); await tester.pumpAndSettle(); - expect(find.byKey(Key("WallpaperPanelPage")), findsOneWidget); + expect(find.byKey(Key("GeneralSettingsPage")), findsOneWidget); }); testWidgets("'Android settings' calls AppsService", (tester) async { final settingsService = MockSettingsService(); final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([]); + when(appsService.launcherSections).thenReturn([]); when(appsService.applications).thenReturn([]); when(settingsService.appHighlightAnimationEnabled).thenReturn(true); await _pumpWidgetWithProviders(tester, settingsService, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.text("System settings")); await tester.pumpAndSettle(); verify(appsService.openSettings()); }); - - testWidgets("'Use 24-hour time format' toggle calls SettingsService", (tester) async { - final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([]); - when(appsService.applications).thenReturn([]); - - await _pumpWidgetWithProviders(tester, mkSettingsService(), appsService); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - //verify(settingsService.setDateTimeFormat("EEE", "HHH")); - }); - - testWidgets("'Crash Reporting' toggle calls SettingsService", (tester) async { - final settingsService = MockSettingsService(); - final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([]); - when(appsService.applications).thenReturn([]); - when(settingsService.appHighlightAnimationEnabled).thenReturn(true); - - await _pumpWidgetWithProviders(tester, settingsService, appsService); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - }); - - testWidgets("'Analytics Reporting' toggle calls SettingsService", (tester) async { - final settingsService = MockSettingsService(); - final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([]); - when(appsService.applications).thenReturn([]); - when(settingsService.appHighlightAnimationEnabled).thenReturn(true); - - await _pumpWidgetWithProviders(tester, settingsService, appsService); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - }); - - testWidgets("'About FLauncher' opens about dialog", (tester) async { + testWidgets("'About LTvLauncher' opens about dialog", (tester) async { final settingsService = MockSettingsService(); final appsService = MockAppsService(); - when(appsService.categoriesWithApps).thenReturn([]); + when(appsService.launcherSections).thenReturn([]); when(appsService.applications).thenReturn([]); when(settingsService.appHighlightAnimationEnabled).thenReturn(true); PackageInfoPlatform.instance = _MockPackageInfoPlatform(); await _pumpWidgetWithProviders(tester, settingsService, appsService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.text("About LTvLauncher")); await tester.pumpAndSettle(); - expect(find.byType(FLauncherAboutDialog), findsOneWidget); + expect(find.byType(LTvLauncherAboutDialog), findsOneWidget); }); } @@ -206,9 +129,15 @@ Future _pumpWidgetWithProviders( ChangeNotifierProvider.value(value: appsService), ], builder: (_, __) => MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, routes: { - LauncherSectionsPanelPage.routeName: (_) => Container(key: Key("CategoriesPanelPage")), - WallpaperPanelPage.routeName: (_) => Container(key: Key("WallpaperPanelPage")), + InterfaceSettingsPage.routeName: (_) => Container(key: Key("InterfaceSettingsPage")), + GeneralSettingsPage.routeName: (_) => Container(key: Key("GeneralSettingsPage")), ApplicationsPanelPage.routeName: (_) => Container(key: Key("ApplicationsPanelPage")), }, home: Material(child: SettingsPanelPage()), @@ -221,8 +150,8 @@ Future _pumpWidgetWithProviders( class _MockPackageInfoPlatform with MockPlatformInterfaceMixin implements PackageInfoPlatform { @override Future getAll({String? baseUrl}) async => PackageInfoData( - appName: "FLauncher", - packageName: "me.efesser.flauncher", + appName: "LTvLauncher", + packageName: "com.leanbitlab.ltvL", version: "1.0.0", buildNumber: "1", buildSignature: "", diff --git a/test/widgets/settings/wallpaper_panel_page_test.dart b/test/widgets/settings/wallpaper_panel_page_test.dart index 247d6a30..07a3005b 100644 --- a/test/widgets/settings/wallpaper_panel_page_test.dart +++ b/test/widgets/settings/wallpaper_panel_page_test.dart @@ -22,6 +22,8 @@ import 'package:flauncher/widgets/settings/gradient_panel_page.dart'; import 'package:flauncher/widgets/settings/wallpaper_panel_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flauncher/l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; @@ -43,8 +45,7 @@ void main() { await _pumpWidgetWithProviders(tester, settingsService, wallpaperService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.text("Gradient")); await tester.pumpAndSettle(); expect(find.byKey(Key("GradientPanelPage")), findsOneWidget); }, skip: true); @@ -56,9 +57,7 @@ void main() { await _pumpWidgetWithProviders(tester, settingsService, wallpaperService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.text("Picture")); await tester.pumpAndSettle(); verify(wallpaperService.pickWallpaper()); }); @@ -70,12 +69,10 @@ void main() { await _pumpWidgetWithProviders(tester, settingsService, wallpaperService); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.tap(find.text("Picture")); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsOneWidget); - expect(find.text("Please install a file explorer in order to pick an image."), findsOneWidget); + expect(find.text("Please install a file explorer in order to pick a picture."), findsOneWidget); }); }); } @@ -85,6 +82,7 @@ Future _pumpWidgetWithProviders( SettingsService settingsService, WallpaperService wallpaperService, ) async { + when(settingsService.timeBasedWallpaperEnabled).thenReturn(false); await tester.pumpWidget( MultiProvider( providers: [ @@ -92,6 +90,12 @@ Future _pumpWidgetWithProviders( ChangeNotifierProvider.value(value: wallpaperService), ], builder: (_, __) => MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, routes: { GradientPanelPage.routeName: (_) => Container(key: Key("GradientPanelPage")), },