├── .codecov.yml ├── .github ├── FUNDING.yml ├── actions │ └── download-externals-action │ │ ├── action.yml │ │ └── download.sh └── workflows │ ├── android-build.yml │ ├── generate-usage.yml │ └── playstore-publish.yml ├── .gitignore ├── .idea ├── AndroidProjectSystem.xml ├── androidTestResultsUserPreferences.xml ├── artifacts │ ├── idriveconnectkit_js_0_7.xml │ ├── idriveconnectkit_jvm_0_7.xml │ └── idriveconnectkit_wasm_js_0_7.xml ├── assetWizardSettings.xml ├── codeStyleSettings.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── crowdin_settings.xml ├── deploymentTargetDropDown.xml ├── deploymentTargetSelector.xml ├── dynamic.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jsonSchemas.xml ├── kotlinc.xml ├── markdown-navigator.xml ├── markdown-navigator │ └── profiles_settings.xml ├── migrations.xml ├── misc.xml ├── navEditor.xml ├── runConfigurations.xml └── vcs.xml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── buildtools │ ├── ColoredOutput.gradle │ ├── SentryProperties.gradle │ ├── checksums.gradle │ ├── external.gradle │ ├── jacoco.gradle │ ├── preprocess_resources.gradle │ └── screenshots.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── hufman │ │ └── androidautoidrive │ │ ├── CalendarTest.kt │ │ ├── DexOpenerAndroidJUnitRunner.kt │ │ ├── EspressoHelpers.kt │ │ ├── ExampleInstrumentedTest.kt │ │ ├── InstrumentedTestNotificationApp.kt │ │ ├── InstrumentedTestNotificationEmoji.kt │ │ ├── InstrumentedTestVirtualDisplay.kt │ │ ├── MainScreenshotTest.kt │ │ ├── MockScenario.kt │ │ ├── MockViewModels.kt │ │ ├── NavigationParserTest.kt │ │ ├── PrivateScreenshotProcessor.kt │ │ └── WelcomeScreenshotTest.kt │ ├── androidTestGmap │ └── java │ │ └── me │ │ └── hufman │ │ └── androidautoidrive │ │ └── InstrumentedTestGMaps.kt │ ├── firebase │ └── java │ │ └── me │ │ └── hufman │ │ └── androidautoidrive │ │ └── Analytics.kt │ ├── gmap │ ├── AndroidManifest.xml │ ├── java │ │ └── me │ │ │ └── hufman │ │ │ └── androidautoidrive │ │ │ ├── carapp │ │ │ └── maps │ │ │ │ ├── GMapsController.kt │ │ │ │ ├── GMapsLocationSource.kt │ │ │ │ ├── GMapsNavController.kt │ │ │ │ ├── GMapsProjection.kt │ │ │ │ ├── MapAppService.kt │ │ │ │ └── MapToggleSettings.kt │ │ │ ├── maps │ │ │ ├── GMapsPlaceSearch.kt │ │ │ ├── GmapKeyValidation.kt │ │ │ └── PlaceSearchProvider.kt │ │ │ └── phoneui │ │ │ └── viewmodels │ │ │ └── MapSettingsModel.kt │ └── res │ │ ├── drawable-night │ │ └── ic_google.xml │ │ ├── drawable │ │ └── ic_google.xml │ │ ├── layout │ │ ├── fragment_map_settings.xml │ │ └── gmaps_projection.xml │ │ └── values │ │ └── styles.xml │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── carapplications │ │ │ ├── carinfo_unsigned │ │ │ │ └── rhmi │ │ │ │ │ ├── common │ │ │ │ │ ├── images.zip │ │ │ │ │ └── texts.zip │ │ │ │ │ └── mini │ │ │ │ │ └── images.zip │ │ │ └── id5statusbar_unsigned │ │ │ │ └── rhmi │ │ │ │ ├── bmw │ │ │ │ └── texts.zip │ │ │ │ └── mini │ │ │ │ └── texts.zip │ │ └── drawable │ │ │ ├── bmw.svg │ │ │ ├── carinfo_common.svg │ │ │ ├── carinfo_mini.svg │ │ │ ├── mini.svg │ │ │ ├── spotify_api_icons.svg │ │ │ └── tip_hand.svg │ ├── java │ │ └── me │ │ │ └── hufman │ │ │ └── androidautoidrive │ │ │ ├── A2DPBroadcastReceiver.kt │ │ │ ├── AnalyticsProvider.kt │ │ │ ├── AppSettings.kt │ │ │ ├── ApplicationCallbacks.kt │ │ │ ├── BackgroundInterruptionDetection.kt │ │ │ ├── CDSContentProvider.kt │ │ │ ├── CarAppWidgetAssetResources.kt │ │ │ ├── CarConnectionListener.kt │ │ │ ├── CarInformation.kt │ │ │ ├── CarProber.kt │ │ │ ├── CarThread.kt │ │ │ ├── ChassisCodes.kt │ │ │ ├── DeferredUpdate.kt │ │ │ ├── DispatcherProvider.kt │ │ │ ├── MainService.kt │ │ │ ├── Observable.kt │ │ │ ├── PhoneAppResources.kt │ │ │ ├── SecurityServiceThread.kt │ │ │ ├── UnicodeCleaner.kt │ │ │ ├── addons │ │ │ ├── AddonAppInfo.kt │ │ │ ├── AddonDiscovery.kt │ │ │ └── AddonsService.kt │ │ │ ├── calendar │ │ │ └── CalendarProvider.kt │ │ │ ├── carapp │ │ │ ├── AMAppList.kt │ │ │ ├── CarAppService.kt │ │ │ ├── FocusTriggerController.kt │ │ │ ├── FocusedStateTracker.kt │ │ │ ├── FullImageView.kt │ │ │ ├── InputState.kt │ │ │ ├── L.kt │ │ │ ├── RHMIActionAbort.kt │ │ │ ├── RHMIApplicationEtchBackground.kt │ │ │ ├── RHMIApplicationSwappable.kt │ │ │ ├── RHMIListFlow.kt │ │ │ ├── RHMIModelMultiSetter.kt │ │ │ ├── RHMIUtils.kt │ │ │ ├── ReadoutController.kt │ │ │ ├── SettingsToggleList.kt │ │ │ ├── assistant │ │ │ │ ├── AssistantApp.kt │ │ │ │ ├── AssistantAppInfo.kt │ │ │ │ ├── AssistantAppService.kt │ │ │ │ ├── AssistantController.kt │ │ │ │ └── AssistantControllerAndroid.kt │ │ │ ├── calendar │ │ │ │ ├── CalendarApp.kt │ │ │ │ ├── CalendarAppService.kt │ │ │ │ ├── RHMIDateUtils.kt │ │ │ │ ├── UpcomingDestination.kt │ │ │ │ └── views │ │ │ │ │ ├── CalendarDayView.kt │ │ │ │ │ ├── CalendarEventView.kt │ │ │ │ │ ├── CalendarMonthView.kt │ │ │ │ │ └── PermissionView.kt │ │ │ ├── carinfo │ │ │ │ ├── CarDetailedInfo.kt │ │ │ │ ├── CarInfoApp.kt │ │ │ │ ├── CarInfoAppService.kt │ │ │ │ ├── CarInformationDiscovery.kt │ │ │ │ ├── CarInformationDiscoveryService.kt │ │ │ │ └── views │ │ │ │ │ ├── CarDetailedView.kt │ │ │ │ │ └── CategoryView.kt │ │ │ ├── maps │ │ │ │ ├── FrameUpdater.kt │ │ │ │ ├── MapApp.kt │ │ │ │ ├── MapAppMode.kt │ │ │ │ ├── MapController.kt │ │ │ │ ├── VirtualDisplayScreenCapture.kt │ │ │ │ └── views │ │ │ │ │ ├── MenuView.kt │ │ │ │ │ ├── PlaceSearchView.kt │ │ │ │ │ └── SearchResultsView.kt │ │ │ ├── music │ │ │ │ ├── AVContextHandler.kt │ │ │ │ ├── ContextTracker.kt │ │ │ │ ├── GlobalMetadata.kt │ │ │ │ ├── MusicApp.kt │ │ │ │ ├── MusicAppMode.kt │ │ │ │ ├── MusicAppService.kt │ │ │ │ ├── MusicImageIDs.kt │ │ │ │ ├── TextScroller.kt │ │ │ │ ├── components │ │ │ │ │ ├── PlaylistItem.kt │ │ │ │ │ └── ProgressGauge.kt │ │ │ │ └── views │ │ │ │ │ ├── AppSwitcherView.kt │ │ │ │ │ ├── BrowsePageView.kt │ │ │ │ │ ├── BrowseView.kt │ │ │ │ │ ├── CustomActionsView.kt │ │ │ │ │ ├── EnqueuedView.kt │ │ │ │ │ ├── FilterInputView.kt │ │ │ │ │ ├── PlaybackView.kt │ │ │ │ │ └── SearchInputView.kt │ │ │ ├── navigation │ │ │ │ ├── NavigationParser.kt │ │ │ │ └── NavigationTrigger.kt │ │ │ └── notifications │ │ │ │ ├── ID5StatusbarApp.kt │ │ │ │ ├── NotificationApp.kt │ │ │ │ ├── NotificationAppService.kt │ │ │ │ ├── NotificationSettings.kt │ │ │ │ ├── PopupAutoCloser.kt │ │ │ │ ├── PopupHistory.kt │ │ │ │ ├── ReadoutInteractions.kt │ │ │ │ ├── ReplyController.kt │ │ │ │ ├── StatusbarController.kt │ │ │ │ └── views │ │ │ │ ├── DetailsView.kt │ │ │ │ ├── ID4PopupView.kt │ │ │ │ ├── ID5PopupView.kt │ │ │ │ ├── NotificationListView.kt │ │ │ │ ├── PermissionView.kt │ │ │ │ ├── PopupView.kt │ │ │ │ └── ReplyView.kt │ │ │ ├── cds │ │ │ ├── CDSConnection.kt │ │ │ ├── CDSDataSubscriptions.kt │ │ │ ├── CDSFlow.kt │ │ │ ├── CDSLiveData.kt │ │ │ ├── CDSMetrics.kt │ │ │ └── CDSValues.kt │ │ │ ├── connections │ │ │ ├── BclStatusListener.kt │ │ │ ├── BtStatus.kt │ │ │ ├── CarConnectionDebugging.kt │ │ │ └── UsbStatus.kt │ │ │ ├── maps │ │ │ ├── CarLocationProvider.kt │ │ │ └── MapPlaceSearch.kt │ │ │ ├── music │ │ │ ├── CustomAction.kt │ │ │ ├── MusicAction.kt │ │ │ ├── MusicAppDiscovery.kt │ │ │ ├── MusicAppInfo.kt │ │ │ ├── MusicBrowser.kt │ │ │ ├── MusicController.kt │ │ │ ├── MusicMetadata.kt │ │ │ ├── MusicSessions.kt │ │ │ ├── PlaybackPosition.kt │ │ │ ├── QueueMetadata.kt │ │ │ ├── RepeatMode.kt │ │ │ ├── SeekingController.kt │ │ │ ├── controllers │ │ │ │ ├── CombinedMusicAppController.kt │ │ │ │ ├── GenericMusicAppController.kt │ │ │ │ ├── MusicAppController.kt │ │ │ │ └── SpotifyAppController.kt │ │ │ └── spotify │ │ │ │ ├── SpotifyAuthStateManager.kt │ │ │ │ ├── SpotifyClientApiBuilderHelper.kt │ │ │ │ ├── SpotifyMusicMetadata.kt │ │ │ │ ├── SpotifyWebApi.kt │ │ │ │ └── TemporaryPlaylistState.kt │ │ │ ├── notifications │ │ │ ├── AudioPlayer.kt │ │ │ ├── CarNotification.kt │ │ │ ├── CarNotificationController.kt │ │ │ ├── NotificationListenerServiceImpl.kt │ │ │ ├── NotificationParser.kt │ │ │ ├── NotificationUpdaterController.kt │ │ │ └── NotificationsState.kt │ │ │ ├── phoneui │ │ │ ├── DonationRequest.kt │ │ │ ├── MusicAppDiscoveryThread.kt │ │ │ ├── MusicPlayerActivity.kt │ │ │ ├── NavHostActivity.kt │ │ │ ├── NavIntentActivity.kt │ │ │ ├── NestedGridView.kt │ │ │ ├── NestedListView.kt │ │ │ ├── PhoneUiUtils.kt │ │ │ ├── SpotifyAuthorizationActivity.kt │ │ │ ├── UIState.kt │ │ │ ├── WelcomeActivity.kt │ │ │ ├── adapters │ │ │ │ ├── DataBoundArrayAdapter.kt │ │ │ │ ├── DataBoundListAdapter.kt │ │ │ │ └── ObservableListCallback.kt │ │ │ ├── controllers │ │ │ │ ├── AddonAppListController.kt │ │ │ │ ├── CalendarPageController.kt │ │ │ │ ├── DependencyInfoController.kt │ │ │ │ ├── MusicAppListController.kt │ │ │ │ ├── MusicPlayerController.kt │ │ │ │ ├── NavSearchFilter.kt │ │ │ │ ├── NavigationSearchController.kt │ │ │ │ ├── NotificationPageController.kt │ │ │ │ ├── OverviewPageController.kt │ │ │ │ ├── PermissionsController.kt │ │ │ │ ├── QuickEditListController.kt │ │ │ │ ├── SendIntentController.kt │ │ │ │ └── SupportPageController.kt │ │ │ ├── fragments │ │ │ │ ├── AddonAppsListFragment.kt │ │ │ │ ├── AddonsPageFragment.kt │ │ │ │ ├── AnalyticsSettingsFragment.kt │ │ │ │ ├── AssistantAppsListFragment.kt │ │ │ │ ├── AssistantPageFragment.kt │ │ │ │ ├── AssistantPermissionsFragment.kt │ │ │ │ ├── BclStatusFragment.kt │ │ │ │ ├── CalendarCalendarsFragment.kt │ │ │ │ ├── CalendarEventsFragment.kt │ │ │ │ ├── CalendarPageFragment.kt │ │ │ │ ├── CalendarSettingsFragment.kt │ │ │ │ ├── CarAdvancedInfoFragment.kt │ │ │ │ ├── CarCapabilitiesFragment.kt │ │ │ │ ├── CarDrivingStatsFragment.kt │ │ │ │ ├── CarInfoPageFragment.kt │ │ │ │ ├── CarSummaryFragment.kt │ │ │ │ ├── ConnectionPageFragment.kt │ │ │ │ ├── ConnectionStatusFragment.kt │ │ │ │ ├── ConnectionSummaryFragment.kt │ │ │ │ ├── ConnectionTipsListFragment.kt │ │ │ │ ├── DependencyInfoFragment.kt │ │ │ │ ├── DependencySummaryFragment.kt │ │ │ │ ├── LanguageSettingsFragment.kt │ │ │ │ ├── MapQuickDestinationsFragment.kt │ │ │ │ ├── MapSettingsFragment.kt │ │ │ │ ├── MapsPageFragment.kt │ │ │ │ ├── MusicAdvancedSettingsFragment.kt │ │ │ │ ├── MusicAppsGridFragment.kt │ │ │ │ ├── MusicAppsListFragment.kt │ │ │ │ ├── MusicBrowseFragment.kt │ │ │ │ ├── MusicBrowsePageFragment.kt │ │ │ │ ├── MusicNowPlayingFragment.kt │ │ │ │ ├── MusicPageFragment.kt │ │ │ │ ├── MusicPermissionsFragment.kt │ │ │ │ ├── MusicQueueFragment.kt │ │ │ │ ├── MusicSearchFragment.kt │ │ │ │ ├── NavigationPageFragment.kt │ │ │ │ ├── NotificationPageFragment.kt │ │ │ │ ├── NotificationPermissionsFragment.kt │ │ │ │ ├── NotificationQuickRepliesFragment.kt │ │ │ │ ├── NotificationSettingsFragment.kt │ │ │ │ ├── OverviewConnectedFragment.kt │ │ │ │ ├── OverviewConnectingFragment.kt │ │ │ │ ├── OverviewPageFragment.kt │ │ │ │ ├── SettingsPageFragment.kt │ │ │ │ ├── SpotifyApiErrorDialog.kt │ │ │ │ ├── SpotifyDowngradeDialog.kt │ │ │ │ ├── SupportPageFragment.kt │ │ │ │ ├── TipsListFragment.kt │ │ │ │ └── welcome │ │ │ │ │ ├── WelcomeAnalyticsFragment.kt │ │ │ │ │ ├── WelcomeCompleteFragment.kt │ │ │ │ │ ├── WelcomeDependenciesFragment.kt │ │ │ │ │ ├── WelcomeFragment.kt │ │ │ │ │ ├── WelcomeMusicFragment.kt │ │ │ │ │ └── WelcomeNotificationFragment.kt │ │ │ └── viewmodels │ │ │ │ ├── AddonsViewModel.kt │ │ │ │ ├── AnalyticsSettingsModel.kt │ │ │ │ ├── BindingAdapters.kt │ │ │ │ ├── CalendarEventsModel.kt │ │ │ │ ├── CalendarSettingsModel.kt │ │ │ │ ├── CarCapabilitiesViewModel.kt │ │ │ │ ├── CarDrivingStatsModel.kt │ │ │ │ ├── CarSummaryModel.kt │ │ │ │ ├── ConnectionStatusModel.kt │ │ │ │ ├── ConnectionTipsModel.kt │ │ │ │ ├── DependencyInfoModel.kt │ │ │ │ ├── LanguageSettingsModel.kt │ │ │ │ ├── MapPageModel.kt │ │ │ │ ├── MusicActivityIconsModel.kt │ │ │ │ ├── MusicActivityModel.kt │ │ │ │ ├── MusicAdvancedSettingsModel.kt │ │ │ │ ├── MusicAppsViewModel.kt │ │ │ │ ├── NavigationStatusModel.kt │ │ │ │ ├── NotificationSettingsModel.kt │ │ │ │ ├── PermissionsModel.kt │ │ │ │ ├── SupportPageModel.kt │ │ │ │ ├── TipsModel.kt │ │ │ │ └── ViewModelProvider.kt │ │ │ └── utils │ │ │ ├── CachedData.kt │ │ │ ├── GraphicsHelpers.kt │ │ │ ├── GsonNullable.kt │ │ │ ├── PackageManagerCompat.kt │ │ │ ├── SparseArrayUtils.kt │ │ │ ├── TimeUtils.kt │ │ │ └── Utils.kt │ ├── res-facets │ │ ├── music_player │ │ │ └── layout │ │ │ │ ├── activity_musicplayer.xml │ │ │ │ ├── music_browse_container.xml │ │ │ │ ├── music_browse_listitem.xml │ │ │ │ ├── music_browsepage.xml │ │ │ │ ├── music_nowplaying.xml │ │ │ │ ├── music_queue_container.xml │ │ │ │ ├── music_queue_listitem.xml │ │ │ │ ├── music_queuepage.xml │ │ │ │ └── music_searchpage.xml │ │ ├── tips │ │ │ ├── drawable │ │ │ │ ├── pic_audioplayer_entrybutton_mini_id4.jpg │ │ │ │ ├── pic_audioplayer_entrybutton_mini_id5.jpg │ │ │ │ ├── pic_batterymode_mybmw.png │ │ │ │ ├── pic_batterymode_mymini.png │ │ │ │ ├── pic_batterymode_spotify.png │ │ │ │ ├── pic_bmw_bookmarks.jpg │ │ │ │ ├── pic_btapp_bmw.jpg │ │ │ │ ├── pic_btapp_mini.jpg │ │ │ │ ├── pic_connassistant_bmw.jpg │ │ │ │ ├── pic_connassistant_mini.jpg │ │ │ │ ├── pic_input_emoji_bmw.jpg │ │ │ │ ├── pic_input_emoji_mini.jpg │ │ │ │ ├── pic_mini_bookmarks.jpg │ │ │ │ ├── pic_music_bluetooth_mini_id5.jpg │ │ │ │ ├── pic_music_queuebutton_bmw_id5.jpg │ │ │ │ ├── pic_music_queuebutton_mini_id5.jpg │ │ │ │ ├── pic_news_ambutton_bmw.jpg │ │ │ │ ├── pic_news_ambutton_bmw_id5.jpg │ │ │ │ ├── pic_news_ambutton_mini_id5.jpg │ │ │ │ ├── pic_news_entrybutton_mini_id4.jpg │ │ │ │ ├── pic_news_list_bmw_id5.jpg │ │ │ │ ├── pic_news_list_mini_id4.jpg │ │ │ │ ├── pic_news_list_mini_id5.jpg │ │ │ │ ├── pic_news_popup_mini_id4.jpg │ │ │ │ ├── pic_news_read_bmw_id5.jpg │ │ │ │ ├── pic_news_read_mini_id5.jpg │ │ │ │ ├── pic_phone_chargemode.png │ │ │ │ ├── pic_phone_music_swipe.png │ │ │ │ ├── tip_batterymode_music.xml │ │ │ │ ├── tip_batterymode_mybmw.xml │ │ │ │ ├── tip_batterymode_mymini.xml │ │ │ │ ├── tip_bmw_bookmark_hand.xml │ │ │ │ ├── tip_bookmark_audioplayer_entrybutton_mini_id4.xml │ │ │ │ ├── tip_bookmark_audioplayer_entrybutton_mini_id5.xml │ │ │ │ ├── tip_bookmark_music_queuebutton_bmw_id5.xml │ │ │ │ ├── tip_bookmark_music_queuebutton_mini_id5.xml │ │ │ │ ├── tip_bookmark_news_ambutton_bmw_id5.xml │ │ │ │ ├── tip_bookmark_news_ambutton_mini_id5.xml │ │ │ │ ├── tip_bookmark_news_entrybutton_mini_id4.xml │ │ │ │ ├── tip_bookmark_news_list_bmw_id5.xml │ │ │ │ ├── tip_bookmark_news_list_mini_id4.xml │ │ │ │ ├── tip_bookmark_news_list_mini_id5.xml │ │ │ │ ├── tip_bookmark_news_popup_mini_id4.xml │ │ │ │ ├── tip_bookmark_news_read_bmw_id5.xml │ │ │ │ ├── tip_bookmark_news_read_mini_id5.xml │ │ │ │ ├── tip_btapp_bmw.xml │ │ │ │ ├── tip_btapp_mini.xml │ │ │ │ ├── tip_connassistant_bmw.xml │ │ │ │ ├── tip_connassistant_mini.xml │ │ │ │ ├── tip_hand.xml │ │ │ │ ├── tip_input_emoji_bmw.xml │ │ │ │ ├── tip_input_emoji_mini.xml │ │ │ │ ├── tip_mini_bookmark_hand.xml │ │ │ │ ├── tip_music_bluetooth_mini_id5.xml │ │ │ │ └── tip_phone_chargemode.xml │ │ │ └── layout │ │ │ │ ├── fragment_tip.xml │ │ │ │ └── fragment_tipslist.xml │ │ └── welcome │ │ │ └── layout │ │ │ ├── activity_welcome.xml │ │ │ ├── fragment_welcome.xml │ │ │ ├── fragment_welcome_analytics.xml │ │ │ ├── fragment_welcome_complete.xml │ │ │ ├── fragment_welcome_dependencies.xml │ │ │ ├── fragment_welcome_music.xml │ │ │ └── fragment_welcome_notification.xml │ ├── res │ │ ├── anim │ │ │ ├── slide_in_right.xml │ │ │ └── slide_out_left.xml │ │ ├── animator │ │ │ ├── equalizer_dance.xml │ │ │ ├── pulse_in.xml │ │ │ └── pulse_out.xml │ │ ├── color │ │ │ └── tip_hand_fill.xml │ │ ├── drawable-night │ │ │ └── dialog_holo_frame.xml │ │ ├── drawable-v24 │ │ │ └── tip_hand.xml │ │ ├── drawable │ │ │ ├── default_dot.xml │ │ │ ├── dialog_holo_frame.xml │ │ │ ├── empty_dot.xml │ │ │ ├── ic_baseline_directions_car_24.xml │ │ │ ├── ic_bell_feather.xml │ │ │ ├── ic_block_black_24dp.xml │ │ │ ├── ic_bluetooth_24dp.xml │ │ │ ├── ic_calendar_feather.xml │ │ │ ├── ic_car_sports.xml │ │ │ ├── ic_carinfo_common.png │ │ │ ├── ic_carinfo_mini.png │ │ │ ├── ic_cellphone_android.xml │ │ │ ├── ic_dancing_equalizer.xml │ │ │ ├── ic_dots_horiz_24dp.xml │ │ │ ├── ic_equalizer_black_24dp.xml │ │ │ ├── ic_error_outline_black_24dp.xml │ │ │ ├── ic_folder_open_black_24dp.xml │ │ │ ├── ic_github_feather.xml │ │ │ ├── ic_heart_feather.xml │ │ │ ├── ic_home_feather.xml │ │ │ ├── ic_launcher_cropped.png │ │ │ ├── ic_map_feather.xml │ │ │ ├── ic_mic_feather.xml │ │ │ ├── ic_music_feather.xml │ │ │ ├── ic_navigation_feather.xml │ │ │ ├── ic_notify.png │ │ │ ├── ic_outline_extension_24.xml │ │ │ ├── ic_pin_drop_24.xml │ │ │ ├── ic_pin_drop_red_24.xml │ │ │ ├── ic_play_circle_outline_black_24dp.xml │ │ │ ├── ic_radio_feather.xml │ │ │ ├── ic_search_black_24dp.xml │ │ │ ├── ic_send_feather.xml │ │ │ ├── ic_settings_feather.xml │ │ │ ├── ic_share_feather.xml │ │ │ ├── ic_slow_motion_video_black_24dp.xml │ │ │ ├── ic_tag_feather.xml │ │ │ ├── ic_test_music_app1.xml │ │ │ ├── ic_test_music_app2.xml │ │ │ ├── ic_usb_24dp.xml │ │ │ ├── logo_bmw.xml │ │ │ ├── logo_mini.xml │ │ │ ├── logo_toyota.xml │ │ │ ├── music_seek_back.xml │ │ │ ├── music_seek_back_heavy.xml │ │ │ ├── music_seek_forward.xml │ │ │ ├── music_seek_forward_heavy.xml │ │ │ ├── selected_dot.xml │ │ │ ├── side_nav_bar.xml │ │ │ ├── spotify_add_library.xml │ │ │ ├── spotify_added_library.xml │ │ │ ├── spotify_repeat_off.xml │ │ │ ├── spotify_repeat_on.xml │ │ │ ├── spotify_repeat_one.xml │ │ │ ├── spotify_shuffle_off.xml │ │ │ ├── spotify_shuffle_on.xml │ │ │ ├── spotify_start_radio.xml │ │ │ └── tab_selector.xml │ │ ├── layout-land │ │ │ └── activity_navhost.xml │ │ ├── layout │ │ │ ├── activity_navhost.xml │ │ │ ├── activity_navintent.xml │ │ │ ├── addonapp_listitem.xml │ │ │ ├── assistantapp_listitem.xml │ │ │ ├── calendar_listitem.xml │ │ │ ├── calendarevent_listitem.xml │ │ │ ├── dialog_error.xml │ │ │ ├── fragment_addon_applist.xml │ │ │ ├── fragment_addonpage.xml │ │ │ ├── fragment_analytics_settings.xml │ │ │ ├── fragment_assistant_applist.xml │ │ │ ├── fragment_assistant_permissions.xml │ │ │ ├── fragment_assistantpage.xml │ │ │ ├── fragment_bcl_status.xml │ │ │ ├── fragment_calendar_calendars.xml │ │ │ ├── fragment_calendar_events.xml │ │ │ ├── fragment_calendar_settings.xml │ │ │ ├── fragment_calendarpage.xml │ │ │ ├── fragment_car_advancedinfo.xml │ │ │ ├── fragment_car_capabilities.xml │ │ │ ├── fragment_car_drivingstats.xml │ │ │ ├── fragment_car_summary.xml │ │ │ ├── fragment_carinfopage.xml │ │ │ ├── fragment_connection_status.xml │ │ │ ├── fragment_connection_summary.xml │ │ │ ├── fragment_connectionpage.xml │ │ │ ├── fragment_dependencyinfo.xml │ │ │ ├── fragment_dependencysummary.xml │ │ │ ├── fragment_language_settings.xml │ │ │ ├── fragment_map_quickdestinations.xml │ │ │ ├── fragment_mapspage.xml │ │ │ ├── fragment_music_advancedsettings.xml │ │ │ ├── fragment_music_appgrid.xml │ │ │ ├── fragment_music_applist.xml │ │ │ ├── fragment_music_permissions.xml │ │ │ ├── fragment_musicpage.xml │ │ │ ├── fragment_nav_header.xml │ │ │ ├── fragment_navigationpage.xml │ │ │ ├── fragment_notification_permissions.xml │ │ │ ├── fragment_notification_quickreplies.xml │ │ │ ├── fragment_notification_settings.xml │ │ │ ├── fragment_notificationpage.xml │ │ │ ├── fragment_overview_connected.xml │ │ │ ├── fragment_overview_connecting.xml │ │ │ ├── fragment_overviewpage.xml │ │ │ ├── fragment_settingspage.xml │ │ │ ├── fragment_supportpage.xml │ │ │ ├── musicapp_griditem.xml │ │ │ ├── musicapp_listitem.xml │ │ │ ├── navigation_listitem.xml │ │ │ └── quickeditlist_listitem.xml │ │ ├── menu │ │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ ├── mipmap │ │ │ └── ic_launcher.png │ │ ├── navigation │ │ │ └── nav_main.xml │ │ ├── raw │ │ │ ├── gmaps_style_aubergine.json │ │ │ ├── gmaps_style_midnight_commander.json │ │ │ └── gmaps_style_night.json │ │ ├── values-b+zh+Hans │ │ │ └── strings.xml │ │ ├── values-de │ │ │ └── strings.xml │ │ ├── values-eo │ │ │ └── strings.xml │ │ ├── values-es │ │ │ └── strings.xml │ │ ├── values-fr │ │ │ └── strings.xml │ │ ├── values-he │ │ │ └── strings.xml │ │ ├── values-hi │ │ │ └── strings.xml │ │ ├── values-hu │ │ │ └── strings.xml │ │ ├── values-it │ │ │ └── strings.xml │ │ ├── values-ja │ │ │ └── strings.xml │ │ ├── values-night │ │ │ └── colors.xml │ │ ├── values-nl │ │ │ └── strings.xml │ │ ├── values-pl │ │ │ └── strings.xml │ │ ├── values-pt │ │ │ └── strings.xml │ │ ├── values-ro │ │ │ └── strings.xml │ │ ├── values-ru │ │ │ └── strings.xml │ │ ├── values-sv │ │ │ └── strings.xml │ │ ├── values-tr │ │ │ └── strings.xml │ │ └── values │ │ │ ├── attrs.xml │ │ │ ├── choices.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ └── resources │ │ └── jacoco-agent.properties │ ├── mapbox │ ├── AndroidManifest.xml │ ├── java │ │ └── me │ │ │ └── hufman │ │ │ └── androidautoidrive │ │ │ ├── carapp │ │ │ └── maps │ │ │ │ ├── MapAppService.kt │ │ │ │ ├── MapToggleSettings.kt │ │ │ │ ├── MapboxController.kt │ │ │ │ ├── MapboxLocationSource.kt │ │ │ │ ├── MapboxNavController.kt │ │ │ │ ├── MapboxProjection.kt │ │ │ │ └── MapboxSettings.kt │ │ │ ├── maps │ │ │ ├── MapboxPlaceSearch.kt │ │ │ ├── MapboxTokenValidation.kt │ │ │ └── PlaceSearchProvider.kt │ │ │ └── phoneui │ │ │ └── viewmodels │ │ │ └── MapSettingsModel.kt │ └── res │ │ ├── drawable │ │ ├── ic_mapbox_bearing.xml │ │ └── ic_mapbox_shadow.xml │ │ ├── layout │ │ ├── fragment_map_settings.xml │ │ └── mapbox_projection.xml │ │ └── values │ │ ├── colors.xml │ │ └── styles.xml │ ├── nomap │ ├── java │ │ └── me │ │ │ └── hufman │ │ │ └── androidautoidrive │ │ │ ├── carapp │ │ │ └── maps │ │ │ │ └── MapToggleSettings.kt │ │ │ ├── maps │ │ │ ├── AddressPlaceSearch.kt │ │ │ └── PlaceSearchProvider.kt │ │ │ └── phoneui │ │ │ └── viewmodels │ │ │ └── MapSettingsModel.kt │ └── res │ │ └── layout │ │ └── fragment_map_settings.xml │ ├── nonalytics │ └── java │ │ └── me │ │ └── hufman │ │ └── androidautoidrive │ │ └── Analytics.kt │ ├── play │ └── AndroidManifest.xml │ ├── sentry │ ├── AndroidManifest.xml │ └── java │ │ └── me │ │ └── hufman │ │ └── androidautoidrive │ │ └── Analytics.kt │ ├── test │ ├── java │ │ ├── android │ │ │ ├── app │ │ │ │ ├── ActivityOptions.java │ │ │ │ ├── AppOpsManager.kt │ │ │ │ └── Person.kt │ │ │ ├── content │ │ │ │ ├── Intent.kt │ │ │ │ └── IntentFilter.kt │ │ │ ├── graphics │ │ │ │ └── Bitmap.kt │ │ │ ├── location │ │ │ │ └── Location.kt │ │ │ ├── os │ │ │ │ ├── Build.kt │ │ │ │ ├── Looper.kt │ │ │ │ ├── Process.kt │ │ │ │ └── SystemClock.kt │ │ │ └── util │ │ │ │ ├── Base64.kt │ │ │ │ ├── Log.kt │ │ │ │ ├── LruCache.kt │ │ │ │ └── SparseArray.kt │ │ └── me │ │ │ └── hufman │ │ │ └── androidautoidrive │ │ │ ├── AppSettingsTest.kt │ │ │ ├── AssetManagerTest.kt │ │ │ ├── AssistantAppTest.kt │ │ │ ├── BackgroundInterruptionDetectionTest.kt │ │ │ ├── CDSDataTest.kt │ │ │ ├── CachedDataTest.kt │ │ │ ├── CarInformationDiscoveryTest.kt │ │ │ ├── CarInformationTest.kt │ │ │ ├── ChassisCodeTest.kt │ │ │ ├── CoroutineTestRule.kt │ │ │ ├── DeferredUpdateTest.kt │ │ │ ├── DonationRequestTest.kt │ │ │ ├── EmojiCleanerTest.kt │ │ │ ├── FocusedStateTrackerTest.kt │ │ │ ├── FullImageViewTest.kt │ │ │ ├── MockBMWRemotingServer.kt │ │ │ ├── MockSelfReturningAnswer.kt │ │ │ ├── NavTriggerTest.kt │ │ │ ├── RHMIApplicationEtchBackgroundTest.kt │ │ │ ├── RHMIApplicationSwappableTest.kt │ │ │ ├── ReflectUtils.kt │ │ │ ├── RhmiListFlowTest.kt │ │ │ ├── SettingsToggleListTest.kt │ │ │ ├── TestCoroutineRule.kt │ │ │ ├── TimeUtilsTest.kt │ │ │ ├── addons │ │ │ └── AddonDiscoveryTest.kt │ │ │ ├── calendar │ │ │ ├── CalendarAppTest.kt │ │ │ ├── CalendarParsingTest.kt │ │ │ ├── RHMIDateUtilsTest.kt │ │ │ └── UpcomingDestinationTest.kt │ │ │ ├── carinfo │ │ │ └── CarInfoAppTest.kt │ │ │ ├── maps │ │ │ ├── CdsLocationProviderTest.kt │ │ │ ├── LatLongTest.kt │ │ │ ├── PlaceSearchViewTest.kt │ │ │ └── SearchResultsViewTest.kt │ │ │ ├── music │ │ │ ├── AVContextTest.kt │ │ │ ├── AppModeTest.kt │ │ │ ├── ContextTrackerTest.kt │ │ │ ├── CustomActionTest.kt │ │ │ ├── MusicAppTest.kt │ │ │ ├── MusicMetadataTest.kt │ │ │ ├── SeekTest.kt │ │ │ ├── SpotifyAuthStateManagerTest.kt │ │ │ ├── SpotifyWebApiTest.kt │ │ │ ├── TextScrollerTest.kt │ │ │ └── controllers │ │ │ │ ├── CombinedMusicAppControllerTest.kt │ │ │ │ ├── GenericMusicAppControllerTest.kt │ │ │ │ └── SpotifyMusicAppControllerTest.kt │ │ │ ├── notifications │ │ │ ├── CarNotificationControllerTest.kt │ │ │ ├── NotificationAppTest.kt │ │ │ ├── NotificationSettingsTest.kt │ │ │ ├── NotificationUpdaterTest.kt │ │ │ ├── ReadoutControllerTest.kt │ │ │ └── StatusbarAppTest.kt │ │ │ └── phoneui │ │ │ ├── CarCapabilitiesModelTest.kt │ │ │ ├── CarSummaryModelTest.kt │ │ │ ├── ConnectionStatusModelTest.kt │ │ │ ├── DependencyInfoModelTest.kt │ │ │ ├── LanguageSettingsModelTest.kt │ │ │ ├── MusicActivityIconsModelTest.kt │ │ │ ├── MusicActivityModelTest.kt │ │ │ ├── MusicAdvancedSettingsModelTest.kt │ │ │ ├── NavigationSearchControllerTest.kt │ │ │ ├── NavigationStatusModelTest.kt │ │ │ ├── NotificationSettingsModelTest.kt │ │ │ ├── PermissionsModelTest.kt │ │ │ └── QuickEditListControllerTest.kt │ └── resources │ │ └── mockito-extensions │ │ └── org.mockito.plugins.MockMaker │ ├── testGmap │ └── java │ │ └── me │ │ └── hufman │ │ └── androidautoidrive │ │ ├── GMapsPlaceSearchTest.kt │ │ ├── MapAppTest.kt │ │ └── MapSettingsModelTest.kt │ ├── testMapbox │ └── java │ │ └── me │ │ └── hufman │ │ └── androidautoidrive │ │ ├── MapSettingsModelTest.kt │ │ └── maps │ │ └── MapboxPlaceSearchTest.kt │ └── testNomap │ └── java │ └── me │ └── hufman │ └── androidautoidrive │ └── MapSettingsModelTest.kt ├── build.gradle ├── crowdin.yml ├── docs ├── _config.yml ├── _includes │ ├── footer.html │ ├── head.html │ ├── header.html │ ├── icon-github.html │ ├── icon-github.svg │ ├── icon-twitter.html │ └── icon-twitter.svg ├── _layouts │ ├── default.html │ ├── page.html │ └── post.html ├── _sass │ ├── _base.scss │ ├── _gallery.scss │ ├── _layout.scss │ ├── _normalize.scss │ └── _syntax-highlighting.scss ├── connection.md ├── css │ └── main.scss ├── developers.md ├── faq.md ├── gallery.md ├── images │ ├── bmw-connected-subscription.png │ ├── demo-actions.gif │ ├── demo-actions.mp4 │ ├── demo-applist.gif │ ├── demo-applist.mp4 │ ├── demo-assistant.mp4 │ ├── demo-browse.gif │ ├── demo-browse.mp4 │ ├── demo-connwizard.mp4 │ ├── demo-convert.sh │ ├── demo-filter.gif │ ├── demo-filter.mp4 │ ├── demo-notifications.gif │ ├── demo-notifications.mp4 │ ├── demo-nowplaying.gif │ ├── demo-nowplaying.mp4 │ ├── developer-buildmenu.png │ ├── developer-externalmissing.png │ ├── developer-importproject.png │ ├── developer-repourl.png │ ├── developer-runmenu.png │ ├── developer-variants.png │ ├── memory-applist.png │ ├── memory-choose.png │ ├── memory-killing.png │ ├── memory-success.png │ ├── screenshot-app.png │ ├── screenshot-connection.png │ ├── screenshot-medialist.jpg │ ├── screenshot-musicapplist.jpg │ ├── screenshot-musicbrowse.jpg │ ├── screenshot-musicplayback.jpg │ ├── screenshot-phoneapps.jpg │ ├── usb-charging.png │ ├── usb-charging2.png │ └── usb-transfer.png └── support.html ├── external └── README.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystore.jks.enc ├── playstore ├── .gitignore ├── README ├── generate.sh └── res │ ├── values-b+zh+Hans │ ├── ISO │ └── strings.xml │ ├── values-de │ ├── ISO │ └── strings.xml │ ├── values-eo │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fr │ ├── ISO │ └── strings.xml │ ├── values-he │ └── strings.xml │ ├── values-hi │ └── strings.xml │ ├── values-hu │ ├── ISO │ └── strings.xml │ ├── values-it │ ├── ISO │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-nl │ ├── ISO │ └── strings.xml │ ├── values-pl │ ├── ISO │ └── strings.xml │ ├── values-pt │ └── strings.xml │ ├── values-ro │ ├── ISO │ └── strings.xml │ ├── values-ru │ ├── ISO │ └── strings.xml │ ├── values-sv │ ├── ISO │ └── strings.xml │ ├── values-tr │ └── strings.xml │ └── values │ ├── ISO │ └── strings.xml ├── settings.gradle ├── spotify-app-remote ├── build.gradle └── spotify-app-remote-release-0.7.2.aar └── usage ├── .gitignore ├── car_report.html ├── generate.py └── localsettings.py /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | 10 | comment: 11 | layout: "reach,diff,files,footer" 12 | require_changes: true 13 | 14 | github_checks: 15 | annotations: false -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://www.buymeacoffee.com/q4jvoxz'] 2 | -------------------------------------------------------------------------------- /.github/actions/download-externals-action/action.yml: -------------------------------------------------------------------------------- 1 | name: "Download Build Dependencies" 2 | description: "Downloads external dependencies to build AndroidAutoIdrive" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - run: ${{ github.action_path }}/download.sh 7 | shell: bash 8 | -------------------------------------------------------------------------------- /.github/workflows/generate-usage.yml: -------------------------------------------------------------------------------- 1 | name: usage 2 | 3 | on: 4 | schedule: 5 | - cron: '15 15 * * *' 6 | push: 7 | branches: ['usage'] 8 | 9 | jobs: 10 | updateUsage: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Checkout the project 14 | uses: actions/checkout@v4 15 | 16 | - name: Generate usage information 17 | run: cd usage && python3 generate.py 18 | env: 19 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 20 | SENTRY_ISSUE_ID: ${{ secrets.SENTRY_ISSUE_ID }} 21 | 22 | - name: Copy to output dir 23 | run: cd usage && mkdir dist && cp *.json *.html dist 24 | 25 | - name: Upload to S3 26 | uses: shallwefootball/s3-upload-action@v1.3.0 27 | with: 28 | source_dir: usage/dist 29 | destination_dir: aaidrive/usage 30 | aws_bucket: bimmergestalt 31 | aws_key_id: ${{ secrets.AWS_KEY_ID }} 32 | aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/gradle.xml 5 | /.idea/modules.xml 6 | /.idea/tasks.xml 7 | /.idea/workspace.xml 8 | /.idea/libraries 9 | /.idea/caches 10 | /.idea/compiler.xml 11 | /.idea/jarRepositories.xml 12 | .DS_Store 13 | /build 14 | /captures 15 | /external 16 | /app/src/test/resources/*.xml 17 | .externalNativeBuild 18 | sentry.properties 19 | _site 20 | .sass-cache 21 | spotify-app-remote/build -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/androidTestResultsUserPreferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/artifacts/idriveconnectkit_js_0_7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/IDriveConnectKit/core/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/idriveconnectkit_jvm_0_7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/IDriveConnectKit/core/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/idriveconnectkit_wasm_js_0_7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/IDriveConnectKit/core/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/crowdin_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/markdown-navigator/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Walter Huf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | 3 | # copied during build from values-he 4 | src/main/res/values-iw -------------------------------------------------------------------------------- /app/buildtools/SentryProperties.gradle: -------------------------------------------------------------------------------- 1 | /** Automatically build the app's sentry.properties file from gradle.properties */ 2 | 3 | project.task("sentryProperties") { 4 | inputs.property("AndroidAutoIdrive_SentryDsn") { 5 | AndroidAutoIdrive_SentryDsn 6 | } 7 | outputs.file("src/main/resources/sentry.properties") 8 | doLast { 9 | file("src/main/resources/sentry.properties").text = """dsn=${System.env.AndroidAutoIdrive_SentryDsn ? System.env.AndroidAutoIdrive_SentryDsn : AndroidAutoIdrive_SentryDsn} 10 | """ 11 | } 12 | } 13 | 14 | project.afterEvaluate { 15 | tasks.each { 16 | if (it.name.startsWith("process")) { 17 | it.dependsOn(tasks.named("sentryProperties")) 18 | it.mustRunAfter(tasks.named("sentryProperties")) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/buildtools/checksums.gradle: -------------------------------------------------------------------------------- 1 | import org.apache.commons.codec.digest.DigestUtils 2 | 3 | buildscript { 4 | repositories { 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath group: 'commons-codec', name: 'commons-codec', version: '1.12' 9 | } 10 | } 11 | 12 | task "generateApkChecksums" { 13 | def packages = fileTree(dir: projectDir, includes: ["**/*.apk"]) 14 | inputs.files(packages) 15 | outputs.files(packages.collect { new File(it.absolutePath + ".sha1") } ) 16 | 17 | doLast { 18 | packages.each {File file -> 19 | File fileSha1 = new File(file.absolutePath + ".sha1") 20 | String checksum 21 | file.withInputStream { ins -> checksum = DigestUtils.sha1Hex(ins) } 22 | fileSha1.text = checksum + " " + file.name + "\n" 23 | } 24 | } 25 | } 26 | 27 | 28 | project.afterEvaluate { 29 | tasks.each { 30 | if (it.name.startsWith("assemble")) { 31 | // generateApkChecksums.inputs.files(it.outputs.files) 32 | it.finalizedBy(generateApkChecksums) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/buildtools/preprocess_resources.gradle: -------------------------------------------------------------------------------- 1 | 2 | // Android chooses to cater to 20-year-old Java compatibility 3 | // instead of fixing their bug and following the ISO standard language code 4 | // and the fractured device market means some phones ignore the incorrect code 5 | // and so both language resources must be provided 6 | task copyHebrewResources(type: Copy) { 7 | from 'src/main/res/values-he/strings.xml' 8 | into 'src/main/res/values-iw' 9 | } 10 | preBuild.dependsOn copyHebrewResources -------------------------------------------------------------------------------- /app/src/androidTest/java/me/hufman/androidautoidrive/CalendarTest.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import me.hufman.androidautoidrive.calendar.CalendarProvider 6 | import org.junit.Ignore 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | @RunWith(AndroidJUnit4::class) 11 | class CalendarTest { 12 | @Test 13 | @Ignore("Debugging") 14 | fun testCalendar() { 15 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 16 | val calendar = CalendarProvider(appContext, AppSettingsViewer()) 17 | calendar.getEvents(2021, 12, 31) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/me/hufman/androidautoidrive/DexOpenerAndroidJUnitRunner.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | import com.github.tmurakami.dexopener.DexOpener 7 | 8 | 9 | class DexOpenerAndroidJUnitRunner: AndroidJUnitRunner() { 10 | @Throws(ClassNotFoundException::class, IllegalAccessException::class, InstantiationException::class) 11 | override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application? { 12 | DexOpener.install(this) // Call me first! 13 | return super.newApplication(cl, className, context) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/me/hufman/androidautoidrive/PrivateScreenshotProcessor.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive 2 | 3 | import android.content.Context 4 | import androidx.test.runner.screenshot.BasicScreenCaptureProcessor 5 | import androidx.test.runner.screenshot.ScreenCapture 6 | import java.io.File 7 | import java.util.* 8 | 9 | class PrivateScreenshotProcessor(context: Context): BasicScreenCaptureProcessor() { 10 | init { 11 | this.mDefaultScreenshotPath = File( 12 | context.getExternalFilesDir(null)!!.absolutePath, 13 | "screenshots" 14 | ) 15 | } 16 | 17 | override fun getFilename(prefix: String): String = prefix 18 | override fun process(capture: ScreenCapture?): String { 19 | val imageFolder = mDefaultScreenshotPath 20 | val filename = if (capture!!.name == null) defaultFilename else getFilename(capture.name) 21 | val suffix = "." + capture.format.toString().lowercase(Locale.getDefault()) 22 | val imageFile = File(imageFolder, filename + suffix) 23 | println("Saving to $imageFile") 24 | return super.process(capture) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/gmap/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/gmap/java/me/hufman/androidautoidrive/carapp/maps/GMapsLocationSource.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.maps 2 | 3 | import android.location.Location 4 | import com.google.android.gms.maps.LocationSource 5 | 6 | class GMapsLocationSource: LocationSource { 7 | private var listener: LocationSource.OnLocationChangedListener? = null 8 | var location: Location? = null 9 | private set 10 | 11 | override fun activate(p0: LocationSource.OnLocationChangedListener) { 12 | listener = p0 13 | } 14 | 15 | override fun deactivate() { 16 | listener = null 17 | } 18 | 19 | fun onLocationUpdate(location: Location) { 20 | listener ?: return 21 | 22 | this.location = location 23 | listener?.onLocationChanged(location) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/gmap/java/me/hufman/androidautoidrive/carapp/maps/MapToggleSettings.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.maps 2 | 3 | import me.hufman.androidautoidrive.AppSettings 4 | 5 | object MapToggleSettings { 6 | val settings = listOf( 7 | AppSettings.KEYS.MAP_INVERT_SCROLL, 8 | AppSettings.KEYS.MAP_BUILDINGS, 9 | AppSettings.KEYS.MAP_TRAFFIC, 10 | ) 11 | } -------------------------------------------------------------------------------- /app/src/gmap/java/me/hufman/androidautoidrive/maps/PlaceSearchProvider.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.maps 2 | 3 | import android.content.Context 4 | import me.hufman.androidautoidrive.CarInformation 5 | 6 | class PlaceSearchProvider(private val context: Context) { 7 | fun getInstance(): MapPlaceSearch { 8 | val locationProvider = CdsLocationProvider(CarInformation.cachedCdsData, false) 9 | return GMapsPlaceSearch.getInstance(context, locationProvider) 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/gmap/res/drawable-night/ic_google.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/gmap/res/drawable/ic_google.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/gmap/res/layout/gmaps_projection.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /app/src/gmap/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/assets/.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't try to do any text parsing on these files 2 | # They need to match a whitelist of SHA256 checksums 3 | 4 | * -text -------------------------------------------------------------------------------- /app/src/main/assets/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | basecoreOnlineServices 3 | bmwone 4 | calendar 5 | cdsbaseapp 6 | multimedia 7 | news 8 | smartthings 9 | spotify -------------------------------------------------------------------------------- /app/src/main/assets/carapplications/carinfo_unsigned/rhmi/common/images.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BimmerGestalt/AAIdrive/afd9c6f5bc4cf3fc0029eb00c7a61adcb1164837/app/src/main/assets/carapplications/carinfo_unsigned/rhmi/common/images.zip -------------------------------------------------------------------------------- /app/src/main/assets/carapplications/carinfo_unsigned/rhmi/common/texts.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BimmerGestalt/AAIdrive/afd9c6f5bc4cf3fc0029eb00c7a61adcb1164837/app/src/main/assets/carapplications/carinfo_unsigned/rhmi/common/texts.zip -------------------------------------------------------------------------------- /app/src/main/assets/carapplications/carinfo_unsigned/rhmi/mini/images.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BimmerGestalt/AAIdrive/afd9c6f5bc4cf3fc0029eb00c7a61adcb1164837/app/src/main/assets/carapplications/carinfo_unsigned/rhmi/mini/images.zip -------------------------------------------------------------------------------- /app/src/main/assets/carapplications/id5statusbar_unsigned/rhmi/bmw/texts.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BimmerGestalt/AAIdrive/afd9c6f5bc4cf3fc0029eb00c7a61adcb1164837/app/src/main/assets/carapplications/id5statusbar_unsigned/rhmi/bmw/texts.zip -------------------------------------------------------------------------------- /app/src/main/assets/carapplications/id5statusbar_unsigned/rhmi/mini/texts.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BimmerGestalt/AAIdrive/afd9c6f5bc4cf3fc0029eb00c7a61adcb1164837/app/src/main/assets/carapplications/id5statusbar_unsigned/rhmi/mini/texts.zip -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/AnalyticsProvider.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive 2 | 3 | import android.content.Context 4 | import me.hufman.androidautoidrive.music.MusicAppInfo 5 | 6 | interface AnalyticsProvider { 7 | fun init(context: Context) 8 | fun reportMusicAppProbe(appInfo: MusicAppInfo) 9 | fun reportCarProbeFailure(port: Int, message: String?, throwable: Throwable?) 10 | fun reportCarProbeDiscovered(port: Int?, vehicleType: String?, hmiType: String?) 11 | fun reportCarCapabilities(capabilities: Map) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/CarAppWidgetAssetResources.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive 2 | 3 | import android.content.Context 4 | import io.bimmergestalt.idriveconnectkit.android.CarAppAssetResources 5 | import java.io.InputStream 6 | import java.util.* 7 | 8 | class CarAppWidgetAssetResources(context: Context, name: String): CarAppAssetResources(context, name) { 9 | // BMWOne has a widgets DB to upload 10 | fun getWidgetsDB(brand: String): InputStream? { 11 | return loadFile("carapplications/$name/rhmi/${brand.lowercase(Locale.ROOT)}/widgetdb.zip") ?: 12 | loadFile("carapplications/$name/rhmi/common/widgetdb.zip") 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/DispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | /** 7 | * A thin interface to allow replacing dispatchers during tests 8 | */ 9 | @Suppress("PropertyName") 10 | interface DispatcherProvider { 11 | val Main: CoroutineDispatcher 12 | get() = Dispatchers.Main 13 | val Default: CoroutineDispatcher 14 | get() = Dispatchers.Default 15 | val IO: CoroutineDispatcher 16 | get() = Dispatchers.IO 17 | val Unconfined: CoroutineDispatcher 18 | get() = Dispatchers.Unconfined 19 | } 20 | 21 | class DefaultDispatcherProvider : DispatcherProvider -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/SecurityServiceThread.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive 2 | 3 | import android.os.Handler 4 | import android.os.HandlerThread 5 | import android.os.Looper 6 | import io.bimmergestalt.idriveconnectkit.android.security.SecurityAccess 7 | 8 | class SecurityServiceThread(val securityAccess: SecurityAccess): HandlerThread("SecurityServiceThread") { 9 | override fun onLooperPrepared() { 10 | securityAccess.connect() 11 | } 12 | 13 | fun connect() { 14 | Handler(Looper.myLooper()!!).post { 15 | securityAccess.connect() 16 | } 17 | } 18 | 19 | fun disconnect() { 20 | Handler(Looper.myLooper()!!).post { 21 | securityAccess.disconnect() 22 | quitSafely() 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/FocusedStateTracker.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp 2 | 3 | /** 4 | * Used by an HMI Event handler to track which state is currently focused 5 | */ 6 | class FocusedStateTracker { 7 | private val _isFocused = HashMap() 8 | val isFocused: Map = _isFocused 9 | 10 | fun onFocus(id: Int, focused: Boolean = true) { 11 | _isFocused[id] = focused 12 | } 13 | 14 | fun onBlur(id: Int) { 15 | _isFocused[id] = false 16 | } 17 | 18 | fun getFocused(): Int? { 19 | return isFocused.filterValues { it }.keys.firstOrNull() 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/RHMIActionAbort.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp 2 | 3 | /** 4 | * Used to signal from an RHMIActionCallback that the rhmi_onActionEvent should return success=false 5 | */ 6 | class RHMIActionAbort: Exception() -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/RHMIModelMultiSetter.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp 2 | 3 | import io.bimmergestalt.idriveconnectkit.rhmi.RHMIModel 4 | 5 | class RHMIModelMultiSetterData(val members: Iterable) { 6 | var value: String 7 | get() { 8 | val member = members.filterNotNull().firstOrNull() 9 | return member?.value ?: "" 10 | } 11 | set(value) { 12 | for (member in members) { 13 | member?.value = value 14 | } 15 | } 16 | } 17 | 18 | 19 | class RHMIModelMultiSetterInt(val members: Iterable) { 20 | var value: Int 21 | get() { 22 | val member = members.filterNotNull().firstOrNull() 23 | return member?.value ?: 0 24 | } 25 | set(value) { 26 | for (member in members) { 27 | member?.value = value 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/assistant/AssistantAppInfo.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.assistant 2 | 3 | import android.graphics.drawable.Drawable 4 | import me.hufman.androidautoidrive.carapp.AMAppInfo 5 | import me.hufman.androidautoidrive.carapp.AMCategory 6 | import me.hufman.androidautoidrive.music.MusicAppInfo 7 | 8 | data class AssistantAppInfo(override val name: String, override val icon: Drawable, 9 | override val packageName: String): AMAppInfo { 10 | 11 | override val category = AMCategory.ONLINE_SERVICES 12 | 13 | override val amAppIdentifier: String 14 | get() = "androidautoidrive.assistant.${this.packageName}" 15 | 16 | override fun equals(other: Any?): Boolean { 17 | if (this === other) return true 18 | if (javaClass != other?.javaClass) return false 19 | 20 | other as MusicAppInfo 21 | 22 | if (name != other.name) return false 23 | if (packageName != other.packageName) return false 24 | 25 | return true 26 | } 27 | 28 | override fun hashCode(): Int { 29 | var result = name.hashCode() 30 | result = 31 * result + packageName.hashCode() 31 | return result 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/assistant/AssistantController.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.assistant 2 | 3 | interface AssistantController { 4 | fun getAssistants(): Set 5 | 6 | fun triggerAssistant(assistant: AssistantAppInfo) 7 | fun supportsSettings(assistant: AssistantAppInfo): Boolean 8 | fun openSettings(assistant: AssistantAppInfo) 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/calendar/RHMIDateUtils.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.calendar 2 | 3 | import java.util.* 4 | 5 | object RHMIDateUtils { 6 | fun convertFromRhmiDate(date: Int): Calendar { 7 | val year = (date shr 16) and 0xffff 8 | val month = (date shr 8) and 0xff 9 | val day = (date shr 0) and 0xff 10 | return Calendar.getInstance().apply { 11 | set(year, month - 1, day) 12 | set(Calendar.HOUR_OF_DAY, 0) 13 | set(Calendar.MINUTE, 0) 14 | set(Calendar.SECOND, 0) 15 | set(Calendar.MILLISECOND, 0) 16 | } 17 | } 18 | fun convertToRhmiDate(date: Calendar): Int { 19 | return (date.get(Calendar.YEAR) shl 16) + 20 | ((date.get(Calendar.MONTH) + 1) shl 8) + 21 | date.get(Calendar.DAY_OF_MONTH) 22 | } 23 | 24 | fun convertToRhmiTime(time: Calendar): Int { 25 | return (time.get(Calendar.SECOND) shl 16) + 26 | (time.get(Calendar.MINUTE) shl 8) + 27 | time.get(Calendar.HOUR_OF_DAY) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/calendar/views/PermissionView.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.calendar.views 2 | 3 | import io.bimmergestalt.idriveconnectkit.rhmi.RHMIComponent 4 | import io.bimmergestalt.idriveconnectkit.rhmi.RHMIModel 5 | import io.bimmergestalt.idriveconnectkit.rhmi.RHMIState 6 | import me.hufman.androidautoidrive.carapp.L 7 | 8 | class PermissionView(val state: RHMIState) { 9 | companion object { 10 | fun fits(state: RHMIState): Boolean { 11 | return state is RHMIState.PlainState && 12 | state.componentsList.size == 1 && 13 | state.componentsList.filterIsInstance().size == 1 14 | } 15 | } 16 | 17 | val label: RHMIComponent.Label = state.componentsList.filterIsInstance().first() 18 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/music/components/PlaylistItem.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.music.components 2 | 3 | /** 4 | * @param isAnimated - Whether the icon is replaced with a loading spinner 5 | * @param isEnabled - Whether the user can action this item 6 | * @param leftImage - the icon to show on the left side 7 | * @param firstText - first line of text 8 | * @param secondText - second line of text 9 | */ 10 | fun PlaylistItem(isAnimated: Boolean, isEnabled: Boolean, leftImage: Any, firstText: String, secondText: String = ""): Array { 11 | return arrayOf( 12 | isAnimated, leftImage, firstText, 13 | "", false, // firstRightImage 14 | "", false, // secondRightImage 15 | secondText, secondText.isNotBlank(), 16 | isEnabled 17 | ) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/music/components/ProgressGauge.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.music.components 2 | 3 | import io.bimmergestalt.idriveconnectkit.rhmi.RHMIModel 4 | import me.hufman.androidautoidrive.carapp.RHMIModelMultiSetterInt 5 | 6 | interface ProgressGauge { 7 | /** 8 | * Sets the percentage of progress, in [0..100] 9 | */ 10 | var value: Int 11 | } 12 | 13 | class ProgressGaugeToolbarState(val model: RHMIModelMultiSetterInt): ProgressGauge { 14 | override var value: Int 15 | get() = model.value 16 | set(value) { model.value = value } 17 | } 18 | 19 | class ProgressGaugeAudioState(val model: RHMIModel.RaDataModel): ProgressGauge { 20 | override var value: Int 21 | get() = (model.value.toDouble() * 100).toInt() 22 | set(value) { model.value = (value / 100.0).toString() } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/notifications/PopupAutoCloser.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.notifications 2 | 3 | import android.os.Handler 4 | import me.hufman.androidautoidrive.carapp.notifications.views.PopupView 5 | 6 | class PopupAutoCloser(val handler: Handler, val popupView: PopupView) { 7 | companion object { 8 | const val DELAY = 10000L 9 | } 10 | 11 | val runnable = Runnable { 12 | popupView.hideNotification() 13 | } 14 | 15 | fun start() { 16 | handler.removeCallbacks(runnable) 17 | handler.postDelayed(runnable, DELAY) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/notifications/views/PermissionView.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.notifications.views 2 | 3 | import io.bimmergestalt.idriveconnectkit.rhmi.RHMIComponent 4 | import io.bimmergestalt.idriveconnectkit.rhmi.RHMIState 5 | import me.hufman.androidautoidrive.carapp.L 6 | 7 | class PermissionView(val state: RHMIState) { 8 | companion object { 9 | fun fits(state: RHMIState): Boolean { 10 | return state is RHMIState.PlainState && 11 | state.componentsList.filterIsInstance().isNotEmpty() 12 | } 13 | } 14 | 15 | val label: RHMIComponent.Label = state.componentsList.filterIsInstance().first() 16 | 17 | fun initWidgets() { 18 | label.setVisible(true) 19 | label.setEnabled(false) 20 | label.getModel()?.asRaDataModel()?.value = L.NOTIFICATION_PERMISSION_NEEDED 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/carapp/notifications/views/PopupView.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.carapp.notifications.views 2 | 3 | import me.hufman.androidautoidrive.notifications.CarNotification 4 | 5 | interface PopupView { 6 | var currentNotification: CarNotification? 7 | fun initWidgets() 8 | fun showNotification(sbn: CarNotification) 9 | fun hideNotification() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/maps/MapPlaceSearch.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.maps 2 | 3 | import kotlinx.coroutines.Deferred 4 | import java.io.Serializable 5 | 6 | data class MapResult(val id: String, val name: String, 7 | val address: String? = null, 8 | val location: LatLong? = null, 9 | val distanceKm: Float? = null): Serializable { 10 | override fun toString(): String { 11 | return if (address != null) { 12 | if (name.isNotBlank()) { 13 | "$name\n$address" 14 | } else { 15 | address 16 | } 17 | } else { 18 | name 19 | } 20 | } 21 | } 22 | 23 | interface MapPlaceSearch { 24 | fun searchLocationsAsync(query: String): Deferred> 25 | fun resultInformationAsync(resultId: String): Deferred 26 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/music/MusicAction.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.music 2 | 3 | import android.support.v4.media.session.PlaybackStateCompat 4 | 5 | enum class MusicAction(val flag: Long) { 6 | PLAY(PlaybackStateCompat.ACTION_PLAY), 7 | PAUSE(PlaybackStateCompat.ACTION_PAUSE), 8 | REWIND(PlaybackStateCompat.ACTION_REWIND), 9 | FAST_FORWARD(PlaybackStateCompat.ACTION_FAST_FORWARD), 10 | PLAY_FROM_SEARCH(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH), 11 | SEEK_TO(PlaybackStateCompat.ACTION_SEEK_TO), 12 | SET_SHUFFLE_MODE(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE), 13 | SKIP_TO_NEXT(PlaybackStateCompat.ACTION_SKIP_TO_NEXT), 14 | SKIP_TO_PREVIOUS(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS), 15 | SKIP_TO_QUEUE_ITEM(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM), 16 | SET_REPEAT_MODE(PlaybackStateCompat.ACTION_SET_REPEAT_MODE) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/music/PlaybackPosition.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.music 2 | 3 | import android.os.SystemClock 4 | import kotlin.math.min 5 | 6 | /** Given a snapshot of playback position, return the song's current playback position in ms */ 7 | class PlaybackPosition(val isPaused: Boolean, // basically whether to show a Play button and blink the time 8 | val isBuffering: Boolean, // whether to show a loader spinner in AudioHmiState 9 | val lastPositionUpdateTime: Long = SystemClock.elapsedRealtime(), 10 | val lastPosition: Long, 11 | val maximumPosition: Long) { 12 | fun getPosition(): Long { 13 | return if (isPaused || lastPositionUpdateTime == 0L) { 14 | lastPosition 15 | } else { 16 | val estimatedPosition = lastPosition + (SystemClock.elapsedRealtime() - lastPositionUpdateTime) 17 | if (maximumPosition >= 0) { 18 | min(estimatedPosition, maximumPosition) 19 | } else { 20 | estimatedPosition 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/music/QueueMetadata.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.music 2 | 3 | import android.graphics.Bitmap 4 | 5 | data class QueueMetadata(val title: String? = null, 6 | val subtitle: String? = null, 7 | val songs: List? = null, 8 | val coverArt: Bitmap? = null, 9 | val mediaId: String? = null 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/music/RepeatMode.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.music 2 | 3 | enum class RepeatMode { 4 | ALL, ONE, OFF 5 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/music/spotify/TemporaryPlaylistState.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.music.spotify 2 | 3 | data class TemporaryPlaylistState(var hashCode: String, val playlistUri: String, val playlistId: String, var playlistTitle: String?, var originalPlaylistUri: String) -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/notifications/NotificationsState.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.notifications 2 | 3 | import androidx.annotation.VisibleForTesting 4 | import java.util.* 5 | 6 | object NotificationsState { 7 | var serviceConnected = false // whether the notification listener service is running 8 | 9 | val notifications = ArrayList() // the notifications shown in the list 10 | 11 | fun getNotificationByKey(key: String?): CarNotification? { 12 | key ?: return null 13 | return synchronized(notifications) { 14 | notifications.find { it.key == key } 15 | } 16 | } 17 | 18 | fun cloneNotifications(): List { 19 | return synchronized(notifications) { 20 | ArrayList(notifications) 21 | } 22 | } 23 | 24 | fun replaceNotifications(updated: Iterable) { 25 | synchronized(notifications) { 26 | notifications.clear() 27 | notifications.addAll(updated) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/phoneui/NestedGridView.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.phoneui 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.widget.GridView 7 | 8 | class NestedGridView(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0): GridView(context, attrs, defStyleAttr) { 9 | constructor(context: Context): this(context, null, 0) 10 | constructor(context: Context, attrs: AttributeSet? = null): this(context, attrs, 0) 11 | 12 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 13 | super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(View.MEASURED_SIZE_MASK, MeasureSpec.AT_MOST)) 14 | layoutParams.height = measuredHeight 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/phoneui/NestedListView.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.phoneui 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.widget.ListView 7 | 8 | class NestedListView(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0): ListView(context, attrs, defStyleAttr) { 9 | constructor(context: Context): this(context, null, 0) 10 | constructor(context: Context, attrs: AttributeSet? = null): this(context, attrs, 0) 11 | 12 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 13 | super.onMeasure(widthMeasureSpec, View.MeasureSpec.makeMeasureSpec(View.MEASURED_SIZE_MASK, View.MeasureSpec.AT_MOST)) 14 | layoutParams.height = measuredHeight 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/phoneui/UIState.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.phoneui 2 | 3 | import me.hufman.androidautoidrive.music.MusicAppInfo 4 | 5 | object UIState { 6 | var selectedMusicApp: MusicAppInfo? = null 7 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/phoneui/adapters/ObservableListCallback.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.phoneui.adapters 2 | 3 | import androidx.databinding.ObservableList 4 | 5 | class ObservableListCallback(val callback: (ObservableList?) -> Unit): ObservableList.OnListChangedCallback>() { 6 | override fun onChanged(sender: ObservableList?) { 7 | callback(sender) 8 | } 9 | 10 | override fun onItemRangeChanged(sender: ObservableList?, positionStart: Int, itemCount: Int) { 11 | callback(sender) 12 | } 13 | 14 | override fun onItemRangeInserted(sender: ObservableList?, positionStart: Int, itemCount: Int) { 15 | callback(sender) 16 | } 17 | 18 | override fun onItemRangeMoved(sender: ObservableList?, fromPosition: Int, toPosition: Int, itemCount: Int) { 19 | callback(sender) 20 | } 21 | 22 | override fun onItemRangeRemoved(sender: ObservableList?, positionStart: Int, itemCount: Int) { 23 | callback(sender) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/phoneui/controllers/AddonAppListController.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.phoneui.controllers 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import me.hufman.androidautoidrive.addons.AddonAppInfo 7 | import me.hufman.androidautoidrive.utils.PackageManagerCompat.resolveActivityCompat 8 | 9 | class AddonAppListController(val context: Context, val permissionsController: PermissionsController) { 10 | fun openApplicationPermissions(appInfo: AddonAppInfo) { 11 | permissionsController.openApplicationPermissions(appInfo.packageName) 12 | } 13 | 14 | private fun tryOpenActivity(intent: Intent): Boolean { 15 | if (context.packageManager.resolveActivityCompat(intent, 0) != null) { 16 | try { 17 | context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) 18 | return true 19 | } catch (e: ActivityNotFoundException) { 20 | } catch (e: IllegalArgumentException) {} 21 | } 22 | return false 23 | } 24 | 25 | fun openIntent(intent: Intent?) { 26 | if (intent != null) { 27 | tryOpenActivity(intent) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/phoneui/controllers/CalendarPageController.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.phoneui.controllers 2 | 3 | import me.hufman.androidautoidrive.phoneui.viewmodels.CalendarSettingsModel 4 | import me.hufman.androidautoidrive.phoneui.viewmodels.PermissionsModel 5 | 6 | class CalendarPageController(val calendarSettingsModel: CalendarSettingsModel, 7 | val permissionsModel: PermissionsModel, 8 | val permissionsController: PermissionsController) { 9 | 10 | fun onChangedSwitchCalendar(isChecked: Boolean) { 11 | calendarSettingsModel.calendarEnabled.setValue(isChecked) 12 | if (isChecked) { 13 | // make sure we have permissions to read the notifications 14 | if (permissionsModel.hasCalendarPermission.value != true) { 15 | permissionsController.promptCalendar() 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/phoneui/controllers/MusicAppListController.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.phoneui.controllers 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.view.View 6 | import me.hufman.androidautoidrive.music.MusicAppInfo 7 | import me.hufman.androidautoidrive.phoneui.MusicPlayerActivity 8 | import me.hufman.androidautoidrive.phoneui.UIState 9 | import me.hufman.androidautoidrive.phoneui.ViewHelpers.visible 10 | 11 | class MusicAppListController(val activity: Activity, val permissionsController: PermissionsController) { 12 | fun openApplicationPermissions(appInfo: MusicAppInfo) { 13 | permissionsController.openApplicationPermissions(appInfo.packageName) 14 | } 15 | 16 | fun openMusicApp(appInfo: MusicAppInfo) { 17 | UIState.selectedMusicApp = appInfo 18 | val intent = Intent(activity, MusicPlayerActivity::class.java) 19 | activity.startActivity(intent) 20 | } 21 | 22 | fun toggleFeatures(view: View) { 23 | view.visible = !view.visible 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/phoneui/controllers/OverviewPageController.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.phoneui.controllers 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.navigation.fragment.findNavController 5 | import me.hufman.androidautoidrive.R 6 | 7 | class OverviewPageController(val fragment: Fragment) { 8 | fun onClickConnecting() { 9 | fragment.findNavController().navigate(R.id.nav_connection) 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/phoneui/controllers/SendIntentController.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.phoneui.controllers 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | 7 | class SendIntentController(val appContext: Context) { 8 | fun viewUri(uri: Uri) { 9 | val intent = Intent(Intent.ACTION_VIEW, uri) 10 | .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 11 | appContext.startActivity(intent) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/me/hufman/androidautoidrive/phoneui/fragments/AddonsPageFragment.kt: -------------------------------------------------------------------------------- 1 | package me.hufman.androidautoidrive.phoneui.fragments 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.Button 10 | import androidx.fragment.app.Fragment 11 | import me.hufman.androidautoidrive.R 12 | 13 | class AddonsPageFragment: Fragment() { 14 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 15 | return inflater.inflate(R.layout.fragment_addonpage, container, false) 16 | } 17 | 18 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 19 | super.onViewCreated(view, savedInstanceState) 20 | view.findViewById