├── web ├── online │ ├── connectivity_check.txt │ └── status.html ├── avc.wasm ├── favicon.ico ├── icons │ ├── apple-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── ms-icon-70x70.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── apple-icon-precomposed.png │ └── manifest.json ├── browserconfig.xml ├── mjpeg.js ├── h264Brodway.js ├── h264WebCodecs.js ├── audio-ws-worker.js ├── estimator.js └── h264BrodwayWorker.js ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── .gitignore └── Podfile ├── lib ├── common │ ├── navigation │ │ ├── ta_page_type.dart │ │ ├── ta_page.dart │ │ ├── ta_navigator.dart │ │ └── ta_page_factory.dart │ ├── ui │ │ ├── constants │ │ │ ├── ta_colors.dart │ │ │ ├── ta_timing.dart │ │ │ └── ta_dimens.dart │ │ └── components │ │ │ ├── ta_app_bar.dart │ │ │ ├── ta_bottom_sheet.dart │ │ │ └── ta_bottom_navigation_bar.dart │ ├── di │ │ ├── ta_locator.dart │ │ ├── network_module.dart │ │ └── app_module.dart │ ├── utils │ │ ├── logger.dart │ │ └── audio_api.dart │ └── network │ │ ├── health_service.dart │ │ ├── github_service.dart │ │ ├── device_info_service.dart │ │ ├── display_service.dart │ │ ├── health_service.g.dart │ │ ├── configuration_service.dart │ │ ├── github_service.g.dart │ │ ├── base_websocket_transport.dart │ │ ├── device_info_service.g.dart │ │ └── display_service.g.dart ├── feature │ ├── connectivityCheck │ │ ├── model │ │ │ └── connectivity_state.dart │ │ └── cubit │ │ │ └── connectivity_check_cubit.dart │ ├── home │ │ ├── cubit │ │ │ ├── ota_update_state.dart │ │ │ └── ota_update_cubit.dart │ │ ├── model │ │ │ ├── github_release.g.dart │ │ │ └── github_release.dart │ │ ├── repository │ │ │ └── github_release_repository.dart │ │ └── widget │ │ │ ├── settings_button.dart │ │ │ ├── display_size_watcher.dart │ │ │ ├── update_button.dart │ │ │ └── audio_button.dart │ ├── settings │ │ ├── repository │ │ │ ├── device_info_repository.dart │ │ │ └── system_configuration_repository.dart │ │ ├── bloc │ │ │ ├── device_info_state.dart │ │ │ ├── audio_configuration_state.dart │ │ │ ├── gps_configuration_state.dart │ │ │ ├── rear_display_configuration_state.dart │ │ │ ├── display_configuration_state.dart │ │ │ ├── device_info_cubit.dart │ │ │ ├── gps_configuration_cubit.dart │ │ │ ├── audio_configuration_cubit.dart │ │ │ ├── rear_display_configuration_cubit.dart │ │ │ ├── system_configuration_state.dart │ │ │ └── display_configuration_cubit.dart │ │ ├── widget │ │ │ ├── settings_tile.dart │ │ │ ├── settings_section.dart │ │ │ ├── gps_settings.dart │ │ │ ├── settings_page.dart │ │ │ ├── device_settings.dart │ │ │ └── sound_settings.dart │ │ └── model │ │ │ ├── device_info.g.dart │ │ │ ├── softap_band_type.dart │ │ │ ├── device_info.dart │ │ │ ├── system_configuration_response_body.dart │ │ │ └── system_configuration_response_body.g.dart │ ├── releaseNotes │ │ ├── model │ │ │ ├── release_notes.dart │ │ │ ├── version.dart │ │ │ ├── release_notes.g.dart │ │ │ ├── version.g.dart │ │ │ ├── changelog_item.dart │ │ │ └── changelog_item.g.dart │ │ ├── widget │ │ │ ├── detail │ │ │ │ └── release_notes_changelog_item_details_view.dart │ │ │ ├── list │ │ │ │ └── release_notes_versions_list.dart │ │ │ ├── card │ │ │ │ └── release_notes_changelog_item_card.dart │ │ │ └── release_notes_page.dart │ │ └── cubit │ │ │ ├── release_notes_state.dart │ │ │ └── release_notes_cubit.dart │ ├── touchscreen │ │ ├── model │ │ │ ├── virtual_touchscreen_slot_state.dart │ │ │ └── virtual_touchscreen_command.dart │ │ ├── touchscreen_view.dart │ │ └── cubit │ │ │ └── touchscreen_cubit.dart │ ├── display │ │ ├── repository │ │ │ └── display_repository.dart │ │ ├── cubit │ │ │ └── display_state.dart │ │ ├── model │ │ │ └── remote_display_state.g.dart │ │ └── widget │ │ │ └── display_view.dart │ ├── donations │ │ └── widget │ │ │ └── donation_page.dart │ └── about │ │ └── about_page.dart └── main.dart ├── .fvm └── fvm_config.json ├── fonts └── Roboto-Regular.ttf ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── eu │ │ │ │ │ └── gapinski │ │ │ │ │ └── tesla_android_automotive │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── images └── png │ └── tesla-android-logo.png ├── devtools_options.yaml ├── .vscode ├── settings.json └── launch.json ├── README.md ├── .gitignore ├── .metadata ├── pubspec.yaml ├── analysis_options.yaml └── jenkins └── multi-branch-ci.groovy /web/online/connectivity_check.txt: -------------------------------------------------------------------------------- 1 | online -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/common/navigation/ta_page_type.dart: -------------------------------------------------------------------------------- 1 | enum TAPageType { standard, dialog } -------------------------------------------------------------------------------- /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.35.0", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /web/avc.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/avc.wasm -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/favicon.ico -------------------------------------------------------------------------------- /web/online/status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /web/icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon.png -------------------------------------------------------------------------------- /web/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/favicon-16x16.png -------------------------------------------------------------------------------- /web/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/favicon-32x32.png -------------------------------------------------------------------------------- /web/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/favicon-96x96.png -------------------------------------------------------------------------------- /web/icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /lib/feature/connectivityCheck/model/connectivity_state.dart: -------------------------------------------------------------------------------- 1 | enum ConnectivityState { backendAccessible, backendUnreachable } -------------------------------------------------------------------------------- /web/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /web/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /web/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /web/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /web/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /web/icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /web/icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /images/png/tesla-android-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/images/png/tesla-android-logo.png -------------------------------------------------------------------------------- /web/icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/android-icon-36x36.png -------------------------------------------------------------------------------- /web/icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/android-icon-48x48.png -------------------------------------------------------------------------------- /web/icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/android-icon-72x72.png -------------------------------------------------------------------------------- /web/icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/android-icon-96x96.png -------------------------------------------------------------------------------- /web/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /web/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /web/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /web/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /web/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /web/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /web/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /web/icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/web/icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/common/ui/constants/ta_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TAColors { 4 | static const settingsPrimaryColor = Color(0xFFB71C1C); 5 | } 6 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesla-android/flutter-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/eu/gapinski/tesla_android_automotive/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package eu.gapinski.tesla_android_automotive 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /lib/feature/home/cubit/ota_update_state.dart: -------------------------------------------------------------------------------- 1 | abstract class OTAUpdateState {} 2 | 3 | class OTAUpdateStateInitial extends OTAUpdateState {} 4 | 5 | class OTAUpdateStateAvailable extends OTAUpdateState {} 6 | 7 | class OTAUpdateStateNotAvailable extends OTAUpdateState {} -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /lib/common/di/ta_locator.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:tesla_android/common/di/ta_locator.config.dart'; 4 | 5 | final getIt = GetIt.instance; 6 | 7 | @InjectableInit() 8 | Future configureTADependencies() => getIt.init(); 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /web/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/common/utils/logger.dart: -------------------------------------------------------------------------------- 1 | mixin Logger { 2 | void log(String message) { 3 | print("[$runtimeType $hashCode] $message" ); 4 | } 5 | 6 | void logException({exception, StackTrace? stackTrace}) { 7 | print("[$runtimeType] ${exception.toString()}"); 8 | print("[$runtimeType] ${stackTrace.toString()}"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": ".fvm/flutter_sdk", 3 | // Remove .fvm files from search 4 | "search.exclude": { 5 | "**/.fvm": true 6 | }, 7 | // Remove from file watching 8 | "files.watcherExclude": { 9 | "**/.fvm": true 10 | }, 11 | "cSpell.words": [ 12 | "Dimens" 13 | ] 14 | } -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /lib/common/ui/constants/ta_timing.dart: -------------------------------------------------------------------------------- 1 | class TATiming { 2 | static const splashPageTransitionDuration = Duration(seconds: 2); 3 | static const webRtcTimeoutDuration = Duration(seconds: 5); 4 | static const snackBarDuration = Duration(seconds: 30); 5 | static const animationDuration = Duration(milliseconds: 250); 6 | static const timeoutDuration = Duration(milliseconds: 250); 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/common/di/network_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | @module 5 | abstract class NetworkModule { 6 | @singleton 7 | Dio get dio => Dio( 8 | BaseOptions( 9 | connectTimeout: const Duration(seconds: 5), 10 | receiveTimeout: const Duration(seconds: 5), 11 | sendTimeout: const Duration(seconds: 5), 12 | ), 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/feature/settings/repository/device_info_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:injectable/injectable.dart'; 2 | import 'package:tesla_android/common/network/device_info_service.dart'; 3 | import 'package:tesla_android/feature/settings/model/device_info.dart'; 4 | 5 | @injectable 6 | class DeviceInfoRepository { 7 | final DeviceInfoService _service; 8 | 9 | DeviceInfoRepository(this._service); 10 | 11 | Future getDeviceInfo(){ 12 | return _service.getDeviceInfo(); 13 | } 14 | } -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /web/mjpeg.js: -------------------------------------------------------------------------------- 1 | var img = document.getElementById("image"); 2 | var canvas = document.getElementById("canvas"); 3 | var urlCreator = window.URL || window.webkitURL; 4 | img.style.display = "block"; 5 | canvas.style.display = "none"; 6 | 7 | var currentImageUrl; 8 | 9 | function drawDisplayFrame(blob) { 10 | if (currentImageUrl) { 11 | urlCreator.revokeObjectURL(currentImageUrl); 12 | } 13 | 14 | 15 | let imageUrl = urlCreator.createObjectURL(blob); 16 | currentImageUrl = imageUrl; 17 | 18 | img.src = imageUrl; 19 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/feature/settings/bloc/device_info_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:tesla_android/feature/settings/model/device_info.dart'; 2 | 3 | abstract class DeviceInfoState {} 4 | 5 | class DeviceInfoStateInitial extends DeviceInfoState {} 6 | 7 | class DeviceInfoStateLoading extends DeviceInfoState {} 8 | 9 | class DeviceInfoStateLoaded 10 | extends DeviceInfoState { 11 | final DeviceInfo deviceInfo; 12 | 13 | DeviceInfoStateLoaded({ 14 | required this.deviceInfo, 15 | }); 16 | } 17 | class DeviceInfoStateError extends DeviceInfoState {} 18 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/model/release_notes.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:tesla_android/feature/releaseNotes/model/version.dart'; 3 | 4 | part 'release_notes.g.dart'; 5 | 6 | @JsonSerializable() 7 | class ReleaseNotes { 8 | final List versions; 9 | 10 | const ReleaseNotes({ 11 | required this.versions, 12 | }); 13 | 14 | factory ReleaseNotes.fromJson(Map json) => 15 | _$ReleaseNotesFromJson(json); 16 | 17 | Map toJson() => _$ReleaseNotesToJson(this); 18 | } 19 | -------------------------------------------------------------------------------- /lib/feature/home/model/github_release.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'github_release.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | GitHubRelease _$GitHubReleaseFromJson(Map json) => 10 | GitHubRelease(name: json['name'] as String); 11 | 12 | Map _$GitHubReleaseToJson(GitHubRelease instance) => 13 | {'name': instance.name}; 14 | -------------------------------------------------------------------------------- /lib/feature/touchscreen/model/virtual_touchscreen_slot_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | class VirtualTouchscreenSlotState { 4 | int slotIndex; // 0-9 5 | int trackingId; // active pointer index or -1 6 | Offset position; 7 | 8 | VirtualTouchscreenSlotState.initial({required this.slotIndex}) 9 | : trackingId = -1, 10 | position = Offset.zero; 11 | 12 | static List generateSlots() { 13 | return List.generate( 14 | 10, 15 | (index) => VirtualTouchscreenSlotState.initial(slotIndex: index), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tesla Android 2 | 3 | Flutter app for Tesla Android. 4 | 5 | Please refer to https://teslaandroid.com for release notes, hardware requirements and the install guide. 6 | 7 | ## Getting Started 8 | 9 | ``` 10 | flutter pub get 11 | flutter packages pub run build_runner build --delete-conflicting-outputs 12 | flutter build web 13 | ``` 14 | 15 | In order to build this project for debugging make sure to disable cors in Chrome and connect to Tesla Android Wi-Fi network 16 | 17 | #### Please consider supporting the project: 18 | 19 | [Donations](https://teslaandroid.com/donations) 20 | 21 | -------------------------------------------------------------------------------- /lib/common/network/health_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart' hide Headers; 2 | import 'package:flavor/flavor.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | import 'package:retrofit/retrofit.dart'; 5 | 6 | part 'health_service.g.dart'; 7 | 8 | @injectable 9 | @RestApi() 10 | abstract class HealthService { 11 | @factoryMethod 12 | factory HealthService( 13 | Dio dio, 14 | Flavor flavor, 15 | ) => 16 | _HealthService( 17 | dio, 18 | baseUrl: flavor.getString("configurationApiBaseUrl"), 19 | ); 20 | 21 | @GET("/health") 22 | Future getHealthCheck(); 23 | } 24 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/model/version.dart: -------------------------------------------------------------------------------- 1 | import 'package:tesla_android/feature/releaseNotes/model/changelog_item.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'version.g.dart'; 5 | 6 | @JsonSerializable() 7 | class Version { 8 | final String versionName; 9 | final List changelogItems; 10 | 11 | const Version({ 12 | required this.versionName, 13 | required this.changelogItems, 14 | }); 15 | 16 | factory Version.fromJson(Map json) => 17 | _$VersionFromJson(json); 18 | 19 | Map toJson() => _$VersionToJson(this); 20 | } 21 | -------------------------------------------------------------------------------- /lib/feature/home/model/github_release.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'github_release.g.dart'; 5 | 6 | @JsonSerializable() 7 | class GitHubRelease extends Equatable { 8 | final String name; 9 | 10 | const GitHubRelease({ 11 | required this.name, 12 | }); 13 | 14 | factory GitHubRelease.fromJson(Map json) => 15 | _$GitHubReleaseFromJson(json); 16 | 17 | Map toJson() => _$GitHubReleaseToJson(this); 18 | 19 | @override 20 | List get props => [ 21 | name, 22 | ]; 23 | } 24 | -------------------------------------------------------------------------------- /lib/common/ui/components/ta_app_bar.dart: -------------------------------------------------------------------------------- 1 | // ignore: avoid_web_libraries_in_flutter 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:tesla_android/common/utils/logger.dart'; 5 | 6 | class TaAppBar extends StatelessWidget 7 | with Logger 8 | implements PreferredSizeWidget { 9 | final String? title; 10 | 11 | const TaAppBar({super.key, this.title}); 12 | 13 | @override 14 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return AppBar( 19 | automaticallyImplyLeading: false, 20 | centerTitle: false, 21 | title: Text(title ?? ''), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/model/release_notes.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'release_notes.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ReleaseNotes _$ReleaseNotesFromJson(Map json) => ReleaseNotes( 10 | versions: (json['versions'] as List) 11 | .map((e) => Version.fromJson(e as Map)) 12 | .toList(), 13 | ); 14 | 15 | Map _$ReleaseNotesToJson(ReleaseNotes instance) => 16 | {'versions': instance.versions}; 17 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /lib/feature/settings/bloc/audio_configuration_state.dart: -------------------------------------------------------------------------------- 1 | abstract class AudioConfigurationState {} 2 | 3 | class AudioConfigurationStateInitial extends AudioConfigurationState {} 4 | 5 | class AudioConfigurationStateLoading extends AudioConfigurationState {} 6 | 7 | class AudioConfigurationStateSettingsFetched 8 | extends AudioConfigurationState { 9 | final bool isEnabled; 10 | final int volume; 11 | 12 | AudioConfigurationStateSettingsFetched({ 13 | required this.isEnabled, 14 | required this.volume, 15 | }); 16 | } 17 | 18 | class AudioConfigurationStateSettingsUpdateInProgress 19 | extends AudioConfigurationState {} 20 | 21 | class AudioConfigurationStateError extends AudioConfigurationState {} 22 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /lib/feature/settings/bloc/gps_configuration_state.dart: -------------------------------------------------------------------------------- 1 | abstract class GPSConfigurationState {} 2 | 3 | class GPSConfigurationStateInitial extends GPSConfigurationState {} 4 | 5 | class GPSConfigurationStateLoading extends GPSConfigurationState {} 6 | 7 | class GPSConfigurationStateLoaded 8 | extends GPSConfigurationState { 9 | final bool isGPSEnabled; 10 | 11 | GPSConfigurationStateLoaded({ 12 | required this.isGPSEnabled, 13 | }); 14 | } 15 | 16 | class GPSConfigurationStateUpdateInProgress 17 | extends GPSConfigurationState { 18 | final bool isGPSEnabled; 19 | 20 | GPSConfigurationStateUpdateInProgress({ 21 | required this.isGPSEnabled, 22 | }); 23 | } 24 | 25 | class GPSConfigurationStateError extends GPSConfigurationState {} 26 | -------------------------------------------------------------------------------- /lib/feature/home/repository/github_release_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:injectable/injectable.dart'; 2 | import 'package:tesla_android/common/network/device_info_service.dart'; 3 | import 'package:tesla_android/common/network/github_service.dart'; 4 | import 'package:tesla_android/feature/home/model/github_release.dart'; 5 | 6 | @injectable 7 | class GitHubReleaseRepository { 8 | final GitHubService _service; 9 | final DeviceInfoService _deviceInfoService; 10 | 11 | GitHubReleaseRepository( 12 | this._service, 13 | this._deviceInfoService, 14 | ); 15 | 16 | Future getLatestRelease() { 17 | return _service.getLatestRelease(); 18 | } 19 | 20 | Future openUpdater() { 21 | return _deviceInfoService.openUpdater(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/model/version.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'version.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Version _$VersionFromJson(Map json) => Version( 10 | versionName: json['versionName'] as String, 11 | changelogItems: (json['changelogItems'] as List) 12 | .map((e) => ChangelogItem.fromJson(e as Map)) 13 | .toList(), 14 | ); 15 | 16 | Map _$VersionToJson(Version instance) => { 17 | 'versionName': instance.versionName, 18 | 'changelogItems': instance.changelogItems, 19 | }; 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [{ 7 | "name": "flutter-app", 8 | "request": "launch", 9 | "type": "dart" 10 | }, 11 | { 12 | "name": "flutter-app (profile mode)", 13 | "request": "launch", 14 | "type": "dart", 15 | "flutterMode": "profile" 16 | }, 17 | { 18 | "name": "flutter-app (release mode)", 19 | "request": "launch", 20 | "type": "dart", 21 | "flutterMode": "release" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /lib/common/network/github_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart' hide Headers; 2 | import 'package:flavor/flavor.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | import 'package:retrofit/retrofit.dart'; 5 | import 'package:tesla_android/feature/home/model/github_release.dart'; 6 | 7 | part 'github_service.g.dart'; 8 | 9 | @injectable 10 | @RestApi() 11 | abstract class GitHubService { 12 | @factoryMethod 13 | factory GitHubService( 14 | Dio dio, 15 | Flavor flavor, 16 | ) => 17 | _GitHubService( 18 | dio, 19 | baseUrl: "https://api.github.com", 20 | ); 21 | 22 | @GET("/repos/tesla-android/android-raspberry-pi/releases/latest") 23 | @Headers({ 24 | "X-GitHub-Api-Version": "2022-11-28", 25 | }) Future getLatestRelease(); 26 | } 27 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/widget/detail/release_notes_changelog_item_details_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 3 | import 'package:tesla_android/feature/releaseNotes/model/changelog_item.dart'; 4 | 5 | class ReleaseNotesChangelogItemDetailsView extends StatelessWidget { 6 | final ChangelogItem changelogItem; 7 | 8 | const ReleaseNotesChangelogItemDetailsView({ 9 | super.key, 10 | required, 11 | required this.changelogItem, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Padding( 17 | padding: TADimens.basePadding, 18 | child: Text( 19 | changelogItem.descriptionMarkdown, 20 | style: Theme.of(context).textTheme.bodyLarge, 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/common/network/device_info_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart' hide Headers; 2 | import 'package:flavor/flavor.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | import 'package:retrofit/retrofit.dart'; 5 | import 'package:tesla_android/feature/settings/model/device_info.dart'; 6 | 7 | part 'device_info_service.g.dart'; 8 | 9 | @injectable 10 | @RestApi() 11 | abstract class DeviceInfoService { 12 | @factoryMethod 13 | factory DeviceInfoService( 14 | Dio dio, 15 | Flavor flavor, 16 | ) => 17 | _DeviceInfoService( 18 | dio, 19 | baseUrl: flavor.getString("configurationApiBaseUrl"), 20 | ); 21 | 22 | @GET("/deviceInfo") 23 | @DioResponseType(ResponseType.json) 24 | Future getDeviceInfo(); 25 | 26 | @GET("/openUpdater") 27 | Future openUpdater(); 28 | } 29 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/model/changelog_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'changelog_item.g.dart'; 5 | 6 | @JsonSerializable() 7 | class ChangelogItem extends Equatable { 8 | final String title; 9 | final String shortDescription; 10 | final String descriptionMarkdown; 11 | 12 | const ChangelogItem({ 13 | required this.title, 14 | required this.shortDescription, 15 | required this.descriptionMarkdown, 16 | }); 17 | 18 | factory ChangelogItem.fromJson(Map json) => 19 | _$ChangelogItemFromJson(json); 20 | 21 | Map toJson() => _$ChangelogItemToJson(this); 22 | 23 | @override 24 | List get props => [ 25 | title, 26 | shortDescription, 27 | descriptionMarkdown, 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /lib/feature/home/widget/settings_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tesla_android/common/navigation/ta_navigator.dart'; 3 | import 'package:tesla_android/common/navigation/ta_page.dart'; 4 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 5 | import 'package:tesla_android/common/utils/logger.dart'; 6 | 7 | class SettingsButton extends StatelessWidget with Logger { 8 | const SettingsButton({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return IconButton( 13 | color: Colors.white, 14 | onPressed: () { 15 | TANavigator.pushReplacement( 16 | context: context, 17 | page: TAPage.about, 18 | ); 19 | }, 20 | icon: const Icon( 21 | Icons.settings, 22 | size: TADimens.statusBarIconSize, 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/model/changelog_item.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'changelog_item.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ChangelogItem _$ChangelogItemFromJson(Map json) => 10 | ChangelogItem( 11 | title: json['title'] as String, 12 | shortDescription: json['shortDescription'] as String, 13 | descriptionMarkdown: json['descriptionMarkdown'] as String, 14 | ); 15 | 16 | Map _$ChangelogItemToJson(ChangelogItem instance) => 17 | { 18 | 'title': instance.title, 19 | 'shortDescription': instance.shortDescription, 20 | 'descriptionMarkdown': instance.descriptionMarkdown, 21 | }; 22 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /web/icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | admin/ 2 | pihole/ 3 | # Miscellaneous 4 | *.class 5 | *.log 6 | *.pyc 7 | *.swp 8 | .DS_Store 9 | .atom/ 10 | .buildlog/ 11 | .history 12 | .svn/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .packages 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Web related 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | .fvm/flutter_sdk -------------------------------------------------------------------------------- /lib/common/network/display_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart' hide Headers; 2 | import 'package:flavor/flavor.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | import 'package:retrofit/retrofit.dart'; 5 | import 'package:tesla_android/feature/display/model/remote_display_state.dart'; 6 | 7 | part 'display_service.g.dart'; 8 | 9 | @injectable 10 | @RestApi() 11 | abstract class DisplayService { 12 | @factoryMethod 13 | factory DisplayService( 14 | Dio dio, 15 | Flavor flavor, 16 | ) => 17 | _DisplayService( 18 | dio, 19 | baseUrl: flavor.getString("configurationApiBaseUrl"), 20 | ); 21 | 22 | @GET("/displayState") 23 | @DioResponseType(ResponseType.json) 24 | Future getDisplayState(); 25 | 26 | @POST("/displayState") 27 | @Headers({ 28 | "Content-Type": "application/json", 29 | }) 30 | Future updateDisplayConfiguration(@Body() RemoteDisplayState configuration); 31 | } 32 | -------------------------------------------------------------------------------- /lib/feature/settings/bloc/rear_display_configuration_state.dart: -------------------------------------------------------------------------------- 1 | 2 | abstract class RearDisplayConfigurationState {} 3 | 4 | class RearDisplayConfigurationStateInitial 5 | extends RearDisplayConfigurationState {} 6 | 7 | class RearDisplayConfigurationStateLoading 8 | extends RearDisplayConfigurationState {} 9 | 10 | class RearDisplayConfigurationStateSettingsFetched 11 | extends RearDisplayConfigurationState { 12 | final bool isRearDisplayEnabled; 13 | final bool isRearDisplayPrioritised; 14 | final bool isCurrentDisplayPrimary; 15 | 16 | RearDisplayConfigurationStateSettingsFetched({ 17 | required this.isRearDisplayEnabled, 18 | required this.isRearDisplayPrioritised, 19 | required this.isCurrentDisplayPrimary, 20 | }); 21 | } 22 | 23 | class RearDisplayConfigurationStateSettingsUpdateInProgress 24 | extends RearDisplayConfigurationState {} 25 | 26 | class RearDisplayConfigurationStateError 27 | extends RearDisplayConfigurationState {} 28 | -------------------------------------------------------------------------------- /lib/common/utils/audio_api.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | library; 3 | 4 | import 'dart:js_interop'; 5 | import 'package:web/web.dart' as web; 6 | 7 | @JS('setupAudioConfig') 8 | external void setupAudioConfig(String configJson); 9 | 10 | @JS('startAudioFromGesture') 11 | external void startAudioFromGesture(); 12 | 13 | @JS('stopAudio') 14 | external void stopAudio(); 15 | 16 | @JS('getAudioState') 17 | external String getAudioState(); 18 | 19 | VoidCallback addAudioStateListener(void Function(String state) onState) { 20 | final jsListener = (web.Event e) { 21 | String? state; 22 | if (e is web.CustomEvent) { 23 | final detail = e.detail; // JSAny? 24 | if (detail is JSString) { 25 | state = detail.toDart; 26 | } 27 | } 28 | if (state != null) { 29 | onState(state!); 30 | } 31 | }.toJS; 32 | 33 | web.window.addEventListener('audio-state', jsListener); 34 | 35 | return () { 36 | web.window.removeEventListener('audio-state', jsListener); 37 | }; 38 | } 39 | 40 | typedef VoidCallback = void Function(); -------------------------------------------------------------------------------- /web/h264Brodway.js: -------------------------------------------------------------------------------- 1 | var img = document.getElementById("image"); 2 | var canvas = document.getElementById("canvas"); 3 | var statsElement = document.getElementById('stats'); 4 | img.style.display = "none"; 5 | canvas.style.display = "block"; 6 | 7 | canvas.width = window.innerWidth; 8 | canvas.height = window.innerHeight; 9 | 10 | const worker = new Worker('h264BrodwayWorker.js'); 11 | const offscreenCanvas = canvas.transferControlToOffscreen(); 12 | 13 | worker.postMessage({ canvas: offscreenCanvas, displayWidth: displayWidth, displayHeight: displayHeight, windowWidth: window.innerWidth, windowHeight: window.innerHeight }, [offscreenCanvas]); 14 | 15 | worker.onmessage = function (event) { 16 | const stats = event.data; 17 | updateStatsDisplay(stats); 18 | }; 19 | 20 | function updateStatsDisplay(stats) { 21 | 22 | if (statsElement) { 23 | statsElement.textContent = 'Skipped Frames: ' + stats.skippedFrames + ' Buffer size: ' + stats.bufferLength; 24 | } 25 | } 26 | 27 | function drawDisplayFrame(arrayBuffer) { 28 | worker.postMessage({ h264Data: arrayBuffer }); 29 | } -------------------------------------------------------------------------------- /web/h264WebCodecs.js: -------------------------------------------------------------------------------- 1 | var img = document.getElementById("image"); 2 | var canvas = document.getElementById("canvas"); 3 | var statsElement = document.getElementById('stats'); 4 | img.style.display = "none"; 5 | canvas.style.display = "block"; 6 | 7 | canvas.width = window.innerWidth; 8 | canvas.height = window.innerHeight; 9 | 10 | const worker = new Worker('h264WebCodecsWorker.js'); 11 | const offscreenCanvas = canvas.transferControlToOffscreen(); 12 | 13 | worker.postMessage({ canvas: offscreenCanvas, displayWidth: displayWidth, displayHeight: displayHeight, windowWidth: window.innerWidth, windowHeight: window.innerHeight }, [offscreenCanvas]); 14 | 15 | worker.onmessage = function (event) { 16 | const stats = event.data; 17 | updateStatsDisplay(stats); 18 | }; 19 | 20 | function updateStatsDisplay(stats) { 21 | 22 | if (statsElement) { 23 | statsElement.textContent = 'Skipped Frames: ' + stats.skippedFrames + ' Buffer size: ' + stats.bufferLength; 24 | } 25 | } 26 | 27 | function drawDisplayFrame(arrayBuffer) { 28 | worker.postMessage({ h264Data: arrayBuffer }); 29 | } -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "5dcb86f68f239346676ceb1ed1ea385bd215fba1" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 17 | base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 18 | - platform: web 19 | create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 20 | base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/feature/settings/bloc/display_configuration_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:tesla_android/feature/display/model/remote_display_state.dart'; 2 | 3 | abstract class DisplayConfigurationState {} 4 | 5 | class DisplayConfigurationStateInitial extends DisplayConfigurationState {} 6 | 7 | class DisplayConfigurationStateLoading extends DisplayConfigurationState {} 8 | 9 | class DisplayConfigurationStateSettingsFetched 10 | extends DisplayConfigurationState { 11 | final DisplayResolutionModePreset resolutionPreset; 12 | final DisplayRendererType renderer; 13 | final bool isResponsive; 14 | final DisplayQualityPreset quality; 15 | final DisplayRefreshRatePreset refreshRate; 16 | 17 | DisplayConfigurationStateSettingsFetched({ 18 | required this.resolutionPreset, 19 | required this.renderer, 20 | required this.isResponsive, 21 | required this.refreshRate, 22 | required this.quality, 23 | }); 24 | } 25 | 26 | class DisplayConfigurationStateSettingsUpdateInProgress 27 | extends DisplayConfigurationState {} 28 | 29 | class DisplayConfigurationStateError extends DisplayConfigurationState {} 30 | -------------------------------------------------------------------------------- /lib/feature/settings/widget/settings_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 3 | 4 | class SettingsTile extends StatelessWidget { 5 | final IconData icon; 6 | final String title; 7 | final String? subtitle; 8 | final Widget trailing; 9 | final bool dense; 10 | 11 | const SettingsTile({ 12 | super.key, 13 | required this.icon, 14 | required this.title, 15 | this.subtitle, 16 | required this.trailing, 17 | this.dense = true, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return ListTile( 23 | leading: Icon(icon), 24 | title: Text(title), 25 | subtitle: subtitle != null ? Text(subtitle!) : const SizedBox.shrink(), 26 | trailing: SizedBox( 27 | width: dense ? TADimens.settingsTileTrailingWidthDense : TADimens 28 | .settingsTileTrailingWidth, 29 | child: Row( 30 | children: [ 31 | const Spacer(), 32 | trailing, 33 | ], 34 | )), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/feature/touchscreen/model/virtual_touchscreen_command.dart: -------------------------------------------------------------------------------- 1 | class VirtualTouchScreenCommand { 2 | final int? absMtSlot; //ABS_MT_POSITION_X 3 | final int? absMtTrackingId; //ABS_MT_TRACKING_ID 4 | final int? absMtPositionX; //ABS_MT_POSITION_X 5 | final int? absMtPositionY; //ABS_MT_POSITION_Y 6 | final bool synReport; //SYN_REPORT 7 | 8 | VirtualTouchScreenCommand({ 9 | this.absMtSlot, 10 | this.absMtTrackingId, 11 | this.absMtPositionX, 12 | this.absMtPositionY, 13 | this.synReport = false, 14 | }); 15 | 16 | String build() { 17 | var command = "touchScreenCommand:"; 18 | if (absMtSlot != null) command += 's $absMtSlot\n'; 19 | if (absMtTrackingId != null) { 20 | command += 'T $absMtTrackingId\n'; 21 | if (absMtTrackingId == -1) { 22 | command += 'a 0\n'; 23 | } else { 24 | command += 'a 1\n'; 25 | } 26 | } 27 | if (absMtPositionX != null) command += 'X $absMtPositionX\n'; 28 | if (absMtPositionY != null) command += 'Y $absMtPositionY\n'; 29 | if (synReport) command += 'e 0\nS 0\n'; 30 | return command; 31 | } 32 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: tesla_android 2 | description: Tesla Android 3 | 4 | publish_to: 'none' 5 | 6 | version: 2025.46.1 7 | 8 | environment: 9 | flutter: "3.35.0" 10 | sdk: "3.9.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | dio: 5.9.0 16 | retrofit: 4.7.2 17 | pointer_interceptor: 0.10.1+2 18 | get_it: 8.2.0 19 | injectable: 2.5.1 20 | flavor: ^2.0.0 21 | flutter_bloc: 9.1.1 22 | rxdart: 0.28.0 23 | shared_preferences: 2.5.3 24 | flutter_markdown: 0.7.7+1 25 | qr_flutter: 4.1.0 26 | logger: 2.6.1 27 | json_serializable: 6.11.1 28 | package_info_plus: 8.3.1 29 | url_launcher: 6.3.2 30 | equatable: 2.0.7 31 | web_socket_client: 0.2.1 32 | web: 1.1.1 33 | json_annotation: 4.9.0 34 | 35 | dev_dependencies: 36 | flutter_test: 37 | sdk: flutter 38 | flutter_lints: 6.0.0 39 | build_runner: 2.7.1 40 | injectable_generator: 2.8.1 41 | retrofit_generator: 10.0.5 42 | 43 | flutter: 44 | assets: 45 | - images/png/ 46 | - fonts/ 47 | uses-material-design: true 48 | fonts: 49 | - family: Roboto 50 | fonts: 51 | - asset: fonts/Roboto-Regular.ttf 52 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/cubit/release_notes_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:tesla_android/feature/releaseNotes/model/changelog_item.dart'; 2 | import 'package:tesla_android/feature/releaseNotes/model/release_notes.dart'; 3 | import 'package:tesla_android/feature/releaseNotes/model/version.dart'; 4 | 5 | abstract class ReleaseNotesState {} 6 | 7 | class ReleaseNotesStateInitial extends ReleaseNotesState {} 8 | 9 | class ReleaseNotesStateLoading extends ReleaseNotesState {} 10 | 11 | class ReleaseNotesStateUnavailable extends ReleaseNotesState {} 12 | 13 | class ReleaseNotesStateLoaded extends ReleaseNotesState { 14 | final ReleaseNotes releaseNotes; 15 | final Version selectedVersion; 16 | final ChangelogItem selectedChangelogItem; 17 | 18 | ReleaseNotesStateLoaded({ 19 | required this.releaseNotes, 20 | }) : selectedVersion = releaseNotes.versions.first, 21 | selectedChangelogItem = 22 | releaseNotes.versions.first.changelogItems.first; 23 | 24 | ReleaseNotesStateLoaded.withSelection({ 25 | required this.releaseNotes, 26 | required this.selectedVersion, 27 | required this.selectedChangelogItem, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/feature/home/widget/display_size_watcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:tesla_android/feature/display/cubit/display_cubit.dart'; 4 | 5 | class DisplaySizeWatcher extends StatefulWidget { 6 | final Widget Function(Size size) builder; 7 | const DisplaySizeWatcher({super.key, required this.builder}); 8 | 9 | @override 10 | State createState() => _DisplaySizeWatcherState(); 11 | } 12 | 13 | class _DisplaySizeWatcherState extends State { 14 | Size? _lastSize; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return LayoutBuilder( 19 | builder: (context, constraints) { 20 | final size = Size(constraints.maxWidth, constraints.maxHeight); 21 | 22 | if (_lastSize != size) { 23 | _lastSize = size; 24 | WidgetsBinding.instance.addPostFrameCallback((_) { 25 | if (!mounted) return; 26 | context.read().onWindowSizeChanged(size); 27 | }); 28 | } 29 | 30 | return widget.builder(size); 31 | }, 32 | ); 33 | } 34 | } -------------------------------------------------------------------------------- /lib/feature/home/widget/update_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 4 | import 'package:tesla_android/common/utils/logger.dart'; 5 | import 'package:tesla_android/feature/home/cubit/ota_update_cubit.dart'; 6 | import 'package:tesla_android/feature/home/cubit/ota_update_state.dart'; 7 | 8 | class UpdateButton extends StatelessWidget with Logger { 9 | const UpdateButton({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return BlocBuilder( 14 | builder: (context, state) { 15 | if (state is OTAUpdateStateAvailable) { 16 | return IconButton( 17 | color: Colors.amber, 18 | onPressed: () { 19 | BlocProvider.of(context).launchUpdater(); 20 | }, 21 | icon: const Icon( 22 | Icons.download_rounded, 23 | size: TADimens.statusBarIconSize, 24 | ), 25 | ); 26 | } else { 27 | return const SizedBox(); 28 | } 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/feature/settings/bloc/device_info_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:tesla_android/common/utils/logger.dart'; 4 | import 'package:tesla_android/feature/settings/bloc/device_info_state.dart'; 5 | import 'package:tesla_android/feature/settings/repository/device_info_repository.dart'; 6 | 7 | @injectable 8 | class DeviceInfoCubit extends Cubit 9 | with Logger { 10 | final DeviceInfoRepository _repository; 11 | 12 | DeviceInfoCubit(this._repository) 13 | : super(DeviceInfoStateInitial()); 14 | 15 | void fetchConfiguration() async { 16 | if (!isClosed) emit(DeviceInfoStateLoading()); 17 | try { 18 | final healthState = await _repository.getDeviceInfo(); 19 | emit( 20 | DeviceInfoStateLoaded( 21 | deviceInfo: healthState), 22 | ); 23 | } catch (exception, stacktrace) { 24 | logException( 25 | exception: exception, 26 | stackTrace: stacktrace, 27 | ); 28 | if (!isClosed) { 29 | emit( 30 | DeviceInfoStateError(), 31 | ); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/common/navigation/ta_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:tesla_android/common/navigation/ta_page_type.dart'; 2 | 3 | class TAPage { 4 | final String title; 5 | final String route; 6 | final TAPageType type; 7 | 8 | const TAPage({ 9 | required this.title, 10 | required this.route, 11 | required this.type, 12 | }); 13 | 14 | static const empty = TAPage( 15 | title: "Empty", 16 | route: "/empty", 17 | type: TAPageType.standard, 18 | ); 19 | 20 | static const home = TAPage( 21 | title: "Home", 22 | route: "/", 23 | type: TAPageType.standard, 24 | ); 25 | 26 | static const releaseNotes = TAPage( 27 | title: "Release Notes", 28 | route: "/releaseNotes", 29 | type: TAPageType.standard, 30 | ); 31 | 32 | static const donations = TAPage( 33 | title: "Donations", 34 | route: "/donate", 35 | type: TAPageType.standard, 36 | ); 37 | 38 | static const about = TAPage( 39 | title: "About", 40 | route: "/about", 41 | type: TAPageType.standard, 42 | ); 43 | 44 | static const settings = TAPage( 45 | title: "Settings", 46 | route: "/settings", 47 | type: TAPageType.standard, 48 | ); 49 | 50 | static List get availablePages { 51 | return const [empty, home, releaseNotes, about, settings]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/feature/settings/widget/settings_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 4 | 5 | abstract class SettingsSection extends StatelessWidget { 6 | final String name; 7 | final IconData icon; 8 | 9 | const SettingsSection({ 10 | super.key, 11 | required this.name, 12 | required this.icon, 13 | }); 14 | 15 | @nonVirtual 16 | @override 17 | Widget build(BuildContext context) { 18 | return SingleChildScrollView( 19 | child: Scrollbar( 20 | child: Padding( 21 | padding: const EdgeInsets.all( 22 | TADimens.baseContentMargin, 23 | ), 24 | child: Center( 25 | child: Container( 26 | constraints: const BoxConstraints( 27 | maxWidth: TADimens.settingsPageTableMaxWidth, 28 | ), 29 | child: body(context), 30 | ), 31 | ), 32 | ), 33 | ), 34 | ); 35 | } 36 | 37 | Widget body(BuildContext context); 38 | 39 | Widget get divider => const Padding( 40 | padding: EdgeInsets.symmetric( 41 | vertical: TADimens.baseContentMargin, 42 | ), 43 | child: Divider(), 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /lib/feature/display/repository/display_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:injectable/injectable.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | import 'package:tesla_android/common/network/display_service.dart'; 4 | import 'package:tesla_android/feature/display/model/remote_display_state.dart'; 5 | 6 | @injectable 7 | class DisplayRepository { 8 | final DisplayService _service; 9 | final SharedPreferences _sharedPreferences; 10 | 11 | final String _isPrimaryDisplaySharedPreferencesKey = 12 | 'DisplayRepository_isPrimaryDisplaySharedPreferencesKey'; 13 | 14 | DisplayRepository(this._service, this._sharedPreferences); 15 | 16 | Future getDisplayState() { 17 | return _service.getDisplayState(); 18 | } 19 | 20 | Future updateDisplayConfiguration(RemoteDisplayState configuration) { 21 | return _service.updateDisplayConfiguration(configuration); 22 | } 23 | 24 | Future isPrimaryDisplay() async { 25 | final state = await getDisplayState(); 26 | if (state.isRearDisplayEnabled == 1) { 27 | return _sharedPreferences.getBool(_isPrimaryDisplaySharedPreferencesKey); 28 | } else { 29 | return true; 30 | } 31 | } 32 | 33 | Future setDisplayType(bool isPrimaryDisplay) { 34 | return _sharedPreferences.setBool( 35 | _isPrimaryDisplaySharedPreferencesKey, 36 | isPrimaryDisplay, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/feature/settings/model/device_info.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'device_info.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | DeviceInfo _$DeviceInfoFromJson(Map json) => DeviceInfo( 10 | cpuTemperature: (json['cpu_temperature'] as num?)?.toInt() ?? 0, 11 | serialNumber: json['serial_number'] as String? ?? 'undefined', 12 | deviceModel: json['device_model'] as String? ?? 'undefined', 13 | isCarPlayDetected: (json['is_carplay_detected'] as num?)?.toInt() ?? 0, 14 | isModemDetected: (json['is_modem_detected'] as num?)?.toInt() ?? 0, 15 | releaseType: json['release_type'] as String? ?? 'undefined', 16 | otaUrl: json['ota_url'] as String? ?? 'undefined', 17 | isGPSEnabled: (json['is_gps_enabled'] as num?)?.toInt() ?? 0, 18 | ); 19 | 20 | Map _$DeviceInfoToJson(DeviceInfo instance) => 21 | { 22 | 'cpu_temperature': instance.cpuTemperature, 23 | 'serial_number': instance.serialNumber, 24 | 'device_model': instance.deviceModel, 25 | 'is_modem_detected': instance.isModemDetected, 26 | 'is_carplay_detected': instance.isCarPlayDetected, 27 | 'release_type': instance.releaseType, 28 | 'ota_url': instance.otaUrl, 29 | 'is_gps_enabled': instance.isGPSEnabled, 30 | }; 31 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/cubit/release_notes_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:tesla_android/feature/releaseNotes/cubit/release_notes_state.dart'; 4 | import 'package:tesla_android/feature/releaseNotes/model/changelog_item.dart'; 5 | import 'package:tesla_android/feature/releaseNotes/model/version.dart'; 6 | import 'package:tesla_android/feature/releaseNotes/repository/release_notes_repository.dart'; 7 | 8 | @injectable 9 | class ReleaseNotesCubit extends Cubit { 10 | final ReleaseNotesRepository _repository; 11 | 12 | ReleaseNotesCubit(this._repository) : super(ReleaseNotesStateInitial()); 13 | 14 | void loadReleaseNotes() async { 15 | emit(ReleaseNotesStateLoading()); 16 | try { 17 | final releaseNotes = await _repository.getReleaseNotes(); 18 | emit(ReleaseNotesStateLoaded(releaseNotes: releaseNotes)); 19 | } catch (error) { 20 | emit(ReleaseNotesStateUnavailable()); 21 | } 22 | } 23 | 24 | void updateSelection({ 25 | required Version version, 26 | required ChangelogItem changelogItem, 27 | }) { 28 | if (state is ReleaseNotesStateLoaded) { 29 | final releaseNotes = (state as ReleaseNotesStateLoaded).releaseNotes; 30 | emit(ReleaseNotesStateLoaded.withSelection( 31 | releaseNotes: releaseNotes, 32 | selectedVersion: version, 33 | selectedChangelogItem: changelogItem)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | import 'package:tesla_android/common/di/ta_locator.dart'; 4 | import 'package:tesla_android/common/navigation/ta_page_factory.dart'; 5 | import 'package:tesla_android/common/ui/constants/ta_colors.dart'; 6 | import 'package:tesla_android/common/utils/logger.dart'; 7 | 8 | Future main() async { 9 | await configureTADependencies(); 10 | 11 | runApp( 12 | TeslaAndroid(), 13 | ); 14 | } 15 | 16 | class TeslaAndroid extends StatelessWidget with Logger { 17 | TeslaAndroid({super.key}); 18 | 19 | final TAPageFactory _pageFactory = getIt(); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | MediaQueryData windowData = MediaQueryData.fromView(View.of(context)); 24 | 25 | return MediaQuery( 26 | data: windowData.copyWith(devicePixelRatio: 1.0, textScaler: const TextScaler.linear(1.5)), 27 | child: MaterialApp( 28 | debugShowCheckedModeBanner: false, 29 | navigatorKey: getIt>(), 30 | title: 'Tesla Android', 31 | theme: ThemeData( 32 | brightness: Brightness.light, 33 | useMaterial3: true, 34 | colorSchemeSeed: TAColors.settingsPrimaryColor, 35 | fontFamily: 'Roboto'), 36 | darkTheme: ThemeData( 37 | brightness: Brightness.dark, 38 | useMaterial3: true, 39 | colorSchemeSeed: TAColors.settingsPrimaryColor, 40 | fontFamily: 'Roboto'), 41 | themeMode: ThemeMode.system, 42 | initialRoute: _pageFactory.initialRoute, 43 | routes: _pageFactory.getRoutes(), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/feature/connectivityCheck/cubit/connectivity_check_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | import 'package:tesla_android/common/network/health_service.dart'; 6 | import 'package:tesla_android/feature/connectivityCheck/model/connectivity_state.dart'; 7 | import 'package:web/web.dart'; 8 | 9 | @injectable 10 | class ConnectivityCheckCubit extends Cubit { 11 | final HealthService _healthService; 12 | 13 | ConnectivityCheckCubit(this._healthService) 14 | : super(ConnectivityState.backendAccessible) { 15 | _observeBackendAccessibility(); 16 | } 17 | 18 | static const _standardCheckInterval = Duration(seconds: 30); 19 | static const _offlineCheckInterval = Duration(seconds: 5); 20 | 21 | void _observeBackendAccessibility() { 22 | checkConnectivity(); 23 | Timer.periodic(_standardCheckInterval, (timer) async { 24 | if (state == ConnectivityState.backendAccessible) checkConnectivity(); 25 | }); 26 | Timer.periodic(_offlineCheckInterval, (timer) async { 27 | if (state != ConnectivityState.backendAccessible) checkConnectivity(); 28 | }); 29 | } 30 | 31 | void checkConnectivity() async { 32 | try { 33 | await _healthService.getHealthCheck(); 34 | _onRequestSuccess(); 35 | } catch (_) { 36 | _onRequestFailure(); 37 | } 38 | } 39 | 40 | void _onRequestFailure() async { 41 | emit(ConnectivityState.backendUnreachable); 42 | } 43 | 44 | void _onRequestSuccess() { 45 | if (state == ConnectivityState.backendUnreachable) { 46 | window.location.reload(); 47 | } 48 | emit(ConnectivityState.backendAccessible); 49 | } 50 | } -------------------------------------------------------------------------------- /lib/common/di/app_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:flavor/flavor.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:injectable/injectable.dart' hide Environment; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | import 'package:web/web.dart'; 7 | 8 | @module 9 | abstract class AppModule { 10 | @singleton 11 | Flavor get provideFlavor { 12 | const defaultDomain = "device.teslaandroid.com"; 13 | final isLocalHost = window.location.hostname.contains("localhost"); 14 | final domain = isLocalHost ? defaultDomain : window.location.hostname; 15 | final isSSL = window.location.protocol.contains("https") || kDebugMode; 16 | final httpProtocol = isSSL ? "https://" : "http://"; 17 | final webSocketProtocol = isSSL ? "wss://" : "ws://"; 18 | return Flavor.create( 19 | isLocalHost ? Environment.dev : Environment.production, 20 | color: isLocalHost ? Colors.green : Colors.red, 21 | properties: { 22 | 'isSSL': isSSL, 23 | 'touchscreenWebSocket': '$webSocketProtocol$domain/sockets/touchscreen', 24 | 'gpsWebSocket': '$webSocketProtocol$domain/sockets/gps', 25 | 'audioWebSocket': '$webSocketProtocol$domain/sockets/audio', 26 | 'displayWebSocket': '$webSocketProtocol$domain/sockets/display', 27 | //'displayWebSocket': '$httpProtocol$domain/stream', 28 | 'configurationApiBaseUrl': '$httpProtocol$domain/api', 29 | }, 30 | ); 31 | } 32 | 33 | @singleton 34 | @preResolve 35 | Future get sharedPreferences => 36 | SharedPreferences.getInstance(); 37 | 38 | @lazySingleton 39 | GlobalKey get navigatorKey => GlobalKey(); 40 | } 41 | -------------------------------------------------------------------------------- /lib/feature/settings/model/softap_band_type.dart: -------------------------------------------------------------------------------- 1 | enum SoftApBandType { 2 | band2_4GHz( 3 | name: "2.4 GHz", 4 | band: 1, 5 | channel: 6, 6 | channelWidth: 2, 7 | ), 8 | band5GHz36( 9 | name: "5 GHZ - Channel 36", 10 | band: 2, 11 | channel: 36, 12 | channelWidth: 3, 13 | ), 14 | band5GHz44( 15 | name: "5 GHZ - Channel 44", 16 | band: 2, 17 | channel: 44, 18 | channelWidth: 3, 19 | ), 20 | band5GHz149( 21 | name: "5 GHZ - Channel 149", 22 | band: 2, 23 | channel: 149, 24 | channelWidth: 3, 25 | ), 26 | band5GHz157( 27 | name: "5 GHZ - Channel 157", 28 | band: 2, 29 | channel: 157, 30 | channelWidth: 3, 31 | ); 32 | 33 | const SoftApBandType({ 34 | required this.name, 35 | required this.band, 36 | required this.channel, 37 | required this.channelWidth, 38 | }); 39 | 40 | // packages/modules/Wifi/framework/java/android/net/wifi/SoftApConfiguration.java 41 | final int band; 42 | final int channel; 43 | 44 | // packages/modules/Wifi/framework/java/android/net/wifi/SoftApInfo.java 45 | final int channelWidth; 46 | final String name; 47 | 48 | static SoftApBandType matchBandTypeFromConfig( 49 | {required band, 50 | required channel, 51 | required channelWidth}) { 52 | if (band == 1) { 53 | return SoftApBandType.band2_4GHz; 54 | } 55 | switch (channel) { 56 | case 36: 57 | return SoftApBandType.band5GHz36; 58 | case 44: 59 | return SoftApBandType.band5GHz44; 60 | case 149: 61 | return SoftApBandType.band5GHz149; 62 | case 157: 63 | return SoftApBandType.band5GHz157; 64 | default: 65 | return SoftApBandType.band5GHz36; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/feature/donations/widget/donation_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:qr_flutter/qr_flutter.dart'; 3 | import 'package:tesla_android/common/navigation/ta_page.dart'; 4 | import 'package:tesla_android/common/ui/components/ta_app_bar.dart'; 5 | import 'package:tesla_android/common/ui/components/ta_bottom_navigation_bar.dart'; 6 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 7 | 8 | class DonationPage extends StatelessWidget { 9 | const DonationPage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: TaAppBar( 15 | title: TAPage.donations.title, 16 | ), 17 | bottomNavigationBar: const TaBottomNavigationBar(currentIndex: 3), 18 | body: Center( 19 | child: Column( 20 | mainAxisAlignment: MainAxisAlignment.center, 21 | children: [ 22 | const SizedBox( 23 | width: TADimens.donationTextWidth, 24 | child: Text( 25 | "Thank you for considering a donation to support the ongoing development of Tesla Android. As a community-founded project, your contribution can truly make a difference!", 26 | textAlign: TextAlign.center, 27 | ), 28 | ), 29 | const SizedBox( 30 | height: TADimens.baseContentMargin, 31 | ), 32 | SizedBox( 33 | width: TADimens.donationQRSize, 34 | height: TADimens.donationQRSize, 35 | child: QrImageView( 36 | data: "https://teslaandroid.com/donations", 37 | version: QrVersions.auto, 38 | ) 39 | ), 40 | ], 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/feature/settings/bloc/gps_configuration_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:tesla_android/common/utils/logger.dart'; 4 | import 'package:tesla_android/feature/settings/bloc/gps_configuration_state.dart'; 5 | import 'package:tesla_android/feature/settings/repository/system_configuration_repository.dart'; 6 | 7 | @injectable 8 | class GPSConfigurationCubit extends Cubit with Logger { 9 | final SystemConfigurationRepository _configurationRepository; 10 | 11 | GPSConfigurationCubit(this._configurationRepository) 12 | : super(GPSConfigurationStateInitial()); 13 | 14 | Future fetchConfiguration() async { 15 | if (!isClosed) emit(GPSConfigurationStateLoading()); 16 | try { 17 | final configuration = await _configurationRepository.getConfiguration(); 18 | emit( 19 | GPSConfigurationStateLoaded( 20 | isGPSEnabled: configuration.isGPSEnabled == 1), 21 | ); 22 | } catch (exception, stacktrace) { 23 | logException( 24 | exception: exception, 25 | stackTrace: stacktrace, 26 | ); 27 | if (!isClosed) { 28 | emit( 29 | GPSConfigurationStateError(), 30 | ); 31 | } 32 | } 33 | } 34 | 35 | void setState(bool newState) async { 36 | emit(GPSConfigurationStateUpdateInProgress(isGPSEnabled: newState)); 37 | try { 38 | await _configurationRepository.setGPSState(newState == true ? 1 : 0); 39 | emit(GPSConfigurationStateLoaded(isGPSEnabled: newState)); 40 | } catch (exception, stacktrace) { 41 | logException( 42 | exception: exception, 43 | stackTrace: stacktrace, 44 | ); 45 | emit(GPSConfigurationStateError()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /web/audio-ws-worker.js: -------------------------------------------------------------------------------- 1 | let ws = null; 2 | let want = false; 3 | let currentUrl = ""; 4 | let backoff = 1000; 5 | const MAX_BACKOFF = 10000; 6 | 7 | function connect() { 8 | if (!want || !currentUrl) return; 9 | 10 | try { 11 | ws = new WebSocket(currentUrl); 12 | ws.binaryType = "arraybuffer"; 13 | 14 | ws.onopen = () => { 15 | backoff = 1000; 16 | postMessage({ type: "open" }); 17 | }; 18 | 19 | ws.onmessage = (ev) => { 20 | if (ev.data instanceof ArrayBuffer) { 21 | postMessage({ type: "data", buf: ev.data }, [ev.data]); 22 | } else if (ev.data instanceof Blob) { 23 | ev.data.arrayBuffer().then((ab) => { 24 | postMessage({ type: "data", buf: ab }, [ab]); 25 | }).catch((e) => { 26 | postMessage({ type: "error", message: e && e.message ? e.message : "blob->arrayBuffer failed" }); 27 | }); 28 | } 29 | }; 30 | 31 | ws.onerror = () => { 32 | postMessage({ type: "error", message: "ws error" }); 33 | }; 34 | 35 | ws.onclose = () => { 36 | postMessage({ type: "close" }); 37 | scheduleReconnect(); 38 | }; 39 | } catch (e) { 40 | postMessage({ type: "error", message: e && e.message ? e.message : "ws create failed" }); 41 | scheduleReconnect(); 42 | } 43 | } 44 | 45 | function scheduleReconnect() { 46 | if (!want) return; 47 | const delay = backoff; 48 | backoff = Math.min(MAX_BACKOFF, backoff * 2); 49 | setTimeout(connect, delay); 50 | } 51 | 52 | self.onmessage = (e) => { 53 | const { type, url } = e.data || {}; 54 | if (type === "start" && url) { 55 | want = true; 56 | currentUrl = url; 57 | backoff = 1000; 58 | connect(); 59 | } else if (type === "stop") { 60 | want = false; 61 | if (ws) { 62 | try { ws.close(); } catch {} 63 | ws = null; 64 | } 65 | } 66 | }; -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | tesla_android_automotive 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/widget/list/release_notes_versions_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 3 | import 'package:tesla_android/feature/releaseNotes/model/changelog_item.dart'; 4 | import 'package:tesla_android/feature/releaseNotes/model/version.dart'; 5 | import 'package:tesla_android/feature/releaseNotes/widget/card/release_notes_changelog_item_card.dart'; 6 | 7 | class ReleaseNotesVersionList extends StatelessWidget { 8 | final List versions; 9 | final Version selectedVersion; 10 | final ChangelogItem selectedChangelogItem; 11 | 12 | const ReleaseNotesVersionList({ 13 | super.key, 14 | required this.versions, 15 | required this.selectedVersion, 16 | required this.selectedChangelogItem, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return ListView.builder( 22 | padding: TADimens.basePaddingHorizontal, 23 | itemCount: versions.length, 24 | itemBuilder: _versionItemBuilder, 25 | ); 26 | } 27 | 28 | Widget _versionItemBuilder( 29 | BuildContext context, 30 | int index, 31 | ) { 32 | final version = versions[index]; 33 | final textTheme = Theme.of(context).textTheme; 34 | return Column( 35 | mainAxisAlignment: MainAxisAlignment.start, 36 | crossAxisAlignment: CrossAxisAlignment.start, 37 | children: [ 38 | Padding( 39 | padding: TADimens.basePaddingVertical, 40 | child: Text( 41 | version.versionName, 42 | style: textTheme.titleMedium, 43 | ), 44 | ), 45 | ...version.changelogItems.map( 46 | (e) => ReleaseNotesChangelogItemCard( 47 | changelogItem: e, 48 | version: version, 49 | isActive: e == selectedChangelogItem, 50 | ), 51 | ), 52 | ], 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/common/ui/components/ta_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TABottomSheet extends StatelessWidget { 4 | final String title; 5 | final Widget body; 6 | 7 | const TABottomSheet({ 8 | super.key, 9 | required this.title, 10 | required this.body, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return FractionallySizedBox( 16 | heightFactor: 0.95, 17 | child: Scaffold( 18 | body: Column( 19 | children: [ 20 | _dismissHandle(), 21 | _title(context), 22 | Expanded(child: _content()), 23 | ], 24 | ), 25 | ), 26 | ); 27 | } 28 | 29 | Widget _dismissHandle() { 30 | return Padding( 31 | padding: const EdgeInsets.symmetric(vertical: 15), 32 | child: Center( 33 | child: Container( 34 | width: 100, 35 | height: 4, 36 | color: Colors.grey.shade500, 37 | )), 38 | ); 39 | } 40 | 41 | Widget _title(BuildContext context) { 42 | return SizedBox( 43 | width: double.infinity, 44 | child: Stack( 45 | children: [ 46 | Padding( 47 | padding: const EdgeInsets.symmetric( 48 | vertical: 40, 49 | ), 50 | child: Center( 51 | child: Text( 52 | title, 53 | style: Theme.of(context).textTheme.titleLarge, 54 | ), 55 | )), 56 | Positioned( 57 | left: 0, 58 | bottom: 0, 59 | right: 0, 60 | child: Container( 61 | height: 1, 62 | color: Colors.grey.shade500, 63 | )) 64 | ], 65 | ), 66 | ); 67 | } 68 | 69 | Widget _content() { 70 | return Container( 71 | padding: const EdgeInsets.symmetric(vertical: 15), 72 | child: body, 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/feature/settings/repository/system_configuration_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:injectable/injectable.dart'; 2 | import 'package:tesla_android/common/network/configuration_service.dart'; 3 | import 'package:tesla_android/feature/settings/model/system_configuration_response_body.dart'; 4 | 5 | @injectable 6 | class SystemConfigurationRepository { 7 | final ConfigurationService _configurationService; 8 | 9 | SystemConfigurationRepository(this._configurationService); 10 | 11 | Future getConfiguration() { 12 | return _configurationService.getConfiguration(); 13 | } 14 | 15 | Future setSoftApBand(int band) { 16 | return _configurationService.setSoftApBand(band); 17 | } 18 | 19 | Future setSoftApChannel(int channel) { 20 | return _configurationService.setSoftApChannel(channel); 21 | } 22 | 23 | Future setSoftApChannelWidth(int channelWidth) { 24 | return _configurationService.setSoftApChannelWidth(channelWidth); 25 | } 26 | 27 | Future setSoftApState(int isEnabledFlag) { 28 | return _configurationService.setSoftApState(isEnabledFlag); 29 | } 30 | 31 | Future setOfflineModeState(int isEnabledFlag) { 32 | return _configurationService.setOfflineModeState(isEnabledFlag); 33 | } 34 | 35 | Future setOfflineModeTelemetryState(int isEnabledFlag) { 36 | return _configurationService.setOfflineModeTelemetryState(isEnabledFlag); 37 | } 38 | 39 | Future setOfflineModeTeslaFirmwareDownloads(int isEnabledFlag) { 40 | return _configurationService 41 | .setOfflineModeTeslaFirmwareDownloads(isEnabledFlag); 42 | } 43 | 44 | Future setBrowserAudioState(int isEnabledFlag) { 45 | return _configurationService.setBrowserAudioState(isEnabledFlag); 46 | } 47 | 48 | Future setBrowserAudioVolume(int volume) { 49 | return _configurationService.setBrowserAudioVolume(volume); 50 | } 51 | 52 | Future setGPSState(int isEnabled) { 53 | return _configurationService.setGPSState(isEnabled); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/common/ui/components/ta_bottom_navigation_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tesla_android/common/navigation/ta_navigator.dart'; 3 | import 'package:tesla_android/common/navigation/ta_page.dart'; 4 | import 'package:tesla_android/common/utils/logger.dart'; 5 | 6 | class TaBottomNavigationBar extends StatelessWidget with Logger { 7 | final int currentIndex; 8 | 9 | const TaBottomNavigationBar({super.key, required this.currentIndex}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return BottomNavigationBar( 14 | type: BottomNavigationBarType.fixed, 15 | items: const [ 16 | BottomNavigationBarItem( 17 | icon: Icon(Icons.android), 18 | label: 'Android OS', 19 | ), 20 | BottomNavigationBarItem( 21 | icon: Icon(Icons.info_outlined), 22 | label: 'About', 23 | ), 24 | BottomNavigationBarItem( 25 | icon: Icon(Icons.notes), 26 | label: 'Release Notes', 27 | ), 28 | BottomNavigationBarItem( 29 | icon: Icon(Icons.monetization_on), label: "Donations"), 30 | BottomNavigationBarItem( 31 | icon: Icon(Icons.settings), 32 | label: 'Settings', 33 | ), 34 | ], 35 | currentIndex: currentIndex, 36 | onTap: (index) { 37 | final page = _getPageForIndex(index); 38 | if(page == null) { 39 | return; 40 | } 41 | TANavigator.pushReplacement( 42 | context: context, page: page, animated: index == 0); 43 | }, 44 | ); 45 | } 46 | 47 | TAPage? _getPageForIndex(int index) { 48 | switch (index) { 49 | case 0: 50 | return TAPage.home; 51 | case 1: 52 | return TAPage.about; 53 | case 2: 54 | return TAPage.releaseNotes; 55 | case 3: 56 | return TAPage.donations; 57 | case 4: 58 | return TAPage.settings; 59 | default: 60 | return null; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/common/navigation/ta_navigator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tesla_android/common/di/ta_locator.dart'; 3 | import 'package:tesla_android/common/navigation/ta_page.dart'; 4 | import 'package:tesla_android/common/navigation/ta_page_factory.dart'; 5 | import 'package:tesla_android/common/navigation/ta_page_type.dart'; 6 | 7 | class TANavigator { 8 | static TAPageFactory get _pageFactory => getIt(); 9 | 10 | static Future push({ 11 | required BuildContext context, 12 | required TAPage page, 13 | }) { 14 | switch (page.type) { 15 | case TAPageType.standard: 16 | return Navigator.of(context).pushNamed(page.route); 17 | case TAPageType.dialog: 18 | return _pushDialog(context: context, page: page); 19 | } 20 | } 21 | 22 | static Future _pushDialog({ 23 | required BuildContext context, 24 | required TAPage page, 25 | }) { 26 | return showDialog( 27 | context: context, 28 | builder: (context) { 29 | return _pageFactory.buildPage(page).call(context); 30 | }, 31 | ); 32 | } 33 | 34 | static void pushReplacement( 35 | {required BuildContext context, 36 | required TAPage page, 37 | bool animated = true}) { 38 | if (page.type != TAPageType.standard) { 39 | throw UnsupportedError( 40 | "only regular pages can be used in pushReplacement"); 41 | } 42 | if (animated) { 43 | Navigator.of(context).pushReplacementNamed(page.route); 44 | } else { 45 | Navigator.of(context).pushReplacement( 46 | PageRouteBuilder( 47 | pageBuilder: (context, animation1, animation2) { 48 | return getIt().buildPage(page).call(context); 49 | }, 50 | transitionDuration: Duration.zero, 51 | reverseTransitionDuration: Duration.zero, 52 | settings: RouteSettings(name: page.route) 53 | ), 54 | ); 55 | } 56 | } 57 | 58 | static void pop({ 59 | required BuildContext context, 60 | }) { 61 | Navigator.of(context).pop(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/feature/settings/model/device_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'device_info.g.dart'; 5 | 6 | @JsonSerializable() 7 | class DeviceInfo extends Equatable { 8 | @JsonKey(name: "cpu_temperature", defaultValue: 0) 9 | final int cpuTemperature; 10 | @JsonKey(name: "serial_number", defaultValue: "undefined") 11 | final String serialNumber; 12 | @JsonKey(name: "device_model", defaultValue: "undefined") 13 | final String deviceModel; 14 | @JsonKey(name: "is_modem_detected", defaultValue: 0) 15 | final int isModemDetected; 16 | @JsonKey(name: "is_carplay_detected", defaultValue: 0) 17 | final int isCarPlayDetected; 18 | @JsonKey(name: "release_type", defaultValue: "undefined") 19 | final String releaseType; 20 | @JsonKey(name: "ota_url", defaultValue: "undefined") 21 | final String otaUrl; 22 | @JsonKey(name: "is_gps_enabled", defaultValue: 0) 23 | final int isGPSEnabled; 24 | 25 | const DeviceInfo({ 26 | required this.cpuTemperature, 27 | required this.serialNumber, 28 | required this.deviceModel, 29 | required this.isCarPlayDetected, 30 | required this.isModemDetected, 31 | required this.releaseType, 32 | required this.otaUrl, 33 | required this.isGPSEnabled 34 | }); 35 | 36 | factory DeviceInfo.fromJson(Map json) => 37 | _$DeviceInfoFromJson(json); 38 | 39 | Map toJson() => _$DeviceInfoToJson(this); 40 | 41 | @override 42 | List get props => [ 43 | cpuTemperature, 44 | serialNumber, 45 | deviceModel, 46 | isModemDetected, 47 | isCarPlayDetected, 48 | releaseType, 49 | otaUrl, 50 | isGPSEnabled, 51 | ]; 52 | } 53 | 54 | extension DeviceNameExtension on DeviceInfo { 55 | String get deviceName { 56 | if (deviceModel == "rpi4") { 57 | return "Raspberry Pi 4"; 58 | } else if (deviceModel == "cm4") { 59 | return "Compute Module 4"; 60 | } else { 61 | return "UNOFFICIAL $deviceModel"; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 30 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "eu.gapinski.tesla_android_automotive" 47 | minSdkVersion 16 48 | targetSdkVersion 30 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies { 67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 68 | } 69 | -------------------------------------------------------------------------------- /lib/feature/about/about_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:package_info_plus/package_info_plus.dart'; 3 | import 'package:tesla_android/common/navigation/ta_page.dart'; 4 | import 'package:tesla_android/common/ui/components/ta_app_bar.dart'; 5 | import 'package:tesla_android/common/ui/components/ta_bottom_navigation_bar.dart'; 6 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 7 | 8 | class AboutPage extends StatelessWidget { 9 | const AboutPage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: TaAppBar( 15 | title: TAPage.about.title, 16 | ), 17 | body: Center( 18 | child: Column( 19 | crossAxisAlignment: CrossAxisAlignment.center, 20 | mainAxisAlignment: MainAxisAlignment.center, 21 | children: [ 22 | SizedBox( 23 | width: TADimens.splashPageLogoHeight, 24 | height: TADimens.splashPageLogoHeight, 25 | child: ClipRRect( 26 | borderRadius: BorderRadius.circular(TADimens.splashPageRadius), 27 | child: Image.asset("images/png/tesla-android-logo.png"), 28 | ), 29 | ), 30 | const SizedBox( 31 | height: TADimens.PADDING_VALUE, 32 | ), 33 | const Text("Version", style: TextStyle(fontWeight: FontWeight.bold)), 34 | const SizedBox( 35 | height: TADimens.PADDING_S_VALUE, 36 | ), 37 | FutureBuilder(future: PackageInfo.fromPlatform(), builder: (_, snapshot){ 38 | return Text(snapshot.data?.version ?? ''); 39 | }), 40 | const SizedBox( 41 | height: TADimens.PADDING_XXL_VALUE, 42 | ), 43 | const Text("Tesla Android (2nd generation) hardware is now available.\nRetrofits are available for 1st generation devices.\nMore details on https://teslaandroid.com", style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center,), 44 | ], 45 | ), 46 | ), 47 | bottomNavigationBar: const TaBottomNavigationBar(currentIndex: 1)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 13 | 17 | 21 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/feature/touchscreen/touchscreen_view.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:tesla_android/feature/touchscreen/cubit/touchscreen_cubit.dart'; 5 | 6 | class TouchScreenView extends StatelessWidget { 7 | final Size displaySize; 8 | 9 | const TouchScreenView({ 10 | super.key, 11 | required this.displaySize, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final touchScreenCubit = BlocProvider.of(context); 17 | return LayoutBuilder(builder: (context, constraints) { 18 | return Listener( 19 | onPointerDown: (event) { 20 | _handlePointerEvent( 21 | cubit: touchScreenCubit, 22 | event: event, 23 | constraints: constraints, 24 | touchscreenSize: displaySize, 25 | ); 26 | }, 27 | onPointerMove: (event) { 28 | _handlePointerEvent( 29 | cubit: touchScreenCubit, 30 | event: event, 31 | constraints: constraints, 32 | touchscreenSize: displaySize, 33 | ); 34 | }, 35 | onPointerCancel: (event) { 36 | _handlePointerEvent( 37 | cubit: touchScreenCubit, 38 | event: event, 39 | constraints: constraints, 40 | touchscreenSize: displaySize, 41 | ); 42 | }, 43 | onPointerUp: (event) { 44 | _handlePointerEvent( 45 | cubit: touchScreenCubit, 46 | event: event, 47 | constraints: constraints, 48 | touchscreenSize: displaySize, 49 | ); 50 | }, 51 | child: Container(color: Colors.transparent,), 52 | ); 53 | }); 54 | } 55 | 56 | void _handlePointerEvent({ 57 | required TouchscreenCubit cubit, 58 | required BoxConstraints constraints, 59 | required Size touchscreenSize, 60 | required dynamic event, 61 | }) { 62 | if (event is PointerDownEvent) { 63 | cubit.handlePointerDownEvent(event, constraints, touchscreenSize); 64 | } else if (event is PointerMoveEvent) { 65 | cubit.handlePointerMoveEvent(event, constraints, touchscreenSize); 66 | } else if (event is PointerCancelEvent || event is PointerUpEvent) { 67 | cubit.handlePointerUpEvent(event, constraints); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /lib/common/network/health_service.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'health_service.dart'; 4 | 5 | // dart format off 6 | 7 | // ************************************************************************** 8 | // RetrofitGenerator 9 | // ************************************************************************** 10 | 11 | // ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter 12 | 13 | class _HealthService implements HealthService { 14 | _HealthService(this._dio, {this.baseUrl, this.errorLogger}); 15 | 16 | final Dio _dio; 17 | 18 | String? baseUrl; 19 | 20 | final ParseErrorLogger? errorLogger; 21 | 22 | @override 23 | Future getHealthCheck() async { 24 | final _extra = {}; 25 | final queryParameters = {}; 26 | final _headers = {}; 27 | const Map? _data = null; 28 | final _options = _setStreamType( 29 | Options(method: 'GET', headers: _headers, extra: _extra) 30 | .compose( 31 | _dio.options, 32 | '/health', 33 | queryParameters: queryParameters, 34 | data: _data, 35 | ) 36 | .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), 37 | ); 38 | final _result = await _dio.fetch(_options); 39 | final _value = _result.data; 40 | return _value; 41 | } 42 | 43 | RequestOptions _setStreamType(RequestOptions requestOptions) { 44 | if (T != dynamic && 45 | !(requestOptions.responseType == ResponseType.bytes || 46 | requestOptions.responseType == ResponseType.stream)) { 47 | if (T == String) { 48 | requestOptions.responseType = ResponseType.plain; 49 | } else { 50 | requestOptions.responseType = ResponseType.json; 51 | } 52 | } 53 | return requestOptions; 54 | } 55 | 56 | String _combineBaseUrls(String dioBaseUrl, String? baseUrl) { 57 | if (baseUrl == null || baseUrl.trim().isEmpty) { 58 | return dioBaseUrl; 59 | } 60 | 61 | final url = Uri.parse(baseUrl); 62 | 63 | if (url.isAbsolute) { 64 | return url.toString(); 65 | } 66 | 67 | return Uri.parse(dioBaseUrl).resolveUri(url).toString(); 68 | } 69 | } 70 | 71 | // dart format on 72 | -------------------------------------------------------------------------------- /web/estimator.js: -------------------------------------------------------------------------------- 1 | class GpsEstimator { 2 | constructor() { 3 | this._lastLatitude = 0; 4 | this._lastLongitude = 0; 5 | this._lastTime = 0; 6 | this._lastHeading = 0; 7 | } 8 | 9 | estimate(source) { 10 | const latitude = source.latitude; 11 | const longitude = source.longitude; 12 | const updateTime = source.timestamp 13 | 14 | // Lat and Lon are in degrees, so first go to radians 15 | const lon1 = this._degToRad(longitude); 16 | const lon2 = this._degToRad(this._lastLongitude); 17 | const lat1 = this._degToRad(latitude); 18 | const lat2 = this._degToRad(this._lastLatitude); 19 | 20 | // Bearing 21 | // At low speeds <1m/s this is probably wrong due to jitter in the GPS signal. 22 | // Possibly calculate second only if speed is faster 23 | const diffLon = this._degToRad(longitude - this._lastLongitude); 24 | const x = Math.cos(lat1) * Math.sin(diffLon); 25 | const y = 26 | Math.cos(lat2) * Math.sin(lat1) - Math.sin(lat2) * Math.cos(lat1) * Math.cos(diffLon); 27 | const r = Math.atan2(x, y); 28 | const heading = this._radToDeg(r); 29 | 30 | // Straight line distance 31 | // There are about a dozen variants of this, all produce slightly 32 | // different results, some don't even work... this one seems pretty accurate 33 | const earthRadius = 6378137.0; 34 | const dLat = Math.sin(lat2 - lat1) / 2; 35 | const dLon = Math.sin(lon2 - lon1) / 2; 36 | const flatDistance = 37 | 2000 * earthRadius * Math.asin(Math.sqrt(dLat * dLat + Math.cos(lat1) * Math.cos(lat2) * dLon * dLon)); 38 | 39 | // Corner/Bearing compensation 40 | const change = Math.abs(this._lastHeading - heading); 41 | const theta = this._degToRad(change); 42 | const distance = flatDistance / Math.cos(theta); 43 | 44 | const speed = distance / (updateTime - this._lastTime); 45 | 46 | this._lastLatitude = latitude; 47 | this._lastLongitude = longitude; 48 | this._lastTime = updateTime; 49 | this._lastHeading = heading; 50 | 51 | return { 52 | speed: speed, 53 | heading: heading, 54 | }; 55 | } 56 | 57 | _radToDeg(radians) { 58 | const deg = (radians * 180) / Math.PI; 59 | return deg >= 0 ? deg : 360 + deg; 60 | } 61 | 62 | _degToRad(degrees) { 63 | return (degrees * Math.PI) / 180; 64 | } 65 | } -------------------------------------------------------------------------------- /lib/feature/home/widget/audio_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 4 | import 'package:tesla_android/common/utils/audio_api.dart'; 5 | import 'package:tesla_android/common/utils/logger.dart'; 6 | import 'package:tesla_android/feature/settings/bloc/audio_configuration_cubit.dart'; 7 | 8 | import '../../settings/bloc/audio_configuration_state.dart'; 9 | 10 | class AudioButton extends StatefulWidget { 11 | const AudioButton({super.key}); 12 | 13 | @override 14 | State createState() => _AudioButtonState(); 15 | } 16 | 17 | class _AudioButtonState extends State with Logger { 18 | String _state = 'stopped'; 19 | VoidCallback? _removeListener; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | _readInitialState(); 25 | _removeListener = addAudioStateListener((s) { 26 | if (!mounted) return; 27 | setState(() => _state = s); 28 | }); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | _removeListener?.call(); 34 | super.dispose(); 35 | } 36 | 37 | void _readInitialState() { 38 | try { 39 | _state = getAudioState(); 40 | } catch (_) { 41 | _state = 'stopped'; 42 | } 43 | } 44 | 45 | void _onPressed() { 46 | if (_state == 'playing') { 47 | stopAudio(); 48 | setState(() => _state = 'stopped'); 49 | } else { 50 | startAudioFromGesture(); 51 | setState(() => _state = 'playing'); 52 | } 53 | } 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | final bool isPlaying = _state == 'playing'; 58 | 59 | return BlocBuilder( 60 | bloc: BlocProvider.of(context) 61 | ..fetchConfiguration(), 62 | builder: (context, audioState) { 63 | if (audioState is AudioConfigurationStateSettingsFetched && audioState.isEnabled) { 64 | return IconButton( 65 | color: Colors.white, 66 | onPressed: _onPressed, 67 | icon: Icon( 68 | isPlaying ? Icons.volume_up : Icons.volume_off, 69 | size: TADimens.statusBarIconSize, 70 | ), 71 | ); 72 | } else { 73 | return SizedBox(); 74 | } 75 | }, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/feature/display/cubit/display_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | import 'package:tesla_android/feature/display/model/remote_display_state.dart'; 4 | 5 | abstract class DisplayState extends Equatable { 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class DisplayStateInitial extends DisplayState {} 11 | 12 | class DisplayStateResizeCoolDown extends DisplayState { 13 | final Size viewSize; 14 | final Size adjustedSize; 15 | final DisplayResolutionModePreset resolutionPreset; 16 | final DisplayRendererType rendererType; 17 | final DateTime timestamp; 18 | final bool isH264; 19 | final bool isResponsive; 20 | final DisplayRefreshRatePreset refreshRate; 21 | final DisplayQualityPreset quality; 22 | final bool isRearDisplayEnabled; 23 | final bool isRearDisplayPrioritised; 24 | final bool isPrimaryDisplay; 25 | 26 | DisplayStateResizeCoolDown({ 27 | required this.viewSize, 28 | required this.adjustedSize, 29 | required this.resolutionPreset, 30 | required this.rendererType, 31 | required this.isH264, 32 | required this.isResponsive, 33 | required this.refreshRate, 34 | required this.quality, 35 | required this.isRearDisplayEnabled, 36 | required this.isRearDisplayPrioritised, 37 | required this.isPrimaryDisplay, 38 | }) : timestamp = DateTime.now(); 39 | 40 | @override 41 | List get props => [ 42 | viewSize, 43 | adjustedSize, 44 | timestamp, 45 | resolutionPreset, 46 | rendererType, 47 | isResponsive, 48 | isH264, 49 | refreshRate, 50 | quality, 51 | isRearDisplayEnabled, 52 | isRearDisplayPrioritised, 53 | isPrimaryDisplay, 54 | ]; 55 | } 56 | 57 | class DisplayStateDisplayTypeSelectionTriggered extends DisplayState {} 58 | 59 | class DisplayStateDisplayTypeSelectionFinished extends DisplayState {} 60 | 61 | class DisplayStateResizeInProgress extends DisplayState { 62 | DisplayStateResizeInProgress(); 63 | } 64 | 65 | class DisplayStateNormal extends DisplayState { 66 | final Size viewSize; 67 | final Size adjustedSize; 68 | final DisplayRendererType rendererType; 69 | 70 | DisplayStateNormal({ 71 | required this.viewSize, 72 | required this.adjustedSize, 73 | required this.rendererType, 74 | }); 75 | 76 | @override 77 | List get props => [viewSize, adjustedSize, rendererType]; 78 | } 79 | -------------------------------------------------------------------------------- /lib/common/network/configuration_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart' hide Headers; 2 | import 'package:flavor/flavor.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | import 'package:retrofit/retrofit.dart'; 5 | import 'package:tesla_android/feature/settings/model/system_configuration_response_body.dart'; 6 | 7 | part 'configuration_service.g.dart'; 8 | 9 | @injectable 10 | @RestApi() 11 | abstract class ConfigurationService { 12 | @factoryMethod 13 | factory ConfigurationService( 14 | Dio dio, 15 | Flavor flavor, 16 | ) => 17 | _ConfigurationService( 18 | dio, 19 | baseUrl: flavor.getString("configurationApiBaseUrl"), 20 | ); 21 | 22 | @GET("/configuration") 23 | @DioResponseType(ResponseType.json) 24 | Future getConfiguration(); 25 | 26 | @POST("/softApBand") 27 | @Headers({ 28 | "Content-Type": "text/plain", 29 | }) 30 | Future setSoftApBand(@Body() int band); 31 | 32 | @POST("/softApChannel") 33 | @Headers({ 34 | "Content-Type": "text/plain", 35 | }) 36 | Future setSoftApChannel(@Body() int channel); 37 | 38 | @POST("/softApChannelWidth") 39 | @Headers({ 40 | "Content-Type": "text/plain", 41 | }) 42 | Future setSoftApChannelWidth(@Body() int channelWidth); 43 | 44 | @POST("/softApState") 45 | @Headers({ 46 | "Content-Type": "text/plain", 47 | }) 48 | Future setSoftApState(@Body() int isEnabledFlag); 49 | 50 | @POST("/offlineModeState") 51 | @Headers({ 52 | "Content-Type": "text/plain", 53 | }) 54 | Future setOfflineModeState(@Body() int isEnabledFlag); 55 | 56 | @POST("/offlineModeTelemetryState") 57 | @Headers({ 58 | "Content-Type": "text/plain", 59 | }) 60 | Future setOfflineModeTelemetryState(@Body() int isEnabledFlag); 61 | 62 | @POST("/offlineModeTeslaFirmwareDownloads") 63 | @Headers({ 64 | "Content-Type": "text/plain", 65 | }) 66 | Future setOfflineModeTeslaFirmwareDownloads(@Body() int isEnabledFlag); 67 | 68 | @POST("/browserAudioState") 69 | @Headers({ 70 | "Content-Type": "text/plain", 71 | }) 72 | Future setBrowserAudioState(@Body() int isEnabledFlag); 73 | 74 | @POST("/browserAudioVolume") 75 | @Headers({ 76 | "Content-Type": "text/plain", 77 | }) 78 | Future setBrowserAudioVolume(@Body() int volume); 79 | 80 | @POST("/gpsState") 81 | @Headers({ 82 | "Content-Type": "text/plain", 83 | }) 84 | Future setGPSState(@Body() int state); 85 | } 86 | -------------------------------------------------------------------------------- /lib/feature/settings/model/system_configuration_response_body.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:tesla_android/feature/settings/model/softap_band_type.dart'; 3 | 4 | part 'system_configuration_response_body.g.dart'; 5 | 6 | @JsonSerializable() 7 | class SystemConfigurationResponseBody { 8 | @JsonKey(name: "persist.tesla-android.softap.band_type") 9 | final int bandType; 10 | @JsonKey(name: "persist.tesla-android.softap.channel") 11 | final int channel; 12 | @JsonKey(name: "persist.tesla-android.softap.channel_width") 13 | final int channelWidth; 14 | @JsonKey(name: "persist.tesla-android.softap.is_enabled") 15 | final int isEnabledFlag; 16 | @JsonKey(name: "persist.tesla-android.offline-mode.is_enabled") 17 | final int isOfflineModeEnabledFlag; 18 | @JsonKey(name: "persist.tesla-android.offline-mode.telemetry.is_enabled") 19 | final int isOfflineModeTelemetryEnabledFlag; 20 | @JsonKey(name: "persist.tesla-android.offline-mode.tesla-firmware-downloads") 21 | final int isOfflineModeTeslaFirmwareDownloadsEnabledFlag; 22 | @JsonKey( 23 | name: "persist.tesla-android.browser_audio.is_enabled", 24 | defaultValue: 1, 25 | ) 26 | final int browserAudioIsEnabled; 27 | @JsonKey( 28 | name: "persist.tesla-android.browser_audio.volume", 29 | defaultValue: 100, 30 | ) 31 | final int browserAudioVolume; 32 | @JsonKey( 33 | name: "persist.tesla-android.gps.is_active", 34 | defaultValue: 1, 35 | ) 36 | final int isGPSEnabled; 37 | 38 | SystemConfigurationResponseBody({ 39 | required this.bandType, 40 | required this.channel, 41 | required this.channelWidth, 42 | required this.isEnabledFlag, 43 | required this.isOfflineModeEnabledFlag, 44 | required this.isOfflineModeTelemetryEnabledFlag, 45 | required this.isOfflineModeTeslaFirmwareDownloadsEnabledFlag, 46 | required this.browserAudioIsEnabled, 47 | required this.browserAudioVolume, 48 | required this.isGPSEnabled, 49 | }); 50 | 51 | factory SystemConfigurationResponseBody.fromJson(Map json) => 52 | _$SystemConfigurationResponseBodyFromJson(json); 53 | 54 | Map toJson() => 55 | _$SystemConfigurationResponseBodyToJson(this); 56 | 57 | SoftApBandType get currentSoftApBandType => 58 | SoftApBandType.matchBandTypeFromConfig( 59 | band: bandType, 60 | channel: channel, 61 | channelWidth: channelWidth, 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/widget/card/release_notes_changelog_item_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 4 | import 'package:tesla_android/feature/releaseNotes/cubit/release_notes_cubit.dart'; 5 | import 'package:tesla_android/feature/releaseNotes/model/changelog_item.dart'; 6 | import 'package:tesla_android/feature/releaseNotes/model/version.dart'; 7 | 8 | class ReleaseNotesChangelogItemCard extends StatelessWidget { 9 | final ChangelogItem changelogItem; 10 | final Version version; 11 | final bool isActive; 12 | 13 | const ReleaseNotesChangelogItemCard({ 14 | super.key, 15 | required this.version, 16 | required this.changelogItem, 17 | required this.isActive, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final theme = Theme.of(context); 23 | final textTheme = theme.textTheme; 24 | final cubit = BlocProvider.of(context); 25 | return Card( 26 | color: _getCardColor(context), 27 | child: InkWell( 28 | onTap: () => cubit.updateSelection( 29 | version: version, 30 | changelogItem: changelogItem, 31 | ), 32 | child: Container( 33 | margin: EdgeInsets.zero, 34 | padding: TADimens.halfBasePadding, 35 | width: double.infinity, 36 | child: Column( 37 | mainAxisAlignment: MainAxisAlignment.start, 38 | crossAxisAlignment: CrossAxisAlignment.start, 39 | children: [ 40 | Text(changelogItem.title, style: textTheme.labelLarge), 41 | const SizedBox( 42 | height: TADimens.PADDING_XS_VALUE, 43 | ), 44 | Text( 45 | changelogItem.shortDescription, 46 | style: textTheme.bodySmall, 47 | ), 48 | ], 49 | ), 50 | ), 51 | ), 52 | ); 53 | } 54 | 55 | Color _getCardColor(BuildContext context) { 56 | final theme = Theme.of(context); 57 | final themeCardColor = theme.cardColor; 58 | final brightness = MediaQuery.of(context).platformBrightness; 59 | bool isDark = brightness == Brightness.dark; 60 | if (isDark) { 61 | return isActive ? themeCardColor : Colors.transparent; 62 | } else { 63 | return isActive ? themeCardColor.withOpacity(0.75) : themeCardColor; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /jenkins/multi-branch-ci.groovy: -------------------------------------------------------------------------------- 1 | def getRepoURL() { 2 | sh "git config --get remote.origin.url > .git/remote-url" 3 | return readFile(".git/remote-url").trim() 4 | } 5 | 6 | def getCommitSha() { 7 | sh "git rev-parse HEAD > .git/current-commit" 8 | return readFile(".git/current-commit").trim() 9 | } 10 | 11 | def getCurrentBranch() { 12 | return env.BRANCH_NAME; 13 | } 14 | 15 | void setBuildStatus(String message, String state) { 16 | repoUrl = getRepoURL() 17 | commitSha = getCommitSha() 18 | 19 | step([ 20 | $class : 'GitHubCommitStatusSetter', 21 | reposSource : [$class: "ManuallyEnteredRepositorySource", url: repoUrl], 22 | commitShaSource : [$class: "ManuallyEnteredShaSource", sha: commitSha], 23 | errorHandlers : [[$class: 'ShallowAnyErrorHandler']], 24 | statusResultSource: [$class: "ConditionalStatusResultSource", results: [[$class: "AnyBuildResult", message: message, state: state]]] 25 | ]); 26 | } 27 | 28 | pipeline { 29 | agent { label 'flutter' } 30 | 31 | options { 32 | buildDiscarder(logRotator(artifactNumToKeepStr: '10', artifactDaysToKeepStr:'90')) 33 | } 34 | 35 | stages { 36 | stage('Checkout') { 37 | steps { 38 | checkout scm 39 | } 40 | } 41 | stage('Get dependencies') { 42 | steps { 43 | sh('fvm flutter pub get -v') 44 | } 45 | } 46 | stage('Build runner') { 47 | steps { 48 | sh('fvm flutter packages pub run build_runner build --delete-conflicting-outputs') 49 | } 50 | } 51 | stage('Build') { 52 | steps { 53 | script { 54 | SENTRY_RELEASE = 'flutter-app-ci-' + getCurrentBranch() + '-' + getCommitSha() 55 | } 56 | sh('fvm flutter build web --no-web-resources-cdn') 57 | } 58 | } 59 | stage('Prepare artifacts') { 60 | steps { 61 | sh('cd build/web && zip ' + SENTRY_RELEASE + '.zip -r *') 62 | } 63 | } 64 | } 65 | post { 66 | success { 67 | setBuildStatus("Build succeeded", "SUCCESS"); 68 | archiveArtifacts artifacts: 'build/web/*.zip', fingerprint: true 69 | cleanWs() 70 | } 71 | failure { 72 | setBuildStatus("Build failed", "FAILURE"); 73 | cleanWs() 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/feature/settings/widget/gps_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 4 | import 'package:tesla_android/feature/settings/bloc/gps_configuration_cubit.dart'; 5 | import 'package:tesla_android/feature/settings/bloc/gps_configuration_state.dart'; 6 | import 'package:tesla_android/feature/settings/widget/settings_section.dart'; 7 | import 'package:tesla_android/feature/settings/widget/settings_tile.dart'; 8 | 9 | class GpsSettings extends SettingsSection { 10 | const GpsSettings({super.key}) 11 | : super( 12 | name: "GPS", 13 | icon: Icons.gps_fixed, 14 | ); 15 | 16 | @override 17 | Widget body(BuildContext context) { 18 | return BlocBuilder( 19 | builder: (context, state) { 20 | return Column( 21 | crossAxisAlignment: CrossAxisAlignment.start, 22 | children: [ 23 | SettingsTile( 24 | icon: Icons.gps_fixed, 25 | title: 'GPS', 26 | subtitle: 'Disable if you don\'t use Android navigation apps', 27 | trailing: _gpsStateSwitch(context, state)), 28 | const Padding( 29 | padding: EdgeInsets.all(TADimens.PADDING_S_VALUE), 30 | child: Text( 31 | 'NOTE: GPS via Browser can cause crashes on Tesla Software 2024.14 or newer, the integration is disabled by default until this issue is solved by Tesla. Please be assured that your location data never leaves your car. The real-time location updates sent to your Tesla Android device are solely utilized to emulate a hardware GPS module in the Android OS.'), 32 | ), 33 | ], 34 | ); 35 | } 36 | ); 37 | } 38 | 39 | Widget _gpsStateSwitch(BuildContext context, 40 | GPSConfigurationState state) { 41 | final cubit = BlocProvider.of(context); 42 | if (state is GPSConfigurationStateLoaded) { 43 | return Switch( 44 | value: state.isGPSEnabled, 45 | onChanged: (value) { 46 | cubit.setState(value); 47 | }); 48 | } else if (state is GPSConfigurationStateUpdateInProgress || 49 | state is GPSConfigurationStateLoading) { 50 | return const CircularProgressIndicator(); 51 | } else if (state is GPSConfigurationStateError) { 52 | return const Text("Service error"); 53 | } 54 | return const SizedBox.shrink(); 55 | } 56 | } -------------------------------------------------------------------------------- /lib/feature/settings/bloc/audio_configuration_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:tesla_android/common/utils/logger.dart'; 4 | import 'package:tesla_android/feature/settings/bloc/audio_configuration_state.dart'; 5 | import 'package:tesla_android/feature/settings/repository/system_configuration_repository.dart'; 6 | 7 | @injectable 8 | class AudioConfigurationCubit extends Cubit 9 | with Logger { 10 | final SystemConfigurationRepository _repository; 11 | 12 | AudioConfigurationCubit(this._repository) 13 | : super(AudioConfigurationStateInitial()); 14 | 15 | Future fetchConfiguration() async { 16 | if (!isClosed) emit(AudioConfigurationStateLoading()); 17 | try { 18 | final configuration = await _repository.getConfiguration(); 19 | emit( 20 | AudioConfigurationStateSettingsFetched( 21 | isEnabled: configuration.browserAudioIsEnabled == 1, 22 | volume: configuration.browserAudioVolume), 23 | ); 24 | } catch (exception, stacktrace) { 25 | logException( 26 | exception: exception, 27 | stackTrace: stacktrace, 28 | ); 29 | if (!isClosed) { 30 | emit( 31 | AudioConfigurationStateError(), 32 | ); 33 | } 34 | } 35 | } 36 | 37 | void setVolume(int newVolume) async { 38 | if (!isClosed) emit(AudioConfigurationStateSettingsUpdateInProgress()); 39 | try { 40 | await _repository.setBrowserAudioState(1); 41 | await _repository.setBrowserAudioVolume(newVolume); 42 | if (!isClosed) { 43 | emit( 44 | AudioConfigurationStateSettingsFetched( 45 | isEnabled: true, volume: newVolume), 46 | ); 47 | } 48 | } catch (exception, stackTrace) { 49 | logException( 50 | exception: exception, stackTrace: stackTrace); 51 | if (!isClosed) emit(AudioConfigurationStateError()); 52 | } 53 | } 54 | 55 | void setState(bool isEnabled) async { 56 | if (!isClosed) emit(AudioConfigurationStateSettingsUpdateInProgress()); 57 | try { 58 | await _repository.setBrowserAudioState(isEnabled ? 1 : 0); 59 | await _repository.setBrowserAudioVolume(100); 60 | if (!isClosed) { 61 | emit( 62 | AudioConfigurationStateSettingsFetched( 63 | isEnabled: isEnabled, volume: 100), 64 | ); 65 | } 66 | } catch (exception, stackTrace) { 67 | logException( 68 | exception: exception, stackTrace: stackTrace); 69 | if (!isClosed) emit(AudioConfigurationStateError()); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/feature/settings/model/system_configuration_response_body.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'system_configuration_response_body.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SystemConfigurationResponseBody _$SystemConfigurationResponseBodyFromJson( 10 | Map json, 11 | ) => SystemConfigurationResponseBody( 12 | bandType: (json['persist.tesla-android.softap.band_type'] as num).toInt(), 13 | channel: (json['persist.tesla-android.softap.channel'] as num).toInt(), 14 | channelWidth: (json['persist.tesla-android.softap.channel_width'] as num) 15 | .toInt(), 16 | isEnabledFlag: (json['persist.tesla-android.softap.is_enabled'] as num) 17 | .toInt(), 18 | isOfflineModeEnabledFlag: 19 | (json['persist.tesla-android.offline-mode.is_enabled'] as num).toInt(), 20 | isOfflineModeTelemetryEnabledFlag: 21 | (json['persist.tesla-android.offline-mode.telemetry.is_enabled'] as num) 22 | .toInt(), 23 | isOfflineModeTeslaFirmwareDownloadsEnabledFlag: 24 | (json['persist.tesla-android.offline-mode.tesla-firmware-downloads'] 25 | as num) 26 | .toInt(), 27 | browserAudioIsEnabled: 28 | (json['persist.tesla-android.browser_audio.is_enabled'] as num?) 29 | ?.toInt() ?? 30 | 1, 31 | browserAudioVolume: 32 | (json['persist.tesla-android.browser_audio.volume'] as num?)?.toInt() ?? 33 | 100, 34 | isGPSEnabled: 35 | (json['persist.tesla-android.gps.is_active'] as num?)?.toInt() ?? 1, 36 | ); 37 | 38 | Map _$SystemConfigurationResponseBodyToJson( 39 | SystemConfigurationResponseBody instance, 40 | ) => { 41 | 'persist.tesla-android.softap.band_type': instance.bandType, 42 | 'persist.tesla-android.softap.channel': instance.channel, 43 | 'persist.tesla-android.softap.channel_width': instance.channelWidth, 44 | 'persist.tesla-android.softap.is_enabled': instance.isEnabledFlag, 45 | 'persist.tesla-android.offline-mode.is_enabled': 46 | instance.isOfflineModeEnabledFlag, 47 | 'persist.tesla-android.offline-mode.telemetry.is_enabled': 48 | instance.isOfflineModeTelemetryEnabledFlag, 49 | 'persist.tesla-android.offline-mode.tesla-firmware-downloads': 50 | instance.isOfflineModeTeslaFirmwareDownloadsEnabledFlag, 51 | 'persist.tesla-android.browser_audio.is_enabled': 52 | instance.browserAudioIsEnabled, 53 | 'persist.tesla-android.browser_audio.volume': instance.browserAudioVolume, 54 | 'persist.tesla-android.gps.is_active': instance.isGPSEnabled, 55 | }; 56 | -------------------------------------------------------------------------------- /lib/common/network/github_service.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'github_service.dart'; 4 | 5 | // dart format off 6 | 7 | // ************************************************************************** 8 | // RetrofitGenerator 9 | // ************************************************************************** 10 | 11 | // ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter 12 | 13 | class _GitHubService implements GitHubService { 14 | _GitHubService(this._dio, {this.baseUrl, this.errorLogger}); 15 | 16 | final Dio _dio; 17 | 18 | String? baseUrl; 19 | 20 | final ParseErrorLogger? errorLogger; 21 | 22 | @override 23 | Future getLatestRelease() async { 24 | final _extra = {}; 25 | final queryParameters = {}; 26 | final _headers = {r'X-GitHub-Api-Version': '2022-11-28'}; 27 | _headers.removeWhere((k, v) => v == null); 28 | const Map? _data = null; 29 | final _options = _setStreamType( 30 | Options(method: 'GET', headers: _headers, extra: _extra) 31 | .compose( 32 | _dio.options, 33 | '/repos/tesla-android/android-raspberry-pi/releases/latest', 34 | queryParameters: queryParameters, 35 | data: _data, 36 | ) 37 | .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), 38 | ); 39 | final _result = await _dio.fetch>(_options); 40 | late GitHubRelease _value; 41 | try { 42 | _value = GitHubRelease.fromJson(_result.data!); 43 | } on Object catch (e, s) { 44 | errorLogger?.logError(e, s, _options); 45 | rethrow; 46 | } 47 | return _value; 48 | } 49 | 50 | RequestOptions _setStreamType(RequestOptions requestOptions) { 51 | if (T != dynamic && 52 | !(requestOptions.responseType == ResponseType.bytes || 53 | requestOptions.responseType == ResponseType.stream)) { 54 | if (T == String) { 55 | requestOptions.responseType = ResponseType.plain; 56 | } else { 57 | requestOptions.responseType = ResponseType.json; 58 | } 59 | } 60 | return requestOptions; 61 | } 62 | 63 | String _combineBaseUrls(String dioBaseUrl, String? baseUrl) { 64 | if (baseUrl == null || baseUrl.trim().isEmpty) { 65 | return dioBaseUrl; 66 | } 67 | 68 | final url = Uri.parse(baseUrl); 69 | 70 | if (url.isAbsolute) { 71 | return url.toString(); 72 | } 73 | 74 | return Uri.parse(dioBaseUrl).resolveUri(url).toString(); 75 | } 76 | } 77 | 78 | // dart format on 79 | -------------------------------------------------------------------------------- /lib/feature/settings/widget/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tesla_android/common/navigation/ta_page.dart'; 3 | import 'package:tesla_android/common/ui/components/ta_app_bar.dart'; 4 | import 'package:tesla_android/common/ui/components/ta_bottom_navigation_bar.dart'; 5 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 6 | import 'package:tesla_android/feature/settings/widget/device_settings.dart'; 7 | import 'package:tesla_android/feature/settings/widget/display_settings.dart'; 8 | import 'package:tesla_android/feature/settings/widget/gps_settings.dart'; 9 | import 'package:tesla_android/feature/settings/widget/hotspot_settings.dart'; 10 | import 'package:tesla_android/feature/settings/widget/rear_display_settings.dart'; 11 | import 'package:tesla_android/feature/settings/widget/settings_section.dart'; 12 | import 'package:tesla_android/feature/settings/widget/sound_settings.dart'; 13 | 14 | class SettingsPage extends StatefulWidget { 15 | const SettingsPage({super.key}); 16 | 17 | @override 18 | State createState() => _SettingsPageState(); 19 | } 20 | 21 | class _SettingsPageState extends State { 22 | int _activeIndex = 0; 23 | 24 | late List _sections; 25 | 26 | @override 27 | void initState() { 28 | _sections = [ 29 | const DisplaySettings(), 30 | const RearDisplaySettings(), 31 | const HotspotSettings(), 32 | const SoundSettings(), 33 | const GpsSettings(), 34 | const DeviceSettings(), 35 | ]; 36 | super.initState(); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return Scaffold( 42 | appBar: TaAppBar(title: TAPage.settings.title), 43 | bottomNavigationBar: const TaBottomNavigationBar(currentIndex: 4), 44 | body: Row( 45 | mainAxisAlignment: MainAxisAlignment.start, 46 | crossAxisAlignment: CrossAxisAlignment.start, 47 | children: [ 48 | NavigationDrawer( 49 | selectedIndex: _activeIndex, 50 | onDestinationSelected: (index) { 51 | setState(() { 52 | _activeIndex = index; 53 | }); 54 | }, 55 | children: [ 56 | const SizedBox(height: TADimens.baseContentMargin), 57 | ..._sections.map( 58 | (section) => NavigationDrawerDestination( 59 | icon: Icon(section.icon), 60 | label: Text(section.name), 61 | ), 62 | ), 63 | ], 64 | ), 65 | Expanded( 66 | child: IndexedStack(index: _activeIndex, children: _sections), 67 | ), 68 | ], 69 | ), 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /web/h264BrodwayWorker.js: -------------------------------------------------------------------------------- 1 | importScripts("Decoder.js", "YUVCanvas.js"); 2 | 3 | let pendingFrames = [], 4 | underflow = true, 5 | decoder, 6 | socket, 7 | height, 8 | width, 9 | windowHeight, 10 | windowWidth, 11 | yuvCanvas; 12 | 13 | const MAX_BUFFER_SIZE = 50; 14 | 15 | function isKeyframe(buffer) { 16 | for (let i = 0; i < buffer.length - 4; i++) { 17 | if ( 18 | buffer[i] === 0x00 && 19 | buffer[i + 1] === 0x00 && 20 | buffer[i + 2] === 0x00 && 21 | buffer[i + 3] === 0x01 22 | ) { 23 | const nalHeader = buffer[i + 4]; 24 | const nalType = nalHeader & 0x1F; 25 | return nalType === 5; 26 | } 27 | } 28 | return false; 29 | } 30 | 31 | async function renderFrame() { 32 | underflow = false; 33 | while (pendingFrames.length > 0) { 34 | const frame = pendingFrames.shift(); 35 | drawImageToCanvas(frame); 36 | await Promise.resolve(); 37 | } 38 | underflow = true; 39 | } 40 | 41 | function drawImageToCanvas(image) { 42 | const ySize = width * height; 43 | const uvSize = (width >> 1) * (height >> 1); 44 | 45 | const y = image.subarray(0, ySize); 46 | const u = image.subarray(ySize, ySize + uvSize); 47 | const v = image.subarray(ySize + uvSize, ySize + 2 * uvSize); 48 | 49 | yuvCanvas.drawNextOutputPicture({ 50 | yData: y, 51 | uData: u, 52 | vData: v 53 | }); 54 | 55 | if (image.close) { 56 | image.close(); 57 | } 58 | } 59 | 60 | function handleMessage(dat) { 61 | decoder.decode(dat); 62 | 63 | // If frame buffer is too large, drop until next keyframe 64 | if (pendingFrames.length > MAX_BUFFER_SIZE) { 65 | let foundKeyframe = false; 66 | while (!foundKeyframe && pendingFrames.length > 0) { 67 | const frame = pendingFrames.shift(); 68 | if (isKeyframe(frame)) { 69 | foundKeyframe = true; 70 | pendingFrames.unshift(frame); 71 | } 72 | } 73 | } 74 | } 75 | 76 | self.onmessage = function (event) { 77 | if (event.data.canvas && event.data.displayWidth && event.data.displayHeight) { 78 | const offscreenCanvas = event.data.canvas; 79 | height = event.data.displayHeight; 80 | width = event.data.displayWidth; 81 | windowHeight = event.data.windowHeight; 82 | windowWidth = event.data.windowWidth; 83 | 84 | yuvCanvas = new YUVCanvas({ 85 | canvas: offscreenCanvas, 86 | width: width, 87 | height: height, 88 | type: 'yuv420', 89 | reuseMemory: true, 90 | }); 91 | 92 | decoder = new Decoder({ rgb: false }); 93 | decoder.onPictureDecoded = function (data) { 94 | pendingFrames.push(data); 95 | if (underflow) { 96 | renderFrame(); 97 | } 98 | }; 99 | } else if (event.data.h264Data) { 100 | handleMessage(new Uint8Array(event.data.h264Data)); 101 | } 102 | }; -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/feature/releaseNotes/widget/release_notes_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:tesla_android/common/navigation/ta_page.dart'; 4 | import 'package:tesla_android/common/ui/components/ta_app_bar.dart'; 5 | import 'package:tesla_android/common/ui/components/ta_bottom_navigation_bar.dart'; 6 | import 'package:tesla_android/feature/releaseNotes/cubit/release_notes_cubit.dart'; 7 | import 'package:tesla_android/feature/releaseNotes/cubit/release_notes_state.dart'; 8 | import 'package:tesla_android/feature/releaseNotes/model/changelog_item.dart'; 9 | import 'package:tesla_android/feature/releaseNotes/model/release_notes.dart'; 10 | import 'package:tesla_android/feature/releaseNotes/model/version.dart'; 11 | import 'package:tesla_android/feature/releaseNotes/widget/detail/release_notes_changelog_item_details_view.dart'; 12 | 13 | import 'list/release_notes_versions_list.dart'; 14 | 15 | class ReleaseNotesPage extends StatelessWidget { 16 | const ReleaseNotesPage({super.key}); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | BlocProvider.of(context).loadReleaseNotes(); 21 | 22 | return Scaffold( 23 | appBar: TaAppBar( 24 | title: TAPage.releaseNotes.title, 25 | ), 26 | bottomNavigationBar: const TaBottomNavigationBar( 27 | currentIndex: 2, 28 | ), 29 | body: BlocBuilder( 30 | builder: (context, state) { 31 | if (state is ReleaseNotesStateLoading) { 32 | return _loadingStateWidget(); 33 | } else if (state is ReleaseNotesStateUnavailable) { 34 | return _errorStateWidget(); 35 | } else if (state is ReleaseNotesStateLoaded) { 36 | return _loadedStateWidget(context, state); 37 | } else { 38 | return Container(); 39 | } 40 | })); 41 | } 42 | 43 | Widget _loadingStateWidget() { 44 | return const Center( 45 | child: CircularProgressIndicator(), 46 | ); 47 | } 48 | 49 | Widget _errorStateWidget() { 50 | return const Center( 51 | child: Text("Error loading release notes. Please try again later.")); 52 | } 53 | 54 | Widget _loadedStateWidget( 55 | BuildContext context, ReleaseNotesStateLoaded state) { 56 | final ReleaseNotes releaseNotes = state.releaseNotes; 57 | final Version selectedVersion = state.selectedVersion; 58 | final ChangelogItem selectedChangelogItem = state.selectedChangelogItem; 59 | return Row( 60 | mainAxisAlignment: MainAxisAlignment.start, 61 | crossAxisAlignment: CrossAxisAlignment.start, 62 | children: [ 63 | SizedBox( 64 | width: MediaQuery.of(context).size.width * 0.30, 65 | child: ReleaseNotesVersionList( 66 | versions: releaseNotes.versions.take(3).toList(), 67 | selectedVersion: selectedVersion, 68 | selectedChangelogItem: selectedChangelogItem, 69 | ), 70 | ), 71 | Expanded( 72 | child: ReleaseNotesChangelogItemDetailsView( 73 | changelogItem: selectedChangelogItem), 74 | ) 75 | ], 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/common/network/base_websocket_transport.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:flavor/flavor.dart'; 6 | import 'package:tesla_android/common/di/ta_locator.dart'; 7 | import 'package:tesla_android/common/utils/logger.dart'; 8 | import 'package:web_socket_client/web_socket_client.dart'; 9 | 10 | abstract class BaseWebsocketTransport with Logger { 11 | final String flavorUrlKey; 12 | final bool sendKeepAlive; 13 | String? binaryType; 14 | 15 | Flavor get _flavor => getIt(); 16 | 17 | WebSocket? _webSocketChannel; 18 | 19 | BaseWebsocketTransport({ 20 | required this.flavorUrlKey, 21 | this.binaryType, 22 | this.sendKeepAlive = false, 23 | }); 24 | 25 | bool get _isConnected => 26 | _webSocketChannel?.connection.state is Connected || 27 | _webSocketChannel?.connection.state is Reconnected; 28 | 29 | void connect() { 30 | if (_isConnected) { 31 | return; 32 | } 33 | _connect(); 34 | } 35 | 36 | void disconnect() { 37 | _webSocketChannel?.close(); 38 | _webSocketChannel = null; 39 | } 40 | 41 | Future _connect() async { 42 | _webSocketChannel = WebSocket( 43 | Uri.parse(_flavor.getString( 44 | flavorUrlKey, 45 | )!), 46 | binaryType: binaryType ?? "blob", 47 | pingInterval: const Duration(seconds: 30), 48 | timeout: const Duration(seconds: 10), 49 | backoff: BinaryExponentialBackoff( 50 | initial: const Duration(seconds: 1), 51 | maximumStep: 10, 52 | ), 53 | ); 54 | _webSocketChannel?.messages.listen((message) { 55 | onMessage(message); 56 | }); 57 | _webSocketChannel?.connection.listen( 58 | (event) { 59 | if (event is Connected) { 60 | log("Connected"); 61 | } else if (event is Connecting) { 62 | log("Connecting"); 63 | } else if (event is Reconnected) { 64 | log("Reconnected"); 65 | } else if (event is Reconnecting) { 66 | log("Reconnecting"); 67 | } else if (event is Disconnected) { 68 | log("Disconnected"); 69 | } else if (event is Disconnecting) { 70 | log("Disconnecting"); 71 | } 72 | }, 73 | ); 74 | } 75 | 76 | void onMessage(event) { 77 | // optional 78 | } 79 | 80 | void onOpen() { 81 | //optional 82 | } 83 | 84 | void send(dynamic message) { 85 | if (!_isConnected) return; 86 | _webSocketChannel?.send(message); 87 | } 88 | 89 | void sendString(String string) { 90 | if (!_isConnected) return; 91 | _webSocketChannel?.send(string); 92 | } 93 | 94 | void sendJson(object) { 95 | if (!_isConnected) return; 96 | final jsonString = jsonEncode(object); 97 | sendString(jsonString); 98 | } 99 | 100 | void sendBlob(blob) { 101 | if (!_isConnected) return; 102 | _webSocketChannel?.send(blob); 103 | } 104 | 105 | void sendByteBuffer(ByteBuffer byteBuffer) { 106 | if (!_isConnected) return; 107 | _webSocketChannel?.send(byteBuffer); 108 | } 109 | 110 | void sendTypedData(TypedData typedData) { 111 | if (!_isConnected) return; 112 | _webSocketChannel?.send(typedData); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/common/ui/constants/ta_dimens.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class TADimens { 6 | static const PADDING_XXXS_VALUE = 2.0; 7 | static const PADDING_XXS_VALUE = 4.0; 8 | static const PADDING_XS_VALUE = 6.0; 9 | static const PADDING_S_VALUE = 8.0; 10 | static const PADDING_SEMI_S_VALUE = 10.0; 11 | static const PADDING_VALUE = 12.0; 12 | static const PADDING_L_VALUE = 18.0; 13 | static const PADDING_SEMI_L_VALUE = 22.0; 14 | static const PADDING_XL_VALUE = 25.0; 15 | static const PADDING_XXL_VALUE = 30.0; 16 | static const PADDING_XXXL_VALUE = 40.0; 17 | 18 | static const PADDING_XXXS = EdgeInsets.all(PADDING_XXXS_VALUE); 19 | static const PADDING_XXS = EdgeInsets.all(PADDING_XXS_VALUE); 20 | static const PADDING_XS = EdgeInsets.all(PADDING_XS_VALUE); 21 | static const PADDING_S = EdgeInsets.all(PADDING_S_VALUE); 22 | static const PADDING = EdgeInsets.all(PADDING_VALUE); 23 | static const PADDING_L = EdgeInsets.all(PADDING_L_VALUE); 24 | static const PADDING_XL = EdgeInsets.all(PADDING_XL_VALUE); 25 | static const PADDING_XXL = EdgeInsets.all(PADDING_XXL_VALUE); 26 | static const PADDING_XXXL = EdgeInsets.all(PADDING_XXXL_VALUE); 27 | 28 | static const VERTICAL_PADDING_S = 29 | EdgeInsets.only(top: PADDING_S_VALUE, bottom: PADDING_S_VALUE); 30 | static const VERTICAL_PADDING = 31 | EdgeInsets.only(top: PADDING_VALUE, bottom: PADDING_VALUE); 32 | static const VERTICAL_PADDING_L = 33 | EdgeInsets.only(top: PADDING_L_VALUE, bottom: PADDING_L_VALUE); 34 | 35 | static const ROUND_BORDER_RADIUS_XXS = 8.0; 36 | static const ROUND_BORDER_RADIUS_XS = 10.0; 37 | static const ROUND_BORDER_RADIUS_S = 16.0; 38 | static const ROUND_BORDER_RADIUS = 18.0; 39 | static const ROUND_BORDER_RADIUS_L = 24.0; 40 | static const ROUND_BORDER_RADIUS_XL = 28.0; 41 | static const ROUND_BORDER_RADIUS_XXL = 34.0; 42 | 43 | static const ELEVATION_XXS = 4.0; 44 | static const ELEVATION_XS = 6.0; 45 | static const ELEVATION_S = 8.0; 46 | static const ELEVATION = 10.0; 47 | static const ELEVATION_L = 12.0; 48 | static const ELEVATION_XL = 14.0; 49 | static const ELEVATION_XXL = 16.0; 50 | 51 | static const baseContentMargin = 16.0; 52 | static const halfBaseContentMargin = 8.0; 53 | 54 | static const halfBasePadding = EdgeInsets.all(halfBaseContentMargin); 55 | static const halfBasePaddingHorizontal = 56 | EdgeInsets.symmetric(horizontal: halfBaseContentMargin); 57 | static const halfBasePaddingVertical = 58 | EdgeInsets.symmetric(vertical: halfBaseContentMargin); 59 | 60 | static const basePadding = EdgeInsets.all(baseContentMargin); 61 | static const basePaddingHorizontal = 62 | EdgeInsets.symmetric(horizontal: baseContentMargin); 63 | static const basePaddingVertical = 64 | EdgeInsets.symmetric(vertical: baseContentMargin); 65 | 66 | static const splashPageLogoHeight = 200.0; 67 | static const splashPageRadius = 25.0; 68 | 69 | static const donationTextWidth = 300.0; 70 | static const donationQRSize = 250.0; 71 | 72 | static const backendErrorIconSize = 80.0; 73 | 74 | static const statusBarIconSize = 20.0; 75 | 76 | static const settingsTileTrailingWidthDense = 200.0; 77 | static const settingsTileTrailingWidth = 450.0; 78 | static const settingsPageTableMaxWidth = 1024.0; 79 | } 80 | -------------------------------------------------------------------------------- /lib/feature/display/model/remote_display_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'remote_display_state.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | RemoteDisplayState _$RemoteDisplayStateFromJson(Map json) => 10 | RemoteDisplayState( 11 | width: (json['width'] as num).toInt(), 12 | height: (json['height'] as num).toInt(), 13 | density: (json['density'] as num).toInt(), 14 | resolutionPreset: $enumDecode( 15 | _$DisplayResolutionModePresetEnumMap, 16 | json['resolutionPreset'], 17 | ), 18 | renderer: 19 | $enumDecodeNullable(_$DisplayRendererTypeEnumMap, json['renderer']) ?? 20 | DisplayRendererType.mjpeg, 21 | isResponsive: (json['isResponsive'] as num?)?.toInt() ?? 1, 22 | isH264: (json['isH264'] as num?)?.toInt() ?? 0, 23 | refreshRate: 24 | $enumDecodeNullable( 25 | _$DisplayRefreshRatePresetEnumMap, 26 | json['refreshRate'], 27 | ) ?? 28 | DisplayRefreshRatePreset.refresh30hz, 29 | quality: 30 | $enumDecodeNullable(_$DisplayQualityPresetEnumMap, json['quality']) ?? 31 | DisplayQualityPreset.quality90, 32 | isRearDisplayEnabled: 33 | (json['isRearDisplayEnabled'] as num?)?.toInt() ?? 0, 34 | isRearDisplayPrioritised: 35 | (json['isRearDisplayPrioritised'] as num?)?.toInt() ?? 0, 36 | isHeadless: (json['isHeadless'] as num?)?.toInt(), 37 | ); 38 | 39 | Map _$RemoteDisplayStateToJson(RemoteDisplayState instance) => 40 | { 41 | 'width': instance.width, 42 | 'height': instance.height, 43 | 'density': instance.density, 44 | 'resolutionPreset': 45 | _$DisplayResolutionModePresetEnumMap[instance.resolutionPreset]!, 46 | 'renderer': _$DisplayRendererTypeEnumMap[instance.renderer]!, 47 | 'isHeadless': instance.isHeadless, 48 | 'isResponsive': instance.isResponsive, 49 | 'isH264': instance.isH264, 50 | 'refreshRate': _$DisplayRefreshRatePresetEnumMap[instance.refreshRate]!, 51 | 'quality': _$DisplayQualityPresetEnumMap[instance.quality]!, 52 | 'isRearDisplayEnabled': instance.isRearDisplayEnabled, 53 | 'isRearDisplayPrioritised': instance.isRearDisplayPrioritised, 54 | }; 55 | 56 | const _$DisplayResolutionModePresetEnumMap = { 57 | DisplayResolutionModePreset.res832p: 0, 58 | DisplayResolutionModePreset.res720p: 1, 59 | DisplayResolutionModePreset.res640p: 2, 60 | DisplayResolutionModePreset.res544p: 3, 61 | DisplayResolutionModePreset.res480p: 4, 62 | }; 63 | 64 | const _$DisplayRendererTypeEnumMap = { 65 | DisplayRendererType.mjpeg: 0, 66 | DisplayRendererType.h264WebCodecs: 1, 67 | DisplayRendererType.h264Brodway: 2, 68 | }; 69 | 70 | const _$DisplayRefreshRatePresetEnumMap = { 71 | DisplayRefreshRatePreset.refresh30hz: 30, 72 | DisplayRefreshRatePreset.refresh45hz: 45, 73 | DisplayRefreshRatePreset.refresh60hz: 60, 74 | }; 75 | 76 | const _$DisplayQualityPresetEnumMap = { 77 | DisplayQualityPreset.quality40: 40, 78 | DisplayQualityPreset.quality50: 50, 79 | DisplayQualityPreset.quality60: 60, 80 | DisplayQualityPreset.quality70: 70, 81 | DisplayQualityPreset.quality80: 80, 82 | DisplayQualityPreset.quality90: 90, 83 | }; 84 | -------------------------------------------------------------------------------- /lib/common/network/device_info_service.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'device_info_service.dart'; 4 | 5 | // dart format off 6 | 7 | // ************************************************************************** 8 | // RetrofitGenerator 9 | // ************************************************************************** 10 | 11 | // ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter 12 | 13 | class _DeviceInfoService implements DeviceInfoService { 14 | _DeviceInfoService(this._dio, {this.baseUrl, this.errorLogger}); 15 | 16 | final Dio _dio; 17 | 18 | String? baseUrl; 19 | 20 | final ParseErrorLogger? errorLogger; 21 | 22 | @override 23 | Future getDeviceInfo() async { 24 | final _extra = {}; 25 | final queryParameters = {}; 26 | final _headers = {}; 27 | const Map? _data = null; 28 | final _options = _setStreamType( 29 | Options( 30 | method: 'GET', 31 | headers: _headers, 32 | extra: _extra, 33 | responseType: ResponseType.json, 34 | ) 35 | .compose( 36 | _dio.options, 37 | '/deviceInfo', 38 | queryParameters: queryParameters, 39 | data: _data, 40 | ) 41 | .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), 42 | ); 43 | final _result = await _dio.fetch>(_options); 44 | late DeviceInfo _value; 45 | try { 46 | _value = DeviceInfo.fromJson(_result.data!); 47 | } on Object catch (e, s) { 48 | errorLogger?.logError(e, s, _options); 49 | rethrow; 50 | } 51 | return _value; 52 | } 53 | 54 | @override 55 | Future openUpdater() async { 56 | final _extra = {}; 57 | final queryParameters = {}; 58 | final _headers = {}; 59 | const Map? _data = null; 60 | final _options = _setStreamType( 61 | Options(method: 'GET', headers: _headers, extra: _extra) 62 | .compose( 63 | _dio.options, 64 | '/openUpdater', 65 | queryParameters: queryParameters, 66 | data: _data, 67 | ) 68 | .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), 69 | ); 70 | final _result = await _dio.fetch(_options); 71 | final _value = _result.data; 72 | return _value; 73 | } 74 | 75 | RequestOptions _setStreamType(RequestOptions requestOptions) { 76 | if (T != dynamic && 77 | !(requestOptions.responseType == ResponseType.bytes || 78 | requestOptions.responseType == ResponseType.stream)) { 79 | if (T == String) { 80 | requestOptions.responseType = ResponseType.plain; 81 | } else { 82 | requestOptions.responseType = ResponseType.json; 83 | } 84 | } 85 | return requestOptions; 86 | } 87 | 88 | String _combineBaseUrls(String dioBaseUrl, String? baseUrl) { 89 | if (baseUrl == null || baseUrl.trim().isEmpty) { 90 | return dioBaseUrl; 91 | } 92 | 93 | final url = Uri.parse(baseUrl); 94 | 95 | if (url.isAbsolute) { 96 | return url.toString(); 97 | } 98 | 99 | return Uri.parse(dioBaseUrl).resolveUri(url).toString(); 100 | } 101 | } 102 | 103 | // dart format on 104 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/feature/home/cubit/ota_update_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | import 'package:package_info_plus/package_info_plus.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | import 'package:tesla_android/common/utils/logger.dart'; 8 | import 'package:tesla_android/feature/home/cubit/ota_update_state.dart'; 9 | import 'package:tesla_android/feature/home/repository/github_release_repository.dart'; 10 | 11 | @injectable 12 | class OTAUpdateCubit extends Cubit with Logger { 13 | static const String _lastCheckedKey = 'ota_last_checked'; 14 | static const String _updateAvailableKey = 'ota_update_available'; 15 | static const String _lastVersionKey = 'ota_last_version'; 16 | 17 | final GitHubReleaseRepository _repository; 18 | final SharedPreferences _sharedPreferences; 19 | 20 | OTAUpdateCubit( 21 | this._repository, 22 | this._sharedPreferences, 23 | ) : super(OTAUpdateStateInitial()); 24 | 25 | Future checkForUpdates() async { 26 | final lastCheckedTimestamp = _sharedPreferences.getInt(_lastCheckedKey) ?? 0; 27 | final currentTime = DateTime.now().millisecondsSinceEpoch; 28 | 29 | final packageInfo = await PackageInfo.fromPlatform(); 30 | final storedVersion = _sharedPreferences.getString(_lastVersionKey) ?? ''; 31 | 32 | if (currentTime - lastCheckedTimestamp < 6 * 60 * 60 * 1000 && storedVersion == packageInfo.version) { 33 | if (_sharedPreferences.getBool(_updateAvailableKey) == true) { 34 | emit(OTAUpdateStateAvailable()); 35 | } else { 36 | emit(OTAUpdateStateNotAvailable()); 37 | } 38 | return; 39 | } 40 | 41 | try { 42 | final latestVersion = await _repository.getLatestRelease(); 43 | final areUpdatesAvailable = 44 | _checkIfUpdateIsAvailable(packageInfo.version, latestVersion.name); 45 | 46 | _sharedPreferences.setBool(_updateAvailableKey, areUpdatesAvailable); 47 | _sharedPreferences.setInt(_lastCheckedKey, currentTime); 48 | _sharedPreferences.setString(_lastVersionKey, packageInfo.version); 49 | 50 | if (areUpdatesAvailable) { 51 | emit(OTAUpdateStateAvailable()); 52 | } else { 53 | emit(OTAUpdateStateNotAvailable()); 54 | } 55 | } catch (exception, stacktrace) { 56 | logException( 57 | exception: exception, 58 | stackTrace: stacktrace, 59 | ); 60 | await Future.delayed(const Duration(minutes: 5), () { 61 | checkForUpdates(); 62 | }); 63 | } 64 | } 65 | 66 | void launchUpdater() { 67 | _repository.openUpdater(); 68 | } 69 | 70 | bool _checkIfUpdateIsAvailable(String currentVersion, String latestRelease) { 71 | List currentVersionParts = currentVersion.split('.'); 72 | List latestVersionParts = latestRelease.split('.'); 73 | 74 | if (currentVersionParts.length < 3 || latestVersionParts.length < 3) { 75 | throw const FormatException('Invalid version format'); 76 | } 77 | 78 | int maxLength = max(currentVersionParts.length, latestVersionParts.length); 79 | for (int i = 0; i < maxLength; i++) { 80 | int currentValue = (i < currentVersionParts.length) 81 | ? int.tryParse(currentVersionParts[i]) ?? 0 82 | : 0; 83 | int latestValue = (i < latestVersionParts.length) 84 | ? int.tryParse(latestVersionParts[i]) ?? 0 85 | : 0; 86 | 87 | if (latestValue > currentValue) { 88 | return true; 89 | } else if (latestValue < currentValue) { 90 | return false; 91 | } 92 | } 93 | 94 | return false; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/feature/settings/bloc/rear_display_configuration_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:tesla_android/common/utils/logger.dart'; 4 | import 'package:tesla_android/feature/display/model/remote_display_state.dart'; 5 | import 'package:tesla_android/feature/display/repository/display_repository.dart'; 6 | import 'package:tesla_android/feature/settings/bloc/rear_display_configuration_state.dart'; 7 | 8 | @injectable 9 | class RearDisplayConfigurationCubit extends Cubit 10 | with Logger { 11 | final DisplayRepository _repository; 12 | 13 | RemoteDisplayState? _currentConfig; 14 | 15 | RearDisplayConfigurationCubit(this._repository) 16 | : super(RearDisplayConfigurationStateInitial()); 17 | 18 | void fetchConfiguration() async { 19 | if (!isClosed) emit(RearDisplayConfigurationStateLoading()); 20 | try { 21 | _currentConfig = await _repository.getDisplayState(); 22 | _emitCurrentConfig(); 23 | } catch (exception, stacktrace) { 24 | logException(exception: exception, stackTrace: stacktrace); 25 | if (!isClosed) { 26 | emit(RearDisplayConfigurationStateError()); 27 | } 28 | } 29 | } 30 | 31 | void setRearDisplayState(bool newSetting) async { 32 | var config = _currentConfig; 33 | final isRearDisplayEnabled = newSetting ? 1 : 0; 34 | if (config != null) { 35 | config = config.copyWith(isRearDisplayEnabled: isRearDisplayEnabled); 36 | if (!isClosed) { 37 | emit(RearDisplayConfigurationStateSettingsUpdateInProgress()); 38 | } 39 | try { 40 | await _repository.updateDisplayConfiguration(config); 41 | _currentConfig = _currentConfig?.copyWith( 42 | isRearDisplayEnabled: isRearDisplayEnabled, 43 | ); 44 | _emitCurrentConfig(); 45 | } catch (exception, stackTrace) { 46 | logException(exception: exception, stackTrace: stackTrace); 47 | if (!isClosed) emit(RearDisplayConfigurationStateError()); 48 | } 49 | } else { 50 | log("_currentConfig not available"); 51 | } 52 | } 53 | 54 | void setRearDisplayPrioritization(bool newSetting) async { 55 | var config = _currentConfig; 56 | final isRearDisplayPrioritised = newSetting ? 1 : 0; 57 | if (config != null) { 58 | config = config.copyWith( 59 | isRearDisplayPrioritised: isRearDisplayPrioritised, 60 | ); 61 | if (!isClosed) { 62 | emit(RearDisplayConfigurationStateSettingsUpdateInProgress()); 63 | } 64 | try { 65 | await _repository.updateDisplayConfiguration(config); 66 | _currentConfig = _currentConfig?.copyWith( 67 | isRearDisplayPrioritised: isRearDisplayPrioritised, 68 | ); 69 | _emitCurrentConfig(); 70 | } catch (exception, stackTrace) { 71 | logException(exception: exception, stackTrace: stackTrace); 72 | if (!isClosed) emit(RearDisplayConfigurationStateError()); 73 | } 74 | } else { 75 | log("_currentConfig not available"); 76 | } 77 | } 78 | 79 | void setDisplayType({required bool isCurrentDisplayPrimary}) { 80 | _repository.setDisplayType(isCurrentDisplayPrimary); 81 | _emitCurrentConfig(); 82 | } 83 | 84 | void _emitCurrentConfig() async { 85 | if (!isClosed) { 86 | final isCurrentDisplayPrimary = await _repository.isPrimaryDisplay(); 87 | emit(RearDisplayConfigurationStateSettingsFetched( 88 | isRearDisplayEnabled: _currentConfig!.isRearDisplayEnabled == 1, 89 | isRearDisplayPrioritised: _currentConfig!.isRearDisplayPrioritised == 1, 90 | isCurrentDisplayPrimary: isCurrentDisplayPrimary ?? true, 91 | )); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/common/network/display_service.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'display_service.dart'; 4 | 5 | // dart format off 6 | 7 | // ************************************************************************** 8 | // RetrofitGenerator 9 | // ************************************************************************** 10 | 11 | // ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter 12 | 13 | class _DisplayService implements DisplayService { 14 | _DisplayService(this._dio, {this.baseUrl, this.errorLogger}); 15 | 16 | final Dio _dio; 17 | 18 | String? baseUrl; 19 | 20 | final ParseErrorLogger? errorLogger; 21 | 22 | @override 23 | Future getDisplayState() async { 24 | final _extra = {}; 25 | final queryParameters = {}; 26 | final _headers = {}; 27 | const Map? _data = null; 28 | final _options = _setStreamType( 29 | Options( 30 | method: 'GET', 31 | headers: _headers, 32 | extra: _extra, 33 | responseType: ResponseType.json, 34 | ) 35 | .compose( 36 | _dio.options, 37 | '/displayState', 38 | queryParameters: queryParameters, 39 | data: _data, 40 | ) 41 | .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), 42 | ); 43 | final _result = await _dio.fetch>(_options); 44 | late RemoteDisplayState _value; 45 | try { 46 | _value = RemoteDisplayState.fromJson(_result.data!); 47 | } on Object catch (e, s) { 48 | errorLogger?.logError(e, s, _options); 49 | rethrow; 50 | } 51 | return _value; 52 | } 53 | 54 | @override 55 | Future updateDisplayConfiguration( 56 | RemoteDisplayState configuration, 57 | ) async { 58 | final _extra = {}; 59 | final queryParameters = {}; 60 | final _headers = {r'Content-Type': 'application/json'}; 61 | _headers.removeWhere((k, v) => v == null); 62 | final _data = {}; 63 | _data.addAll(configuration.toJson()); 64 | final _options = _setStreamType( 65 | Options( 66 | method: 'POST', 67 | headers: _headers, 68 | extra: _extra, 69 | contentType: 'application/json', 70 | ) 71 | .compose( 72 | _dio.options, 73 | '/displayState', 74 | queryParameters: queryParameters, 75 | data: _data, 76 | ) 77 | .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), 78 | ); 79 | final _result = await _dio.fetch(_options); 80 | final _value = _result.data; 81 | return _value; 82 | } 83 | 84 | RequestOptions _setStreamType(RequestOptions requestOptions) { 85 | if (T != dynamic && 86 | !(requestOptions.responseType == ResponseType.bytes || 87 | requestOptions.responseType == ResponseType.stream)) { 88 | if (T == String) { 89 | requestOptions.responseType = ResponseType.plain; 90 | } else { 91 | requestOptions.responseType = ResponseType.json; 92 | } 93 | } 94 | return requestOptions; 95 | } 96 | 97 | String _combineBaseUrls(String dioBaseUrl, String? baseUrl) { 98 | if (baseUrl == null || baseUrl.trim().isEmpty) { 99 | return dioBaseUrl; 100 | } 101 | 102 | final url = Uri.parse(baseUrl); 103 | 104 | if (url.isAbsolute) { 105 | return url.toString(); 106 | } 107 | 108 | return Uri.parse(dioBaseUrl).resolveUri(url).toString(); 109 | } 110 | } 111 | 112 | // dart format on 113 | -------------------------------------------------------------------------------- /lib/feature/settings/bloc/system_configuration_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:tesla_android/feature/settings/model/softap_band_type.dart'; 2 | import 'package:tesla_android/feature/settings/model/system_configuration_response_body.dart'; 3 | 4 | abstract class SystemConfigurationState {} 5 | 6 | class SystemConfigurationStateInitial extends SystemConfigurationState {} 7 | 8 | class SystemConfigurationStateLoading extends SystemConfigurationState {} 9 | 10 | class SystemConfigurationStateSettingsFetched extends SystemConfigurationState { 11 | final SystemConfigurationResponseBody currentConfiguration; 12 | 13 | SystemConfigurationStateSettingsFetched({ 14 | required this.currentConfiguration, 15 | }); 16 | } 17 | 18 | class SystemConfigurationStateSettingsFetchingError 19 | extends SystemConfigurationState {} 20 | 21 | class SystemConfigurationStateSettingsModified 22 | extends SystemConfigurationState { 23 | final SystemConfigurationResponseBody currentConfiguration; 24 | final SoftApBandType newBandType; 25 | final bool isSoftApEnabled; 26 | final bool isOfflineModeEnabled; 27 | final bool isOfflineModeTelemetryEnabled; 28 | final bool isOfflineModeTeslaFirmwareDownloadsEnabled; 29 | 30 | SystemConfigurationStateSettingsModified({ 31 | required this.currentConfiguration, 32 | required this.newBandType, 33 | required this.isSoftApEnabled, 34 | required this.isOfflineModeEnabled, 35 | required this.isOfflineModeTelemetryEnabled, 36 | required this.isOfflineModeTeslaFirmwareDownloadsEnabled, 37 | }); 38 | 39 | SystemConfigurationStateSettingsModified.fromCurrentConfiguration({ 40 | required this.currentConfiguration, 41 | SoftApBandType? newBandType, 42 | bool? isSoftApEnabled, 43 | bool? isOfflineModeEnabled, 44 | bool? isOfflineModeTelemetryEnabled, 45 | bool? isOfflineModeTeslaFirmwareDownloadsEnabled, 46 | }) : newBandType = newBandType ?? currentConfiguration.currentSoftApBandType, 47 | isSoftApEnabled = isSoftApEnabled ?? 48 | (currentConfiguration.isEnabledFlag == 1 ? true : false), 49 | isOfflineModeEnabled = isOfflineModeEnabled ?? 50 | (currentConfiguration.isOfflineModeEnabledFlag == 1 ? true : false), 51 | isOfflineModeTelemetryEnabled = isOfflineModeTelemetryEnabled ?? 52 | (currentConfiguration.isOfflineModeTelemetryEnabledFlag == 1 53 | ? true 54 | : false), 55 | isOfflineModeTeslaFirmwareDownloadsEnabled = 56 | isOfflineModeTeslaFirmwareDownloadsEnabled ?? 57 | (currentConfiguration 58 | .isOfflineModeTeslaFirmwareDownloadsEnabledFlag == 59 | 1 60 | ? true 61 | : false); 62 | 63 | SystemConfigurationStateSettingsModified copyWith({ 64 | SystemConfigurationResponseBody? currentConfiguration, 65 | SoftApBandType? newBandType, 66 | bool? isSoftApEnabled, 67 | bool? isOfflineModeEnabled, 68 | bool? isOfflineModeTelemetryEnabled, 69 | bool? isOfflineModeTeslaFirmwareDownloadsEnabled, 70 | }) { 71 | return SystemConfigurationStateSettingsModified( 72 | currentConfiguration: currentConfiguration ?? this.currentConfiguration, 73 | newBandType: newBandType ?? this.newBandType, 74 | isSoftApEnabled: isSoftApEnabled ?? this.isSoftApEnabled, 75 | isOfflineModeEnabled: isOfflineModeEnabled ?? this.isOfflineModeEnabled, 76 | isOfflineModeTelemetryEnabled: 77 | isOfflineModeTelemetryEnabled ?? this.isOfflineModeTelemetryEnabled, 78 | isOfflineModeTeslaFirmwareDownloadsEnabled: 79 | isOfflineModeTeslaFirmwareDownloadsEnabled ?? 80 | this.isOfflineModeTeslaFirmwareDownloadsEnabled); 81 | } 82 | } 83 | 84 | class SystemConfigurationStateSettingsSaved extends SystemConfigurationState { 85 | final SystemConfigurationResponseBody currentConfiguration; 86 | 87 | SystemConfigurationStateSettingsSaved({ 88 | required this.currentConfiguration, 89 | }); 90 | } 91 | 92 | class SystemConfigurationStateSettingsSavingFailedError 93 | extends SystemConfigurationState {} 94 | -------------------------------------------------------------------------------- /lib/common/navigation/ta_page_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | import 'package:tesla_android/common/di/ta_locator.dart'; 5 | import 'package:tesla_android/common/navigation/ta_page.dart'; 6 | import 'package:tesla_android/feature/about/about_page.dart'; 7 | import 'package:tesla_android/feature/display/cubit/display_cubit.dart'; 8 | import 'package:tesla_android/feature/donations/widget/donation_page.dart'; 9 | import 'package:tesla_android/feature/home/cubit/ota_update_cubit.dart'; 10 | import 'package:tesla_android/feature/home/home_page.dart'; 11 | import 'package:tesla_android/feature/releaseNotes/cubit/release_notes_cubit.dart'; 12 | import 'package:tesla_android/feature/releaseNotes/widget/release_notes_page.dart'; 13 | import 'package:tesla_android/feature/settings/bloc/audio_configuration_cubit.dart'; 14 | import 'package:tesla_android/feature/settings/bloc/device_info_cubit.dart'; 15 | import 'package:tesla_android/feature/settings/bloc/display_configuration_cubit.dart'; 16 | import 'package:tesla_android/feature/settings/bloc/gps_configuration_cubit.dart'; 17 | import 'package:tesla_android/feature/settings/bloc/system_configuration_cubit.dart'; 18 | import 'package:tesla_android/feature/settings/widget/settings_page.dart'; 19 | import 'package:tesla_android/feature/touchscreen/cubit/touchscreen_cubit.dart'; 20 | 21 | import '../../feature/settings/bloc/rear_display_configuration_cubit.dart'; 22 | 23 | @injectable 24 | class TAPageFactory { 25 | String initialRoute = TAPage.home.route; 26 | 27 | Map getRoutes() { 28 | return {for (var e in TAPage.availablePages) e.route: buildPage(e)}; 29 | } 30 | 31 | Widget Function(BuildContext context) buildPage(TAPage page) { 32 | return (context) { 33 | switch (page) { 34 | case TAPage.home: 35 | return MultiBlocProvider( 36 | providers: [ 37 | BlocProvider(create: (_) => getIt()), 38 | BlocProvider(create: (_) => getIt()), 39 | BlocProvider(create: (_) => getIt()), 40 | BlocProvider(create: (_) => getIt()), 41 | BlocProvider( 42 | create: (_) => getIt()..checkForUpdates(), 43 | ), 44 | ], 45 | child: const HomePage(), 46 | ); 47 | case TAPage.releaseNotes: 48 | return BlocProvider( 49 | create: (_) => getIt(), 50 | child: const ReleaseNotesPage(), 51 | ); 52 | case TAPage.about: 53 | return const AboutPage(); 54 | case TAPage.donations: 55 | return const DonationPage(); 56 | case TAPage.settings: 57 | return MultiBlocProvider( 58 | providers: [ 59 | BlocProvider( 60 | create: (_) => 61 | getIt()..fetchConfiguration(), 62 | ), 63 | BlocProvider( 64 | create: (_) => 65 | getIt()..fetchConfiguration(), 66 | ), 67 | BlocProvider( 68 | create: (_) => 69 | getIt()..fetchConfiguration(), 70 | ), 71 | BlocProvider( 72 | create: (_) => 73 | getIt() 74 | ..fetchConfiguration(), 75 | ), 76 | BlocProvider( 77 | create: (_) => 78 | getIt()..fetchConfiguration(), 79 | ), 80 | BlocProvider( 81 | create: (_) => getIt()..fetchConfiguration(), 82 | ), 83 | ], 84 | child: const SettingsPage(), 85 | ); 86 | case TAPage.empty: 87 | default: 88 | return const SizedBox(); 89 | } 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/feature/settings/widget/device_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 4 | import 'package:tesla_android/feature/settings/model/device_info.dart'; 5 | import 'package:tesla_android/feature/settings/bloc/device_info_cubit.dart'; 6 | import 'package:tesla_android/feature/settings/bloc/device_info_state.dart'; 7 | import 'package:tesla_android/feature/settings/widget/settings_section.dart'; 8 | import 'package:tesla_android/feature/settings/widget/settings_tile.dart'; 9 | 10 | class DeviceSettings extends SettingsSection { 11 | const DeviceSettings({super.key}) 12 | : super( 13 | name: "Device", 14 | icon: Icons.developer_board, 15 | ); 16 | 17 | @override 18 | Widget body(BuildContext context) { 19 | final textTheme = Theme.of(context).textTheme.bodyLarge; 20 | return BlocBuilder( 21 | builder: (context, state) { 22 | if (state is DeviceInfoStateInitial || state is DeviceInfoStateLoading) { 23 | return const Center( 24 | child: CircularProgressIndicator(), 25 | ); 26 | } else if (state is DeviceInfoStateLoaded) { 27 | return Column( 28 | crossAxisAlignment: CrossAxisAlignment.start, 29 | children: [ 30 | SettingsTile( 31 | icon: Icons.device_thermostat, 32 | title: 'CPU Temperature', 33 | trailing: Text("${state.deviceInfo.cpuTemperature}°C", 34 | style: textTheme)), 35 | const Padding( 36 | padding: EdgeInsets.all(TADimens.PADDING_S_VALUE), 37 | child: Text( 38 | 'CPU temperature should not exceed 80°C. Make sure the device is actively cooled and proper ventilation is provided.'), 39 | ), 40 | divider, 41 | SettingsTile( 42 | dense: false, 43 | icon: Icons.perm_device_info, 44 | title: 'Model', 45 | trailing: Text( 46 | state.deviceInfo.deviceName, 47 | style: textTheme, 48 | )), 49 | SettingsTile( 50 | dense: false, 51 | icon: Icons.developer_board_rounded, 52 | title: 'Serial Number', 53 | trailing: Text( 54 | state.deviceInfo.serialNumber, 55 | style: textTheme, 56 | )), 57 | SettingsTile( 58 | dense: false, 59 | icon: Icons.broadcast_on_home_sharp, 60 | title: 'CarPlay Module', 61 | trailing: Text( 62 | state.deviceInfo.isCarPlayDetected == 1 ? "Connected" : "Not connected", 63 | style: textTheme, 64 | )), 65 | SettingsTile( 66 | dense: false, 67 | icon: Icons.cell_tower, 68 | title: 'LTE Modem', 69 | trailing: Text( 70 | state.deviceInfo.isModemDetected == 1 ? "Detected" : "Not detected", 71 | style: textTheme, 72 | )), 73 | const Padding( 74 | padding: EdgeInsets.all(TADimens.PADDING_S_VALUE), 75 | child: Text( 76 | 'The LTE modem is considered detected when it is properly connected, and the gateway is reachable by Android. IP address 192.168.(0/8).1 is used for this check (Default for E3372 and Alcatel modems).'), 77 | ), 78 | SettingsTile( 79 | dense: false, 80 | icon: Icons.update, 81 | title: 'Release type', 82 | trailing: Text( 83 | state.deviceInfo.releaseType, 84 | style: textTheme, 85 | )), 86 | const Padding( 87 | padding: EdgeInsets.all(TADimens.PADDING_S_VALUE), 88 | child: Text( 89 | 'No support is provided for devices that are running pre-release (beta) software. You can switch your desired release type on https://beta.teslaandroid.com'), 90 | ), 91 | ], 92 | ); 93 | } else { 94 | return const SizedBox(); 95 | } 96 | }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/feature/settings/widget/sound_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:tesla_android/common/ui/constants/ta_colors.dart'; 4 | import 'package:tesla_android/common/ui/constants/ta_dimens.dart'; 5 | import 'package:tesla_android/feature/settings/bloc/audio_configuration_cubit.dart'; 6 | import 'package:tesla_android/feature/settings/bloc/audio_configuration_state.dart'; 7 | import 'package:tesla_android/feature/settings/widget/settings_section.dart'; 8 | import 'package:tesla_android/feature/settings/widget/settings_tile.dart'; 9 | 10 | class SoundSettings extends SettingsSection { 11 | const SoundSettings({super.key}) 12 | : super( 13 | name: "Audio", 14 | icon: Icons.speaker, 15 | ); 16 | 17 | @override 18 | Widget body(BuildContext context) { 19 | final cubit = BlocProvider.of(context); 20 | 21 | return BlocBuilder( 22 | builder: (context, state) { 23 | return Column( 24 | crossAxisAlignment: CrossAxisAlignment.start, 25 | children: [ 26 | SettingsTile( 27 | icon: Icons.speaker, 28 | title: 'Browser audio', 29 | subtitle: 'Disable if you intend to use Bluetooth audio', 30 | trailing: _audioStateSwitch(context, cubit, state)), 31 | divider, 32 | SettingsTile( 33 | icon: Icons.volume_down, 34 | title: 'Volume', 35 | trailing: _audioStateSlider(context, cubit, state), 36 | dense: false, 37 | ), 38 | const Padding( 39 | padding: EdgeInsets.all(TADimens.PADDING_S_VALUE), 40 | child: Text( 41 | 'If you plan to use browser audio continuously in conjunction with video playback, it\'s essential to note that it can be bandwidth-intensive. To optimize your experience, you may want to consider pairing your car with the Tesla Android device over Bluetooth, particularly if your Tesla is equipped with MCU2.'), 42 | ), 43 | const Padding( 44 | padding: EdgeInsets.all(TADimens.PADDING_S_VALUE), 45 | child: Text( 46 | 'In case you encounter a situation where the browser in your Tesla fails to produce sound, a simple reboot of the vehicle should resolve the issue. Please note that this is a known issue with the browser itself.'), 47 | ), 48 | ], 49 | ); 50 | }, 51 | ); 52 | } 53 | 54 | Widget _audioStateSwitch(BuildContext context, AudioConfigurationCubit cubit, 55 | AudioConfigurationState state) { 56 | if (state is AudioConfigurationStateSettingsFetched) { 57 | return Switch( 58 | value: state.isEnabled, 59 | onChanged: (value) { 60 | cubit.setState(value); 61 | }); 62 | } else if (state is AudioConfigurationStateSettingsUpdateInProgress || 63 | state is AudioConfigurationStateLoading) { 64 | return const CircularProgressIndicator(); 65 | } else if (state is AudioConfigurationStateError) { 66 | return const Text("Service error"); 67 | } 68 | return const SizedBox.shrink(); 69 | } 70 | 71 | Widget _audioStateSlider(BuildContext context, AudioConfigurationCubit cubit, 72 | AudioConfigurationState state) { 73 | if (state is AudioConfigurationStateSettingsFetched) { 74 | return Row( 75 | mainAxisAlignment: MainAxisAlignment.end, 76 | children: [ 77 | Text( 78 | "${state.volume} %", 79 | style: const TextStyle( 80 | fontSize: 14, 81 | fontWeight: FontWeight.bold, 82 | color: TAColors.settingsPrimaryColor), 83 | ), 84 | Slider( 85 | divisions: 15, 86 | min: 0, 87 | max: 150, 88 | value: state.volume.toDouble(), 89 | onChanged: (double value) { 90 | cubit.setVolume(value.toInt()); 91 | }, 92 | label: state.volume.toString(), 93 | ), 94 | ], 95 | ); 96 | } else if (state is AudioConfigurationStateSettingsUpdateInProgress || 97 | state is AudioConfigurationStateLoading) { 98 | return const CircularProgressIndicator(); 99 | } else if (state is AudioConfigurationStateError) { 100 | return const Text("Service error"); 101 | } 102 | return const SizedBox.shrink(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/feature/touchscreen/cubit/touchscreen_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'dart:js_interop'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:injectable/injectable.dart'; 6 | import 'package:tesla_android/common/utils/logger.dart'; 7 | import 'package:tesla_android/feature/touchscreen/model/virtual_touchscreen_slot_state.dart'; 8 | import 'package:tesla_android/feature/touchscreen/model/virtual_touchscreen_command.dart'; 9 | import 'package:web/web.dart' as web; 10 | 11 | @injectable 12 | class TouchscreenCubit extends Cubit with Logger { 13 | final List slotsState = 14 | VirtualTouchscreenSlotState.generateSlots(); 15 | TouchscreenCubit() : super(false); 16 | 17 | @override 18 | Future close() { 19 | log("close"); 20 | return super.close(); 21 | } 22 | 23 | void handlePointerDownEvent( 24 | PointerDownEvent event, BoxConstraints constraints, Size touchscreenSize) { 25 | final scaledPointerPosition = 26 | _scalePointerPosition(event.localPosition, constraints, touchscreenSize); 27 | final slot = _getFirstUnusedSlot(); 28 | if (slot == null) return; 29 | slot.trackingId = event.pointer; 30 | slot.position = scaledPointerPosition; 31 | 32 | log("Pointer down, assigned slot ${slot.slotIndex}, trackingId ${slot.trackingId}"); 33 | 34 | final command = VirtualTouchScreenCommand( 35 | absMtSlot: slot.slotIndex, 36 | absMtTrackingId: slot.trackingId, 37 | absMtPositionX: slot.position.dx.toInt(), 38 | absMtPositionY: slot.position.dy.toInt(), 39 | synReport: true); 40 | 41 | sendCommand(command); 42 | } 43 | 44 | void handlePointerMoveEvent( 45 | PointerMoveEvent event, BoxConstraints constraints, Size touchscreenSize) { 46 | final scaledPointerPosition = 47 | _scalePointerPosition(event.localPosition, constraints, touchscreenSize); 48 | final slot = _getSlotFromTrackingId(event.pointer); 49 | if (slot == null) return; 50 | slot.position = scaledPointerPosition; 51 | 52 | log("Pointer move, matched slot ${slot.slotIndex}, trackingId ${slot.trackingId}"); 53 | 54 | final command = VirtualTouchScreenCommand( 55 | absMtSlot: slot.slotIndex, 56 | absMtPositionX: slot.position.dx.toInt(), 57 | absMtPositionY: slot.position.dy.toInt(), 58 | synReport: true); 59 | 60 | sendCommand(command); 61 | } 62 | 63 | void handlePointerUpEvent(PointerEvent event, BoxConstraints constraints) { 64 | final slot = _getSlotFromTrackingId(event.pointer); 65 | if (slot == null) return; 66 | 67 | log("Pointer up, matched slot ${slot.slotIndex}, trackingId ${slot.trackingId}"); 68 | 69 | slot.trackingId = -1; 70 | 71 | final command = VirtualTouchScreenCommand( 72 | absMtSlot: slot.slotIndex, 73 | absMtTrackingId: slot.trackingId, 74 | synReport: true); 75 | 76 | sendCommand(command); 77 | } 78 | 79 | VirtualTouchscreenSlotState? _getFirstUnusedSlot() { 80 | for (final slot in slotsState..shuffle()) { 81 | if (slot.trackingId == -1) { 82 | return slot; 83 | } 84 | } 85 | return null; 86 | } 87 | 88 | VirtualTouchscreenSlotState? _getSlotFromTrackingId(int trackingId) { 89 | for (final slot in slotsState) { 90 | if (slot.trackingId == trackingId) { 91 | return slot; 92 | } 93 | } 94 | return null; 95 | } 96 | 97 | Offset _scalePointerPosition(Offset position, BoxConstraints constraints, Size touchscreenSize) { 98 | final scaleX = touchscreenSize.width / constraints.maxWidth; 99 | final scaleY = touchscreenSize.height / constraints.maxHeight; 100 | 101 | var scaledOffset = position.scale(scaleX, scaleY); 102 | 103 | if (scaledOffset.dx.isNegative) { 104 | scaledOffset = Offset(0, scaledOffset.dy); 105 | } 106 | 107 | if (scaledOffset.dy.isNegative) { 108 | scaledOffset = Offset(scaledOffset.dx, 0); 109 | } 110 | return scaledOffset; 111 | } 112 | 113 | void resetTouchScreen() { 114 | List commands = []; 115 | for (final slot in VirtualTouchscreenSlotState.generateSlots()) { 116 | commands.add(VirtualTouchScreenCommand( 117 | absMtSlot: slot.slotIndex, absMtTrackingId: slot.trackingId)); 118 | } 119 | sendCommands(commands: commands); 120 | } 121 | 122 | void sendCommands({required List commands}) { 123 | commands.add(VirtualTouchScreenCommand(synReport: true)); 124 | 125 | var message = ''; 126 | for (final command in commands) { 127 | message += command.build(); 128 | } 129 | web.window.postMessage(message.toJS, '*'.toJS); 130 | } 131 | 132 | void sendCommand(VirtualTouchScreenCommand command) { 133 | web.window.postMessage(command.build().toJS, '*'.toJS); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/feature/display/widget/display_view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:js_interop'; 3 | import 'dart:ui_web' as ui; 4 | 5 | import 'package:flavor/flavor.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_bloc/flutter_bloc.dart'; 8 | import 'package:tesla_android/common/di/ta_locator.dart'; 9 | import 'package:tesla_android/common/utils/logger.dart'; 10 | import 'package:tesla_android/feature/display/cubit/display_cubit.dart'; 11 | import 'package:tesla_android/feature/display/cubit/display_state.dart'; 12 | import 'package:tesla_android/feature/display/model/remote_display_state.dart'; 13 | import 'package:tesla_android/feature/settings/bloc/audio_configuration_cubit.dart'; 14 | import 'package:tesla_android/feature/settings/bloc/audio_configuration_state.dart'; 15 | import 'package:tesla_android/feature/settings/bloc/gps_configuration_cubit.dart'; 16 | import 'package:tesla_android/feature/settings/bloc/gps_configuration_state.dart'; 17 | import 'package:web/web.dart' as web; 18 | 19 | class DisplayView extends StatefulWidget { 20 | final DisplayRendererType type; 21 | 22 | const DisplayView({super.key, required this.type}); 23 | 24 | @override 25 | State createState() => _IframeViewState(); 26 | } 27 | 28 | class _IframeViewState extends State 29 | with Logger { 30 | static const String _src = "/android.html"; 31 | 32 | final web.HTMLIFrameElement _iframeElement = web.HTMLIFrameElement(); 33 | 34 | web.EventListener? _onMessageJs; 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | 40 | // Prepare the iframe element once. 41 | _iframeElement.src = _src; 42 | _iframeElement.style.border = 'none'; 43 | 44 | ui.platformViewRegistry.registerViewFactory( 45 | _src, 46 | (int viewId) => _iframeElement, 47 | ); 48 | } 49 | 50 | @override 51 | void dispose() { 52 | if (_onMessageJs != null) { 53 | web.window.removeEventListener('message', _onMessageJs!); 54 | _onMessageJs = null; 55 | } 56 | super.dispose(); 57 | } 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | return HtmlElementView( 62 | viewType: _src, 63 | onPlatformViewCreated: (_) { 64 | // Wire a one-time message listener on window to catch "iframeReady" 65 | // from android.html and then send the config payload. 66 | _onMessageJs = (web.Event e) { 67 | if (e is web.MessageEvent) { 68 | final data = e.data; 69 | // android.html posts "iframeReady" to parent on load 70 | if (data is String && data == "iframeReady") { 71 | _sendConfigToIframe(); 72 | } 73 | } 74 | }.toJS; 75 | 76 | web.window.addEventListener('message', _onMessageJs); 77 | }, 78 | ); 79 | } 80 | 81 | void _sendConfigToIframe() async { 82 | final flavor = getIt(); 83 | 84 | final displayCubit = context.read(); 85 | final gpsCubit = context.read(); 86 | final audioCubit = context.read(); 87 | 88 | await gpsCubit.fetchConfiguration(); 89 | await audioCubit.fetchConfiguration(); 90 | 91 | final displayState = displayCubit.state; 92 | final gpsState = gpsCubit.state; 93 | final audioState = audioCubit.state; 94 | 95 | if (displayState is! DisplayStateNormal) { 96 | // If display state isn't ready yet, try again on next frame. 97 | WidgetsBinding.instance.addPostFrameCallback((_) => _sendConfigToIframe()); 98 | return; 99 | } 100 | 101 | // Build config. (Audio values are harmless here; audio is handled in index.html.) 102 | final config = { 103 | 'audioWebsocketUrl': flavor.getString("audioWebSocket") ?? "", 104 | 'displayWebsocketUrl': flavor.getString("displayWebSocket") ?? "", 105 | 'gpsWebsocketUrl': flavor.getString("gpsWebSocket") ?? "", 106 | 'touchScreenWebsocketUrl': flavor.getString("touchscreenWebSocket") ?? "", 107 | 'isGPSEnabled': (gpsState is GPSConfigurationStateLoaded 108 | ? gpsState.isGPSEnabled 109 | : false) 110 | .toString(), 111 | 'isAudioEnabled': (audioState is AudioConfigurationStateSettingsFetched 112 | ? audioState.isEnabled 113 | : true) 114 | .toString(), 115 | 'audioVolume': (audioState is AudioConfigurationStateSettingsFetched 116 | ? (audioState.volume / 100) 117 | : 1.0) 118 | .toStringAsFixed(2), 119 | 'displayRenderer': widget.type.resourcePath(), 120 | 'displayBinaryType': widget.type.binaryType(), 121 | 'displayWidth': displayState.adjustedSize.width.toString(), 122 | 'displayHeight': displayState.adjustedSize.height.toString(), 123 | }; 124 | 125 | final cfgJson = jsonEncode(config).toJS; 126 | web.window.postMessage(cfgJson, '*'.toJS); 127 | _iframeElement.contentWindow?.postMessage(cfgJson, '*'.toJS); 128 | } 129 | } -------------------------------------------------------------------------------- /lib/feature/settings/bloc/display_configuration_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:tesla_android/common/utils/logger.dart'; 4 | import 'package:tesla_android/feature/display/model/remote_display_state.dart'; 5 | import 'package:tesla_android/feature/display/repository/display_repository.dart'; 6 | import 'package:tesla_android/feature/settings/bloc/display_configuration_state.dart'; 7 | 8 | @injectable 9 | class DisplayConfigurationCubit extends Cubit 10 | with Logger { 11 | final DisplayRepository _repository; 12 | 13 | RemoteDisplayState? _currentConfig; 14 | 15 | DisplayConfigurationCubit(this._repository) 16 | : super(DisplayConfigurationStateInitial()); 17 | 18 | void fetchConfiguration() async { 19 | if (!isClosed) emit(DisplayConfigurationStateLoading()); 20 | try { 21 | _currentConfig = await _repository.getDisplayState(); 22 | _emitCurrentConfig(); 23 | } catch (exception, stacktrace) { 24 | logException( 25 | exception: exception, 26 | stackTrace: stacktrace, 27 | ); 28 | if (!isClosed) { 29 | emit( 30 | DisplayConfigurationStateError(), 31 | ); 32 | } 33 | } 34 | } 35 | 36 | void setResponsiveness(bool newSetting) async { 37 | var config = _currentConfig; 38 | final isResponsive = newSetting ? 1 : 0; 39 | if (config != null) { 40 | config = config.copyWith(isResponsive: isResponsive); 41 | if (!isClosed) emit(DisplayConfigurationStateSettingsUpdateInProgress()); 42 | try { 43 | await _repository.updateDisplayConfiguration(config); 44 | _currentConfig = _currentConfig?.copyWith(isResponsive: isResponsive); 45 | _emitCurrentConfig(); 46 | } catch (exception, stackTrace) { 47 | logException(exception: exception, stackTrace: stackTrace); 48 | if (!isClosed) emit(DisplayConfigurationStateError()); 49 | } 50 | } else { 51 | log("_currentConfig not available"); 52 | } 53 | } 54 | 55 | void setResolution(DisplayResolutionModePreset newPreset) async { 56 | var config = _currentConfig; 57 | if (config != null) { 58 | config = config.updateResolution(newPreset: newPreset); 59 | if (!isClosed) emit(DisplayConfigurationStateSettingsUpdateInProgress()); 60 | try { 61 | await _repository.updateDisplayConfiguration(config); 62 | _currentConfig = _currentConfig?.copyWith(resolutionPreset: newPreset); 63 | _emitCurrentConfig(); 64 | } catch (exception, stackTrace) { 65 | logException(exception: exception, stackTrace: stackTrace); 66 | if (!isClosed) emit(DisplayConfigurationStateError()); 67 | } 68 | } else { 69 | log("_currentConfig not available"); 70 | } 71 | } 72 | 73 | void setRenderer(DisplayRendererType newType) async { 74 | var config = _currentConfig; 75 | if (config != null) { 76 | config = config.updateRenderer(newType: newType); 77 | if (!isClosed) emit(DisplayConfigurationStateSettingsUpdateInProgress()); 78 | try { 79 | await _repository.updateDisplayConfiguration(config); 80 | _currentConfig = _currentConfig?.copyWith(renderer: newType); 81 | _emitCurrentConfig(); 82 | } catch (exception, stackTrace) { 83 | logException(exception: exception, stackTrace: stackTrace); 84 | if (!isClosed) emit(DisplayConfigurationStateError()); 85 | } 86 | } else { 87 | log("_currentConfig not available"); 88 | } 89 | } 90 | 91 | void setQuality(DisplayQualityPreset newQuality) async { 92 | var config = _currentConfig; 93 | if (config != null) { 94 | config = config.updateQuality(newQuality: newQuality); 95 | if (!isClosed) emit(DisplayConfigurationStateSettingsUpdateInProgress()); 96 | try { 97 | await _repository.updateDisplayConfiguration(config); 98 | _currentConfig = _currentConfig?.copyWith(quality: newQuality); 99 | _emitCurrentConfig(); 100 | } catch (exception, stackTrace) { 101 | logException(exception: exception, stackTrace: stackTrace); 102 | if (!isClosed) emit(DisplayConfigurationStateError()); 103 | } 104 | } else { 105 | log("_currentConfig not available"); 106 | } 107 | } 108 | 109 | void setRefreshRate(DisplayRefreshRatePreset newRefreshRate) async { 110 | var config = _currentConfig; 111 | if (config != null) { 112 | config = config.updateRefreshRate(newRefreshRate: newRefreshRate); 113 | if (!isClosed) emit(DisplayConfigurationStateSettingsUpdateInProgress()); 114 | try { 115 | await _repository.updateDisplayConfiguration(config); 116 | _currentConfig = _currentConfig?.copyWith(refreshRate: newRefreshRate); 117 | _emitCurrentConfig(); 118 | } catch (exception, stackTrace) { 119 | logException(exception: exception, stackTrace: stackTrace); 120 | if (!isClosed) emit(DisplayConfigurationStateError()); 121 | } 122 | } else { 123 | log("_currentConfig not available"); 124 | } 125 | } 126 | 127 | void _emitCurrentConfig() { 128 | if (!isClosed) { 129 | emit(DisplayConfigurationStateSettingsFetched( 130 | resolutionPreset: _currentConfig!.resolutionPreset, 131 | renderer: _currentConfig!.renderer, 132 | isResponsive: _currentConfig!.isResponsive == 1, 133 | refreshRate: _currentConfig!.refreshRate, 134 | quality: _currentConfig!.quality, 135 | )); 136 | } 137 | } 138 | } 139 | --------------------------------------------------------------------------------