From 933209a9c2fe97077b664aa3ee26b09e29d29d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Tue, 15 Oct 2024 17:19:32 +0800 Subject: [PATCH] feat: add carplay and android auto support (#255) Adds support for Apple CarPlay and Android Auto. Added BaseAutoSceneDelegate for iOS and AndroidAutoBaseScreen for Android on a plugin level to manage setting up the base functionality for each platform. Added example of setting up each platform in the SampleApp. iOS SampleApp has a new separate target for CarPlay support SampleAppCarPlay. Added separate native module NavAutoModule to manage communication between RN code and the map instance. Added a generic event channel so that user actions can be sent from native code to RN. e.g. user presses a button in CarPlay window and this event is sent to RN code. Added a function to NavAutoModule to check whether CarPlay or Android Auto screen has been initialized and is available. Co-authored-by: Joonas Kerttula --- ANDROIDAUTO.md | 100 +++ CARPLAY.md | 86 +++ README.md | 6 + android/build.gradle | 10 +- .../react/navsdk/AndroidAutoBaseScreen.java | 206 ++++++ .../android/react/navsdk/Constants.java | 1 + .../react/navsdk/IMapViewFragment.java | 76 +-- .../react/navsdk/INavViewFragment.java | 2 + .../react/navsdk/INavigationAutoCallback.java | 20 + .../react/navsdk/INavigationViewCallback.java | 41 ++ .../navsdk/INavigationViewController.java | 20 + .../react/navsdk/MapViewController.java | 572 +++++++++++++++++ .../android/react/navsdk/MapViewFragment.java | 190 ++---- .../android/react/navsdk/NavAutoModule.java | 537 ++++++++++++++++ .../android/react/navsdk/NavModule.java | 119 ++-- .../android/react/navsdk/NavViewFragment.java | 607 ++---------------- .../android/react/navsdk/NavViewManager.java | 58 +- .../android/react/navsdk/NavViewModule.java | 9 +- .../google/android/react/navsdk/Package.java | 1 + example/README.md | 21 +- example/android/app/build.gradle | 15 +- .../android/app/src/main/AndroidManifest.xml | 82 ++- .../java/com/sampleapp/ManeuverConverter.java | 265 ++++++++ .../sampleapp/SampleAndroidAutoScreen.java | 150 +++++ .../sampleapp/SampleAndroidAutoService.java | 38 ++ .../sampleapp/SampleAndroidAutoSession.java | 114 ++++ .../src/main/res/xml/automotive_app_desc.xml | 19 + example/android/build.gradle | 2 +- example/ios/Podfile | 25 +- .../ios/SampleApp.xcodeproj/project.pbxproj | 416 ++++++++++-- .../xcshareddata/xcschemes/SampleApp.xcscheme | 28 +- .../xcschemes/SampleAppCarPlay.xcscheme | 88 +++ .../{AppDelegate.mm => AppDelegate.m} | 13 +- example/ios/SampleApp/AppDelegateCarPlay.h | 23 + example/ios/SampleApp/AppDelegateCarPlay.m | 83 +++ example/ios/SampleApp/CarSceneDelegate.h | 20 + example/ios/SampleApp/CarSceneDelegate.m | 42 ++ example/ios/SampleApp/Info-CarPlay.plist | 98 +++ example/ios/SampleApp/PhoneSceneDelegate.h | 24 + example/ios/SampleApp/PhoneSceneDelegate.m | 47 ++ example/ios/SampleApp/SampleApp.entitlements | 8 + example/ios/SampleApp/main.m | 8 + example/package.json | 1 + example/src/App.tsx | 4 +- example/src/{ => controls}/mapsControls.tsx | 2 +- .../src/{ => controls}/navigationControls.tsx | 2 +- example/src/{ => helpers}/overlayModal.tsx | 0 .../src/{ => screens}/MultipleMapsScreen.tsx | 30 +- .../src/{ => screens}/NavigationScreen.tsx | 87 ++- .../BaseCarSceneDelegate.h | 31 + .../BaseCarSceneDelegate.m | 125 ++++ .../NavAutoEventDispatcher.h | 34 + .../NavAutoEventDispatcher.m | 65 ++ .../NavAutoModule.h | 38 ++ .../NavAutoModule.m | 412 ++++++++++++ ios/react-native-navigation-sdk/NavModule.h | 7 + ios/react-native-navigation-sdk/NavModule.m | 25 + package.json | 1 + src/auto/index.ts | 18 + src/auto/types.ts | 59 ++ src/auto/useNavigationAuto.ts | 199 ++++++ src/index.ts | 1 + src/maps/mapView/types.ts | 13 +- .../navigation/NavigationProvider.tsx | 2 +- src/navigation/navigation/types.ts | 15 - .../navigation/useNavigationController.ts | 350 ++++------ src/navigation/navigationView/types.ts | 15 - src/shared/index.ts | 1 + src/shared/useModuleListeners.ts | 127 ++++ 69 files changed, 4679 insertions(+), 1275 deletions(-) create mode 100644 ANDROIDAUTO.md create mode 100644 CARPLAY.md create mode 100644 android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java create mode 100644 android/src/main/java/com/google/android/react/navsdk/INavigationAutoCallback.java create mode 100644 android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java create mode 100644 android/src/main/java/com/google/android/react/navsdk/INavigationViewController.java create mode 100644 android/src/main/java/com/google/android/react/navsdk/MapViewController.java create mode 100644 android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java create mode 100644 example/android/app/src/main/java/com/sampleapp/ManeuverConverter.java create mode 100644 example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoScreen.java create mode 100644 example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoService.java create mode 100644 example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoSession.java create mode 100644 example/android/app/src/main/res/xml/automotive_app_desc.xml create mode 100644 example/ios/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleAppCarPlay.xcscheme rename example/ios/SampleApp/{AppDelegate.mm => AppDelegate.m} (84%) create mode 100644 example/ios/SampleApp/AppDelegateCarPlay.h create mode 100644 example/ios/SampleApp/AppDelegateCarPlay.m create mode 100644 example/ios/SampleApp/CarSceneDelegate.h create mode 100644 example/ios/SampleApp/CarSceneDelegate.m create mode 100644 example/ios/SampleApp/Info-CarPlay.plist create mode 100644 example/ios/SampleApp/PhoneSceneDelegate.h create mode 100644 example/ios/SampleApp/PhoneSceneDelegate.m create mode 100644 example/ios/SampleApp/SampleApp.entitlements rename example/src/{ => controls}/mapsControls.tsx (99%) rename example/src/{ => controls}/navigationControls.tsx (99%) rename example/src/{ => helpers}/overlayModal.tsx (100%) rename example/src/{ => screens}/MultipleMapsScreen.tsx (97%) rename example/src/{ => screens}/NavigationScreen.tsx (83%) create mode 100644 ios/react-native-navigation-sdk/BaseCarSceneDelegate.h create mode 100644 ios/react-native-navigation-sdk/BaseCarSceneDelegate.m create mode 100644 ios/react-native-navigation-sdk/NavAutoEventDispatcher.h create mode 100644 ios/react-native-navigation-sdk/NavAutoEventDispatcher.m create mode 100644 ios/react-native-navigation-sdk/NavAutoModule.h create mode 100644 ios/react-native-navigation-sdk/NavAutoModule.m create mode 100644 src/auto/index.ts create mode 100644 src/auto/types.ts create mode 100644 src/auto/useNavigationAuto.ts create mode 100644 src/shared/useModuleListeners.ts diff --git a/ANDROIDAUTO.md b/ANDROIDAUTO.md new file mode 100644 index 0000000..e7d3202 --- /dev/null +++ b/ANDROIDAUTO.md @@ -0,0 +1,100 @@ +# Navigation for Android Auto + +This guide explains how to enable and integrate Android Auto with the React Native Navigation SDK. + +## Requirements + +- Android device +- Android Auto test device or Android Automotive OS emulator + +## Setup + +Refer to the [Android for Cars developer documentation](https://developer.android.com/training/cars) to understand how the Android Auto works and to complete the initial setup. Key steps include: + +- Installing Android for Cars App Library. +- Configuring your app's manifest file to include Android Auto. +- Declaring a minimum car-app level in your manifest. +- Creating 'CarAppService' and session + +For all the steps above, you can refer to the Android example application for guidance. + +### Screen for Android Auto + +Once your project is configured accordingly, and you are ready to build the screen for Android Auto, you can leverage the `AndroidAutoBaseScreen` provided by the SDK. This base class simplifies the setup by handling initialization, teardown, and rendering the map on the Android Auto display. + +Please refer to the `SampleAndroidAutoScreen.java` file in the Android example app for guidance. + +To customize the Android Auto experience, override the `onGetTemplate` method in your custom AndroidAutoScreen class, providing your own `Template`: + +```java +@NonNull +@Override +public Template onGetTemplate() { + /** ... */ + @SuppressLint("MissingPermission") + NavigationTemplate.Builder navigationTemplateBuilder = + new NavigationTemplate.Builder() + .setActionStrip( + new ActionStrip.Builder() + .addAction( + new Action.Builder() + .setTitle("Re-center") + .setOnClickListener( + () -> { + if (mGoogleMap == null) return; + mGoogleMap.followMyLocation(GoogleMap.CameraPerspective.TILTED); + }) + .build()) + .addAction( + new Action.Builder() + .setTitle("Custom event") + .setOnClickListener( + () -> { + WritableMap map = Arguments.createMap(); + map.putString("sampleKey", "sampleValue"); + sendCustomEvent("sampleEvent", map); + }) + .build()) + .build()) + .setMapActionStrip(new ActionStrip.Builder().addAction(Action.PAN).build()); + /** ... */ +} +``` + +For advanced customization, you can bypass the base class and implement your own screen by inheriting `Screen`. You can use the provided `AndroidAutoBaseScreen` base class as a reference on how to do that. + +### React Native specific setup + +On the React Native side, you can use the `useNavigationAuto` hook to interface with the Android Auto instance. The `mapViewAutoController` allows you to call map functions on the Android Auto map, and you can manage listeners using the provided functions. + +```tsx +const { + mapViewAutoController, + addListeners: addAutoListener, + removeListeners: removeAutoListeners, +} = useNavigationAuto(); + +const navigationAutoCallbacks: NavigationAutoCallbacks = useMemo( + () => ({ + onCustomNavigationAutoEvent: (event: CustomNavigationAutoEvent) => { + console.log('onCustomNavigationAutoEvent:', event); + }, + onAutoScreenAvailabilityChanged: (available: boolean) => { + console.log('onAutoScreenAvailabilityChanged:', available); + setMapViewAutoAvailable(available); + }, + }), + [] +); + +const setMapType = (mapType: MapType) => { + console.log('setMapType', mapType); + mapViewAutoController.setMapType(mapType); +}; +``` + +For a more detailed example, refer to the `NavigationScreen.tsx` in the React Native example application. + +## Example Project + +For a fully functional Android Auto implementation, check out the [SampleApp](./example/android/) Android Studio project. diff --git a/CARPLAY.md b/CARPLAY.md new file mode 100644 index 0000000..f46cd7b --- /dev/null +++ b/CARPLAY.md @@ -0,0 +1,86 @@ +# Navigation for Apple CarPlay + +This guide explains how to enable and integrate Apple CarPlay with the React Native Navigation SDK. + +## Requirements + +- iOS device or iOS simulator (iOS 14.0+) +- CarPlay Simulator +- CarPlay entitlement for your application (provided by Apple) + +## Setup + +Refer to the [Apple CarPlay Developer Guide](https://developer.apple.com/carplay/) to understand how CarPlay works and to complete the initial setup. Key steps include: + +- Adding the CarPlay entitlement to your Xcode project. +- Creating a separate scene for the CarPlay map and enabling support for multiple scenes. + +### SceneDelegate for CarPlay + +Once your project is configured to support multiple scenes, and you are setting up a dedicated scene for CarPlay, you can leverage the `BaseCarSceneDelegate` provided by the SDK. This base class simplifies the setup by handling initialization, teardown, and rendering the map on the CarPlay display. + +Please refer to the `CarSceneDelegate.h` and `CarSceneDelegate.m` files in the iOS example app for guidance. + +To customize the CarPlay experience, override the `getTemplate` method in your custom `CarSceneDelegate` class, providing your own `CPMapTemplate`: + +```objc +- (CPMapTemplate *)getTemplate { +  CPMapTemplate *template = [[CPMapTemplate alloc] init]; +  [template showPanningInterfaceAnimated:YES]; +  +  CPBarButton *customButton = [[CPBarButton alloc] +      initWithTitle:@"Custom Event" +            handler:^(CPBarButton ***_Nonnull** button) { +              NSMutableDictionary *dictionary = [ +               [NSMutableDictionary alloc] init +           ]; +              dictionary[@"sampleDataKey"] = @"sampleDataContent"; +              [[NavAutoModule getOrCreateSharedInstance] +               onCustomNavigationAutoEvent:@"sampleEvent" +               data:dictionary +           ]; +            }]; + +  template.leadingNavigationBarButtons = @[ customButton ]; +  template.trailingNavigationBarButtons = @[]; +  return template; +} +``` + +For advanced customization, you can bypass the base class and implement your own delegate inheriting `CPTemplateApplicationSceneDelegate`. You can use the provided `BaseCarSceneDelegate` base class as a reference on how to do that. + +### React Native Setup + +On the React Native side, you can use the `useNavigationAuto` hook to interface with the CarPlay instance. The `mapViewAutoController` allows you to call map functions on the CarPlay map view, and you can manage listeners using the provided functions. + +```tsx +const { + mapViewAutoController, + addListeners: addAutoListener, + removeListeners: removeAutoListeners, +} = useNavigationAuto(); + +const navigationAutoCallbacks: NavigationAutoCallbacks = useMemo( + () => ({ + onCustomNavigationAutoEvent: (event: CustomNavigationAutoEvent) => { + console.log('onCustomNavigationAutoEvent:', event); + }, + onAutoScreenAvailabilityChanged: (available: boolean) => { + console.log('onAutoScreenAvailabilityChanged:', available); + setMapViewAutoAvailable(available); + }, + }), + [] +); + +const setMapType = (mapType: MapType) => { + console.log('setMapType', mapType); + mapViewAutoController.setMapType(mapType); +}; +``` + +For a more detailed example, refer to the `NavigationScreen.tsx` in the React Native example application. + +## Example Project + +For a fully functional CarPlay implementation, check out the [SampleApp](./example/ios/) Xcode project, which includes the `SampleAppCarPlay` build target. The sample already contains test entitlement so you don't need to request one from Apple to run it. diff --git a/README.md b/README.md index 85b96bb..e6d5b55 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,12 @@ By default, `NavigationView` uses all the available space provided to it. To adj /> ``` +## Support for Android Auto and Apple CarPlay +This plugin is compatible with both Android Auto and Apple CarPlay infotainment systems. For more details, please refer to the respective platform documentation: + +- [Android Auto documentation](./ANDROIDAUTO.md) +- [CarPlay documentation](./CARPLAY.md) + ## Known issues ### Compatibility with other libraries diff --git a/android/build.gradle b/android/build.gradle index 51cbdf8..62ba866 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,11 +1,11 @@ // Copyright 2023 Google LLC -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -37,7 +37,7 @@ if (isNewArchitectureEnabled()) { android { namespace "com.google.android.react.navsdk" - + compileSdkVersion 31 compileOptions { @@ -71,6 +71,8 @@ repositories { } dependencies { + implementation "androidx.car.app:app:1.4.0" + implementation "androidx.car.app:app-projected:1.4.0" implementation 'com.facebook.react:react-native:+' implementation 'com.android.support:multidex:1.0.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' diff --git a/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java b/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java new file mode 100644 index 0000000..c7ac8aa --- /dev/null +++ b/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java @@ -0,0 +1,206 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.react.navsdk; + +import android.app.Presentation; +import android.graphics.Point; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import androidx.annotation.NonNull; +import androidx.car.app.AppManager; +import androidx.car.app.CarContext; +import androidx.car.app.Screen; +import androidx.car.app.SurfaceCallback; +import androidx.car.app.SurfaceContainer; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.Template; +import androidx.car.app.navigation.model.NavigationTemplate; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import com.facebook.react.bridge.ReadableMap; +import com.google.android.gms.maps.CameraUpdate; +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.libraries.navigation.NavigationViewForAuto; +import com.google.android.libraries.navigation.StylingOptions; + +// This class streamlines the Android Auto setup process by managing initialization, teardown, and +// map rendering on the Android Auto display. You can create your own Screen class by extending this +// one and overriding its functions as needed. +// +// For more information on using Android Auto with the Google Navigation SDK, refer to the official +// documentation: +// https://developers.google.com/maps/documentation/navigation/android-sdk/android-auto +public abstract class AndroidAutoBaseScreen extends Screen + implements SurfaceCallback, INavigationViewController { + private static final String VIRTUAL_DISPLAY_NAME = "AndroidAutoNavScreen"; + + private NavigationViewForAuto mNavigationView; + private VirtualDisplay mVirtualDisplay; + private Presentation mPresentation; + protected GoogleMap mGoogleMap; + protected boolean mNavigationInitialized = false; + private MapViewController mMapViewController; + + private boolean mAndroidAutoModuleInitialized = false; + private boolean mNavModuleInitialized = false; + private final AndroidAutoBaseScreen screenInstance = this; + + @Override + public void setStylingOptions(StylingOptions stylingOptions) { + // TODO(jokerttu): set styling to the navigationView + } + + public void onNavigationReady(boolean ready) { + mNavigationInitialized = ready; + } + + public AndroidAutoBaseScreen(@NonNull CarContext carContext) { + + super(carContext); + + NavAutoModule.setModuleReadyListener( + () -> { + mAndroidAutoModuleInitialized = true; + registerControllersForAndroidAutoModule(); + }); + + NavModule.setModuleReadyListener( + () -> { + mNavModuleInitialized = true; + NavModule.getInstance().registerNavigationReadyListener(this::onNavigationReady); + }); + + carContext.getCarService(AppManager.class).setSurfaceCallback(this); + + Lifecycle lifecycle = getLifecycle(); + lifecycle.addObserver(mLifeCycleObserver); + } + + private final LifecycleObserver mLifeCycleObserver = + new DefaultLifecycleObserver() { + @Override + public void onDestroy(@NonNull LifecycleOwner lifecycleOwner) { + if (mNavModuleInitialized) { + try { + NavModule.getInstance() + .unRegisterNavigationReadyListener(screenInstance::onNavigationReady); + } catch (Exception e) { + } + } + } + }; + + private void registerControllersForAndroidAutoModule() { + if (mAndroidAutoModuleInitialized && mMapViewController != null) { + NavAutoModule.getInstance().androidAutoNavigationScreenInitialized(mMapViewController, this); + } + } + + private void unRegisterControllersForAndroidAutoModule() { + if (mAndroidAutoModuleInitialized) { + NavAutoModule.getInstance().androidAutoNavigationScreenDisposed(); + } + } + + private boolean isSurfaceReady(SurfaceContainer surfaceContainer) { + return surfaceContainer.getSurface() != null + && surfaceContainer.getDpi() != 0 + && surfaceContainer.getHeight() != 0 + && surfaceContainer.getWidth() != 0; + } + + @Override + public void onSurfaceAvailable(@NonNull SurfaceContainer surfaceContainer) { + if (!isSurfaceReady(surfaceContainer)) { + return; + } + mVirtualDisplay = + getCarContext() + .getSystemService(DisplayManager.class) + .createVirtualDisplay( + VIRTUAL_DISPLAY_NAME, + surfaceContainer.getWidth(), + surfaceContainer.getHeight(), + surfaceContainer.getDpi(), + surfaceContainer.getSurface(), + DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY); + mPresentation = new Presentation(getCarContext(), mVirtualDisplay.getDisplay()); + + mNavigationView = new NavigationViewForAuto(getCarContext()); + mNavigationView.onCreate(null); + mNavigationView.onStart(); + mNavigationView.onResume(); + + mPresentation.setContentView(mNavigationView); + mPresentation.show(); + + mNavigationView.getMapAsync( + (GoogleMap googleMap) -> { + mGoogleMap = googleMap; + mMapViewController = new MapViewController(); + mMapViewController.initialize(googleMap, () -> null); + registerControllersForAndroidAutoModule(); + invalidate(); + }); + } + + @Override + public void onSurfaceDestroyed(@NonNull SurfaceContainer surfaceContainer) { + unRegisterControllersForAndroidAutoModule(); + mNavigationView.onPause(); + mNavigationView.onStop(); + mNavigationView.onDestroy(); + mGoogleMap = null; + + mPresentation.dismiss(); + mVirtualDisplay.release(); + } + + @Override + public void onScroll(float distanceX, float distanceY) { + if (mGoogleMap == null) { + return; + } + mGoogleMap.moveCamera(CameraUpdateFactory.scrollBy(distanceX, distanceY)); + } + + @Override + public void onScale(float focusX, float focusY, float scaleFactor) { + if (mGoogleMap == null) { + return; + } + CameraUpdate update = + CameraUpdateFactory.zoomBy((scaleFactor - 1), new Point((int) focusX, (int) focusY)); + mGoogleMap.animateCamera(update); // map is set in onSurfaceAvailable. + } + + protected void sendCustomEvent(String type, ReadableMap data) { + NavAutoModule.getInstance().onCustomNavigationAutoEvent(type, data); + } + + @NonNull + @Override + public Template onGetTemplate() { + return new NavigationTemplate.Builder() + .setMapActionStrip(new ActionStrip.Builder().addAction(Action.PAN).build()) + .build(); + } +} diff --git a/android/src/main/java/com/google/android/react/navsdk/Constants.java b/android/src/main/java/com/google/android/react/navsdk/Constants.java index df28645..0c046b3 100644 --- a/android/src/main/java/com/google/android/react/navsdk/Constants.java +++ b/android/src/main/java/com/google/android/react/navsdk/Constants.java @@ -15,6 +15,7 @@ public class Constants { public static final String NAV_JAVASCRIPT_FLAG = "NavJavascriptBridge"; + public static final String NAV_AUTO_JAVASCRIPT_FLAG = "NavAutoJavascriptBridge"; public static final String LAT_FIELD_KEY = "lat"; public static final String LNG_FIELD_KEY = "lng"; } diff --git a/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java index e835afb..f5cde85 100644 --- a/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java @@ -15,85 +15,17 @@ import android.view.View; import com.google.android.gms.maps.GoogleMap; -import com.google.android.gms.maps.model.Circle; -import com.google.android.gms.maps.model.GroundOverlay; -import com.google.android.gms.maps.model.Marker; -import com.google.android.gms.maps.model.Polygon; -import com.google.android.gms.maps.model.Polyline; -import java.io.IOException; -import java.util.Map; +import com.google.android.libraries.navigation.StylingOptions; public interface IMapViewFragment { - void setStylingOptions(Map stylingOptions); + MapViewController getMapController(); - void applyStylingOptions(); - - void setFollowingPerspective(int jsValue); - - void setNightModeOption(int jsValue); - - void setMapType(int jsValue); - - void clearMapView(); - - void resetMinMaxZoomLevel(); - - void animateCamera(Map map); - - Circle addCircle(Map optionsMap); - - Marker addMarker(Map optionsMap); + void setStylingOptions(StylingOptions stylingOptions); - Polyline addPolyline(Map optionsMap); - - Polygon addPolygon(Map optionsMap); - - void removeMarker(String id); - - void removePolyline(String id); - - void removePolygon(String id); - - void removeCircle(String id); - - void removeGroundOverlay(String id); - - GroundOverlay addGroundOverlay(Map map); + void applyStylingOptions(); void setMapStyle(String url); - String fetchJsonFromUrl(String urlString) throws IOException; - - void moveCamera(Map map); - - void setZoomLevel(int level); - - void setIndoorEnabled(boolean isOn); - - void setTrafficEnabled(boolean isOn); - - void setCompassEnabled(boolean isOn); - - void setRotateGesturesEnabled(boolean isOn); - - void setScrollGesturesEnabled(boolean isOn); - - void setScrollGesturesEnabledDuringRotateOrZoom(boolean isOn); - - void setTiltGesturesEnabled(boolean isOn); - - void setZoomControlsEnabled(boolean isOn); - - void setZoomGesturesEnabled(boolean isOn); - - void setBuildingsEnabled(boolean isOn); - - void setMyLocationEnabled(boolean isOn); - - void setMapToolbarEnabled(boolean isOn); - - void setMyLocationButtonEnabled(boolean isOn); - GoogleMap getGoogleMap(); // Fragment diff --git a/android/src/main/java/com/google/android/react/navsdk/INavViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/INavViewFragment.java index 1abbac8..84057f5 100644 --- a/android/src/main/java/com/google/android/react/navsdk/INavViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/INavViewFragment.java @@ -31,4 +31,6 @@ public interface INavViewFragment extends IMapViewFragment { void setRecenterButtonEnabled(boolean enabled); void showRouteOverview(); + + void setNightModeOption(int jsValue); } diff --git a/android/src/main/java/com/google/android/react/navsdk/INavigationAutoCallback.java b/android/src/main/java/com/google/android/react/navsdk/INavigationAutoCallback.java new file mode 100644 index 0000000..302871e --- /dev/null +++ b/android/src/main/java/com/google/android/react/navsdk/INavigationAutoCallback.java @@ -0,0 +1,20 @@ +/** + * Copyright 2023 Google LLC + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.react.navsdk; + +import com.facebook.react.bridge.ReadableMap; + +public interface INavigationAutoCallback { + void onCustomNavigationAutoEvent(String type, ReadableMap data); +} diff --git a/android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java b/android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java new file mode 100644 index 0000000..f51c993 --- /dev/null +++ b/android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java @@ -0,0 +1,41 @@ +/** + * Copyright 2023 Google LLC + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.react.navsdk; + +import com.google.android.gms.maps.model.Circle; +import com.google.android.gms.maps.model.GroundOverlay; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.Polygon; +import com.google.android.gms.maps.model.Polyline; + +public interface INavigationViewCallback { + void onMapReady(); + + void onRecenterButtonClick(); + + void onMarkerClick(Marker marker); + + void onPolylineClick(Polyline polyline); + + void onPolygonClick(Polygon polygon); + + void onCircleClick(Circle circle); + + void onGroundOverlayClick(GroundOverlay groundOverlay); + + void onMarkerInfoWindowTapped(Marker marker); + + void onMapClick(LatLng latLng); +} diff --git a/android/src/main/java/com/google/android/react/navsdk/INavigationViewController.java b/android/src/main/java/com/google/android/react/navsdk/INavigationViewController.java new file mode 100644 index 0000000..b1546ef --- /dev/null +++ b/android/src/main/java/com/google/android/react/navsdk/INavigationViewController.java @@ -0,0 +1,20 @@ +/** + * Copyright 2024 Google LLC + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.react.navsdk; + +import com.google.android.libraries.navigation.StylingOptions; + +public interface INavigationViewController { + void setStylingOptions(StylingOptions stylingOptions); +} diff --git a/android/src/main/java/com/google/android/react/navsdk/MapViewController.java b/android/src/main/java/com/google/android/react/navsdk/MapViewController.java new file mode 100644 index 0000000..68f023b --- /dev/null +++ b/android/src/main/java/com/google/android/react/navsdk/MapViewController.java @@ -0,0 +1,572 @@ +/** + * Copyright 2024 Google LLC + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.react.navsdk; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.graphics.Color; +import androidx.core.util.Supplier; +import com.facebook.react.bridge.UiThreadUtil; +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.Circle; +import com.google.android.gms.maps.model.CircleOptions; +import com.google.android.gms.maps.model.GroundOverlay; +import com.google.android.gms.maps.model.GroundOverlayOptions; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.MapStyleOptions; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import com.google.android.gms.maps.model.Polygon; +import com.google.android.gms.maps.model.PolygonOptions; +import com.google.android.gms.maps.model.Polyline; +import com.google.android.gms.maps.model.PolylineOptions; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; + +public class MapViewController { + private GoogleMap mGoogleMap; + private Supplier activitySupplier; + private INavigationViewCallback mNavigationViewCallback; + private final List markerList = new ArrayList<>(); + private final List polylineList = new ArrayList<>(); + private final List polygonList = new ArrayList<>(); + private final List groundOverlayList = new ArrayList<>(); + private final List circleList = new ArrayList<>(); + private String style = ""; + + public void initialize(GoogleMap googleMap, Supplier activitySupplier) { + this.mGoogleMap = googleMap; + this.activitySupplier = activitySupplier; + } + + public void setupMapListeners(INavigationViewCallback navigationViewCallback) { + this.mNavigationViewCallback = navigationViewCallback; + if (mGoogleMap == null || mNavigationViewCallback == null) return; + + mGoogleMap.setOnMarkerClickListener( + marker -> { + mNavigationViewCallback.onMarkerClick(marker); + return false; + }); + + mGoogleMap.setOnPolylineClickListener( + polyline -> mNavigationViewCallback.onPolylineClick(polyline)); + mGoogleMap.setOnPolygonClickListener( + polygon -> mNavigationViewCallback.onPolygonClick(polygon)); + mGoogleMap.setOnCircleClickListener(circle -> mNavigationViewCallback.onCircleClick(circle)); + mGoogleMap.setOnGroundOverlayClickListener( + groundOverlay -> mNavigationViewCallback.onGroundOverlayClick(groundOverlay)); + mGoogleMap.setOnInfoWindowClickListener( + marker -> mNavigationViewCallback.onMarkerInfoWindowTapped(marker)); + mGoogleMap.setOnMapClickListener(latLng -> mNavigationViewCallback.onMapClick(latLng)); + } + + public GoogleMap getGoogleMap() { + return mGoogleMap; + } + + public Circle addCircle(Map optionsMap) { + if (mGoogleMap == null) { + return null; + } + + CircleOptions options = new CircleOptions(); + + float strokeWidth = + Double.valueOf(CollectionUtil.getDouble("strokeWidth", optionsMap, 0)).floatValue(); + options.strokeWidth(strokeWidth); + + double radius = CollectionUtil.getDouble("radius", optionsMap, 0.0); + options.radius(radius); + + boolean visible = CollectionUtil.getBool("visible", optionsMap, true); + options.visible(visible); + + options.center( + ObjectTranslationUtil.getLatLngFromMap((Map) optionsMap.get("center"))); + + boolean clickable = CollectionUtil.getBool("clickable", optionsMap, false); + options.clickable(clickable); + + String strokeColor = CollectionUtil.getString("strokeColor", optionsMap); + if (strokeColor != null) { + options.strokeColor(Color.parseColor(strokeColor)); + } + + String fillColor = CollectionUtil.getString("fillColor", optionsMap); + if (fillColor != null) { + options.fillColor(Color.parseColor(fillColor)); + } + + Circle circle = mGoogleMap.addCircle(options); + circleList.add(circle); + + return circle; + } + + public Marker addMarker(Map optionsMap) { + if (mGoogleMap == null) { + return null; + } + + String imagePath = CollectionUtil.getString("imgPath", optionsMap); + String title = CollectionUtil.getString("title", optionsMap); + String snippet = CollectionUtil.getString("snippet", optionsMap); + float alpha = Double.valueOf(CollectionUtil.getDouble("alpha", optionsMap, 1)).floatValue(); + float rotation = + Double.valueOf(CollectionUtil.getDouble("rotation", optionsMap, 0)).floatValue(); + boolean draggable = CollectionUtil.getBool("draggable", optionsMap, false); + boolean flat = CollectionUtil.getBool("flat", optionsMap, false); + boolean visible = CollectionUtil.getBool("visible", optionsMap, true); + + MarkerOptions options = new MarkerOptions(); + if (imagePath != null && !imagePath.isEmpty()) { + BitmapDescriptor icon = BitmapDescriptorFactory.fromPath(imagePath); + options.icon(icon); + } + + options.position( + ObjectTranslationUtil.getLatLngFromMap((Map) optionsMap.get("position"))); + + if (title != null) { + options.title(title); + } + + if (snippet != null) { + options.snippet(snippet); + } + + options.flat(flat); + options.alpha(alpha); + options.rotation(rotation); + options.draggable(draggable); + options.visible(visible); + + Marker marker = mGoogleMap.addMarker(options); + + markerList.add(marker); + + return marker; + } + + public Polyline addPolyline(Map optionsMap) { + if (mGoogleMap == null) { + return null; + } + + float width = Double.valueOf(CollectionUtil.getDouble("width", optionsMap, 0)).floatValue(); + boolean clickable = CollectionUtil.getBool("clickable", optionsMap, false); + boolean visible = CollectionUtil.getBool("visible", optionsMap, true); + + ArrayList latLngArr = (ArrayList) optionsMap.get("points"); + + if (latLngArr == null) { + return null; + } + + PolylineOptions options = new PolylineOptions(); + for (int i = 0; i < latLngArr.size(); i++) { + Map latLngMap = (Map) latLngArr.get(i); + LatLng latLng = createLatLng(latLngMap); + options.add(latLng); + } + + String color = CollectionUtil.getString("color", optionsMap); + if (color != null) { + options.color(Color.parseColor(color)); + } + + options.width(width); + options.clickable(clickable); + options.visible(visible); + + Polyline polyline = mGoogleMap.addPolyline(options); + polylineList.add(polyline); + + return polyline; + } + + public Polygon addPolygon(Map optionsMap) { + if (mGoogleMap == null) { + return null; + } + + String strokeColor = CollectionUtil.getString("strokeColor", optionsMap); + String fillColor = CollectionUtil.getString("fillColor", optionsMap); + float strokeWidth = + Double.valueOf(CollectionUtil.getDouble("strokeWidth", optionsMap, 0)).floatValue(); + boolean clickable = CollectionUtil.getBool("clickable", optionsMap, false); + boolean geodesic = CollectionUtil.getBool("geodesic", optionsMap, false); + boolean visible = CollectionUtil.getBool("visible", optionsMap, true); + + ArrayList latLngArr = (ArrayList) optionsMap.get("points"); + + PolygonOptions options = new PolygonOptions(); + for (int i = 0; i < latLngArr.size(); i++) { + Map latLngMap = (Map) latLngArr.get(i); + LatLng latLng = createLatLng(latLngMap); + options.add(latLng); + } + + ArrayList holesArr = (ArrayList) optionsMap.get("holes"); + + for (int i = 0; i < holesArr.size(); i++) { + ArrayList arr = (ArrayList) holesArr.get(i); + + List listHoles = new ArrayList<>(); + + for (int j = 0; j < arr.size(); j++) { + Map latLngMap = (Map) arr.get(j); + LatLng latLng = createLatLng(latLngMap); + + listHoles.add(latLng); + } + + options.addHole(listHoles); + } + + if (fillColor != null) { + options.fillColor(Color.parseColor(fillColor)); + } + + if (strokeColor != null) { + options.strokeColor(Color.parseColor(strokeColor)); + } + + options.strokeWidth(strokeWidth); + options.visible(visible); + options.geodesic(geodesic); + options.clickable(clickable); + + Polygon polygon = mGoogleMap.addPolygon(options); + polygonList.add(polygon); + + return polygon; + } + + public GroundOverlay addGroundOverlay(Map map) { + if (mGoogleMap == null) { + return null; + } + + String imagePath = CollectionUtil.getString("imgPath", map); + float width = Double.valueOf(CollectionUtil.getDouble("width", map, 0)).floatValue(); + float height = Double.valueOf(CollectionUtil.getDouble("height", map, 0)).floatValue(); + float transparency = + Double.valueOf(CollectionUtil.getDouble("transparency", map, 0)).floatValue(); + boolean clickable = CollectionUtil.getBool("clickable", map, false); + boolean visible = CollectionUtil.getBool("visible", map, true); + + Double lat = null; + Double lng = null; + if (map.containsKey("location")) { + Map latlng = (Map) map.get("location"); + if (latlng.get("lat") != null) lat = Double.parseDouble(latlng.get("lat").toString()); + if (latlng.get("lng") != null) lng = Double.parseDouble(latlng.get("lng").toString()); + } + + GroundOverlayOptions options = new GroundOverlayOptions(); + if (imagePath != null && !imagePath.isEmpty()) { + BitmapDescriptor bitmapDescriptor = BitmapDescriptorFactory.fromPath(imagePath); + options.image(bitmapDescriptor); + } + options.position(new LatLng(lat, lng), width, height); + options.transparency(transparency); + options.clickable(clickable); + options.visible(visible); + GroundOverlay groundOverlay = mGoogleMap.addGroundOverlay(options); + groundOverlayList.add(groundOverlay); + return groundOverlay; + } + + public void removeMarker(String id) { + UiThreadUtil.runOnUiThread( + () -> { + for (Marker m : markerList) { + if (m.getId().equals(id)) { + m.remove(); + markerList.remove(m); + return; + } + } + }); + } + + public void removePolyline(String id) { + for (Polyline p : polylineList) { + if (p.getId().equals(id)) { + p.remove(); + polylineList.remove(p); + return; + } + } + } + + public void removePolygon(String id) { + for (Polygon p : polygonList) { + if (p.getId().equals(id)) { + p.remove(); + polygonList.remove(p); + return; + } + } + } + + public void removeCircle(String id) { + for (Circle c : circleList) { + if (c.getId().equals(id)) { + c.remove(); + circleList.remove(c); + return; + } + } + } + + public void removeGroundOverlay(String id) { + for (GroundOverlay g : groundOverlayList) { + if (g.getId().equals(id)) { + g.remove(); + groundOverlayList.remove(g); + return; + } + } + } + + public void setMapStyle(String url) { + Executors.newSingleThreadExecutor() + .execute( + () -> { + try { + style = fetchJsonFromUrl(url); + } catch (IOException e) { + throw new RuntimeException(e); + } + + Activity activity = activitySupplier.get(); + if (activity != null) { + activity.runOnUiThread( + () -> { + MapStyleOptions options = new MapStyleOptions(style); + mGoogleMap.setMapStyle(options); + }); + } + }); + } + + /** Moves the position of the camera to hover over Melbourne. */ + public void moveCamera(Map map) { + LatLng latLng = ObjectTranslationUtil.getLatLngFromMap((Map) map.get("target")); + + float zoom = (float) CollectionUtil.getDouble("zoom", map, 0); + float tilt = (float) CollectionUtil.getDouble("tilt", map, 0); + float bearing = (float) CollectionUtil.getDouble("bearing", map, 0); + + CameraPosition cameraPosition = + CameraPosition.builder().target(latLng).zoom(zoom).tilt(tilt).bearing(bearing).build(); + + mGoogleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); + } + + public void animateCamera(Map map) { + if (mGoogleMap != null) { + int zoom = CollectionUtil.getInt("zoom", map, 0); + int tilt = CollectionUtil.getInt("tilt", map, 0); + int bearing = CollectionUtil.getInt("bearing", map, 0); + int animationDuration = CollectionUtil.getInt("duration", map, 0); + + CameraPosition cameraPosition = + new CameraPosition.Builder() + .target( + ObjectTranslationUtil.getLatLngFromMap( + (Map) map.get("target"))) // Set the target location + .zoom(zoom) // Set the desired zoom level + .tilt(tilt) // Set the desired tilt angle (0 for straight down, 90 for straight up) + .bearing(bearing) // Set the desired bearing (rotation angle in degrees) + .build(); + + mGoogleMap.animateCamera( + CameraUpdateFactory.newCameraPosition(cameraPosition), animationDuration, null); + } + } + + public void setZoomLevel(int level) { + if (mGoogleMap != null) { + mGoogleMap.animateCamera(CameraUpdateFactory.zoomTo(level)); + } + } + + public void setIndoorEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.setIndoorEnabled(isOn); + } + } + + public void setTrafficEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.setTrafficEnabled(isOn); + } + } + + public void setCompassEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.getUiSettings().setCompassEnabled(isOn); + } + } + + public void setRotateGesturesEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.getUiSettings().setRotateGesturesEnabled(isOn); + } + } + + public void setScrollGesturesEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.getUiSettings().setScrollGesturesEnabled(isOn); + } + } + + public void setScrollGesturesEnabledDuringRotateOrZoom(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.getUiSettings().setScrollGesturesEnabledDuringRotateOrZoom(isOn); + } + } + + public void setTiltGesturesEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.getUiSettings().setTiltGesturesEnabled(isOn); + } + } + + public void setZoomControlsEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.getUiSettings().setZoomControlsEnabled(isOn); + } + } + + public void setZoomGesturesEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.getUiSettings().setZoomGesturesEnabled(isOn); + } + } + + public void setBuildingsEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.setBuildingsEnabled(isOn); + } + } + + @SuppressLint("MissingPermission") + public void setMyLocationEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.setMyLocationEnabled(isOn); + } + } + + public void setMapToolbarEnabled(boolean isOn) { + if (mGoogleMap != null) { + mGoogleMap.getUiSettings().setMapToolbarEnabled(isOn); + } + } + + /** Toggles whether the location marker is enabled. */ + public void setMyLocationButtonEnabled(boolean isOn) { + if (mGoogleMap == null) { + return; + } + + UiThreadUtil.runOnUiThread( + () -> { + mGoogleMap.getUiSettings().setMyLocationButtonEnabled(isOn); + }); + } + + public void setMapType(int jsValue) { + if (mGoogleMap == null) { + return; + } + + mGoogleMap.setMapType(EnumTranslationUtil.getMapTypeFromJsValue(jsValue)); + } + + public void clearMapView() { + if (mGoogleMap == null) { + return; + } + + mGoogleMap.clear(); + } + + public void resetMinMaxZoomLevel() { + if (mGoogleMap == null) { + return; + } + + mGoogleMap.resetMinMaxZoomPreference(); + } + + @SuppressLint("MissingPermission") + public void setFollowingPerspective(int jsValue) { + if (mGoogleMap == null) { + return; + } + + mGoogleMap.followMyLocation(EnumTranslationUtil.getCameraPerspectiveFromJsValue(jsValue)); + } + + private String fetchJsonFromUrl(String urlString) throws IOException { + URL url = new URL(urlString); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + InputStream inputStream = connection.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + reader.close(); + inputStream.close(); + return stringBuilder.toString(); + } else { + // Handle error response + throw new IOException("Error response: " + responseCode); + } + } + + private LatLng createLatLng(Map map) { + Double lat = null; + Double lng = null; + if (map.containsKey("lat") && map.containsKey("lng")) { + if (map.get("lat") != null) lat = Double.parseDouble(map.get("lat").toString()); + if (map.get("lng") != null) lng = Double.parseDouble(map.get("lng").toString()); + } + + return new LatLng(lat, lng); + } +} diff --git a/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java index e3e3125..1cb60d6 100644 --- a/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java @@ -41,7 +41,6 @@ import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.GroundOverlayOptions; import com.google.android.gms.maps.model.LatLng; -import com.google.android.gms.maps.model.MapStyleOptions; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.gms.maps.model.Polygon; @@ -49,25 +48,20 @@ import com.google.android.gms.maps.model.Polyline; import com.google.android.gms.maps.model.PolylineOptions; import com.google.android.libraries.navigation.StylingOptions; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.Executors; /** * A fragment that displays a view with a Google Map using MapFragment. This fragment's lifecycle is * managed by NavViewManager. */ @SuppressLint("ValidFragment") -public class MapViewFragment extends SupportMapFragment implements IMapViewFragment { +public class MapViewFragment extends SupportMapFragment + implements IMapViewFragment, INavigationViewCallback { private static final String TAG = "MapViewFragment"; private GoogleMap mGoogleMap; + private MapViewController mMapViewController; private StylingOptions mStylingOptions; private List markerList = new ArrayList<>(); @@ -96,82 +90,69 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat public void onMapReady(GoogleMap googleMap) { mGoogleMap = googleMap; - emitEvent("onMapReady", null); + mMapViewController = new MapViewController(); + mMapViewController.initialize(googleMap, () -> requireActivity()); + + // Setup map listeners with the provided callback + mMapViewController.setupMapListeners(MapViewFragment.this); - mGoogleMap.setOnMarkerClickListener( - new GoogleMap.OnMarkerClickListener() { - @Override - public boolean onMarkerClick(Marker marker) { - emitEvent("onMarkerClick", ObjectTranslationUtil.getMapFromMarker(marker)); - return false; - } - }); - mGoogleMap.setOnPolylineClickListener( - new GoogleMap.OnPolylineClickListener() { - @Override - public void onPolylineClick(Polyline polyline) { - emitEvent( - "onPolylineClick", ObjectTranslationUtil.getMapFromPolyline(polyline)); - } - }); - mGoogleMap.setOnPolygonClickListener( - new GoogleMap.OnPolygonClickListener() { - @Override - public void onPolygonClick(Polygon polygon) { - emitEvent("onPolygonClick", ObjectTranslationUtil.getMapFromPolygon(polygon)); - } - }); - mGoogleMap.setOnCircleClickListener( - new GoogleMap.OnCircleClickListener() { - @Override - public void onCircleClick(Circle circle) { - emitEvent("onCircleClick", ObjectTranslationUtil.getMapFromCircle(circle)); - } - }); - mGoogleMap.setOnGroundOverlayClickListener( - new GoogleMap.OnGroundOverlayClickListener() { - @Override - public void onGroundOverlayClick(GroundOverlay groundOverlay) { - emitEvent( - "onGroundOverlayClick", - ObjectTranslationUtil.getMapFromGroundOverlay(groundOverlay)); - } - }); - - mGoogleMap.setOnInfoWindowClickListener( - new GoogleMap.OnInfoWindowClickListener() { - @Override - public void onInfoWindowClick(Marker marker) { - emitEvent( - "onMarkerInfoWindowTapped", ObjectTranslationUtil.getMapFromMarker(marker)); - } - }); - - mGoogleMap.setOnMapClickListener( - new GoogleMap.OnMapClickListener() { - @Override - public void onMapClick(LatLng latLng) { - emitEvent("onMapClick", ObjectTranslationUtil.getMapFromLatLng(latLng)); - } - }); + emitEvent("onMapReady", null); } }); } - public void applyStylingOptions() {} + @Override + public void onMapReady() { + emitEvent("onMapReady", null); + } - public void setStylingOptions(Map stylingOptions) {} + @Override + public void onRecenterButtonClick() { + emitEvent("onRecenterButtonClick", null); + } - @SuppressLint("MissingPermission") - public void setFollowingPerspective(int jsValue) { - if (mGoogleMap == null) { - return; - } + @Override + public void onMarkerClick(Marker marker) { + emitEvent("onMapReady", ObjectTranslationUtil.getMapFromMarker(marker)); + } + + @Override + public void onPolylineClick(Polyline polyline) { + emitEvent("onPolylineClick", ObjectTranslationUtil.getMapFromPolyline(polyline)); + } - mGoogleMap.followMyLocation(EnumTranslationUtil.getCameraPerspectiveFromJsValue(jsValue)); + @Override + public void onPolygonClick(Polygon polygon) { + emitEvent("onPolygonClick", ObjectTranslationUtil.getMapFromPolygon(polygon)); } - public void setNightModeOption(int jsValue) {} + @Override + public void onCircleClick(Circle circle) { + emitEvent("onCircleClick", ObjectTranslationUtil.getMapFromCircle(circle)); + } + + @Override + public void onGroundOverlayClick(GroundOverlay groundOverlay) { + emitEvent("onGroundOverlayClick", ObjectTranslationUtil.getMapFromGroundOverlay(groundOverlay)); + } + + @Override + public void onMarkerInfoWindowTapped(Marker marker) { + emitEvent("onInfoWindowClick", ObjectTranslationUtil.getMapFromMarker(marker)); + } + + @Override + public void onMapClick(LatLng latLng) { + emitEvent("onMapClick", ObjectTranslationUtil.getMapFromLatLng(latLng)); + } + + public MapViewController getMapController() { + return mMapViewController; + } + + public void applyStylingOptions() {} + + public void setStylingOptions(StylingOptions stylingOptions) {} public void setMapType(int jsValue) { if (mGoogleMap == null) { @@ -492,45 +473,7 @@ public GroundOverlay addGroundOverlay(Map map) { } public void setMapStyle(String url) { - Executors.newSingleThreadExecutor() - .execute( - () -> { - try { - style = fetchJsonFromUrl(url); - } catch (IOException e) { - throw new RuntimeException(e); - } - getActivity() - .runOnUiThread( - (Runnable) - () -> { - MapStyleOptions options = new MapStyleOptions(style); - mGoogleMap.setMapStyle(options); - }); - }); - } - - public String fetchJsonFromUrl(String urlString) throws IOException { - URL url = new URL(urlString); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - - int responseCode = connection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_OK) { - InputStream inputStream = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - StringBuilder stringBuilder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - stringBuilder.append(line); - } - reader.close(); - inputStream.close(); - return stringBuilder.toString(); - } else { - // Handle error response - throw new IOException("Error response: " + responseCode); - } + mMapViewController.setMapStyle(url); } /** Moves the position of the camera to hover over Melbourne. */ @@ -658,25 +601,6 @@ public GoogleMap getGoogleMap() { return mGoogleMap; } - // Navigation related function of the IViewFragment interface. Not used in this class. - public void setNavigationUiEnabled(boolean enableNavigationUi) {} - - public void setTripProgressBarEnabled(boolean enabled) {} - - public void setSpeedometerEnabled(boolean enabled) {} - - public void setSpeedLimitIconEnabled(boolean enabled) {} - - public void setTrafficIncidentCardsEnabled(boolean enabled) {} - - public void setEtaCardEnabled(boolean enabled) {} - - public void setHeaderEnabled(boolean enabled) {} - - public void setRecenterButtonEnabled(boolean enabled) {} - - public void showRouteOverview() {} - public class NavViewEvent extends Event { private String eventName; private @Nullable WritableMap eventData; diff --git a/android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java b/android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java new file mode 100644 index 0000000..dec5202 --- /dev/null +++ b/android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java @@ -0,0 +1,537 @@ +/** + * Copyright 2024 Google LLC + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.react.navsdk; + +import android.location.Location; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.NativeArray; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.google.android.gms.maps.UiSettings; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.Circle; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.Polygon; +import com.google.android.gms.maps.model.Polyline; +import com.google.android.libraries.navigation.StylingOptions; +import java.util.Map; + +/** + * This exposes a series of methods that can be called diretly from the React Native code. They have + * been implemented using promises as it's not recommended for them to be synchronous. + */ +public class NavAutoModule extends ReactContextBaseJavaModule implements INavigationAutoCallback { + public static final String REACT_CLASS = "NavAutoModule"; + private static final String TAG = "AndroidAutoModule"; + private static NavAutoModule instance; + private static ModuleReadyListener moduleReadyListener; + + ReactApplicationContext reactContext; + private MapViewController mMapViewController; + private StylingOptions mStylingOptions; + private INavigationViewController mNavigationViewController; + + public interface ModuleReadyListener { + void onModuleReady(); + } + + public NavAutoModule(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + instance = this; + if (moduleReadyListener != null) { + moduleReadyListener.onModuleReady(); + } + } + + @Override + public String getName() { + return REACT_CLASS; + } + + // Called by the AndroidAuto implementation. See SampleApp for example. + public static synchronized NavAutoModule getInstance() { + if (instance == null) { + throw new IllegalStateException(REACT_CLASS + " instance is null"); + } + return instance; + } + + public static void setModuleReadyListener(ModuleReadyListener listener) { + moduleReadyListener = listener; + if (instance != null && moduleReadyListener != null) { + moduleReadyListener.onModuleReady(); + } + } + + public void androidAutoNavigationScreenInitialized( + MapViewController mapViewController, INavigationViewController navigationViewController) { + mMapViewController = mapViewController; + mNavigationViewController = navigationViewController; + if (mStylingOptions != null && mNavigationViewController != null) { + mNavigationViewController.setStylingOptions(mStylingOptions); + } + sendScreenState(true); + } + + public void androidAutoNavigationScreenDisposed() { + sendScreenState(false); + mMapViewController = null; + mNavigationViewController = null; + } + + public void setStylingOptions(Map stylingOptions) { + mStylingOptions = new StylingOptionsBuilder.Builder(stylingOptions).build(); + if (mStylingOptions != null && mNavigationViewController != null) { + mNavigationViewController.setStylingOptions(mStylingOptions); + } + } + + @ReactMethod + public void setMapType(int jsValue) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setMapType(jsValue); + }); + } + + @ReactMethod + public void setMapStyle(String url) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setMapStyle(url); + }); + } + + @ReactMethod + public void setMapToolbarEnabled(boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setMapToolbarEnabled(isOn); + }); + } + + @ReactMethod + public void addCircle(ReadableMap circleOptionsMap, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + Circle circle = mMapViewController.addCircle(circleOptionsMap.toHashMap()); + + promise.resolve(ObjectTranslationUtil.getMapFromCircle(circle)); + }); + } + + @ReactMethod + public void addMarker(ReadableMap markerOptionsMap, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + Marker marker = mMapViewController.addMarker(markerOptionsMap.toHashMap()); + + promise.resolve(ObjectTranslationUtil.getMapFromMarker(marker)); + }); + } + + @ReactMethod + public void addPolyline(ReadableMap polylineOptionsMap, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + Polyline polyline = mMapViewController.addPolyline(polylineOptionsMap.toHashMap()); + + promise.resolve(ObjectTranslationUtil.getMapFromPolyline(polyline)); + }); + } + + @ReactMethod + public void addPolygon(ReadableMap polygonOptionsMap, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + Polygon polygon = mMapViewController.addPolygon(polygonOptionsMap.toHashMap()); + + promise.resolve(ObjectTranslationUtil.getMapFromPolygon(polygon)); + }); + } + + @ReactMethod + public void removeCircle(String id) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.removeCircle(id); + }); + } + + @ReactMethod + public void removeMarker(String id) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.removeMarker(id); + }); + } + + @ReactMethod + public void removePolyline(String id) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.removePolyline(id); + }); + } + + @ReactMethod + public void removePolygon(String id) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.removePolygon(id); + }); + } + + @ReactMethod + public void clearMapView() { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.clearMapView(); + }); + } + + @ReactMethod + public void setIndoorEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setIndoorEnabled(isOn); + }); + } + + @ReactMethod + public void setTrafficEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setTrafficEnabled(isOn); + }); + } + + @ReactMethod + public void setCompassEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setCompassEnabled(isOn); + }); + } + + @ReactMethod + public void setMyLocationButtonEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setMyLocationButtonEnabled(isOn); + }); + } + + @ReactMethod + public void setMyLocationEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setMyLocationEnabled(isOn); + }); + } + + @ReactMethod + public void setRotateGesturesEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setRotateGesturesEnabled(isOn); + }); + } + + @ReactMethod + public void setScrollGesturesEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setScrollGesturesEnabled(isOn); + }); + } + + @ReactMethod + public void setScrollGesturesEnabledDuringRotateOrZoom(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setScrollGesturesEnabledDuringRotateOrZoom(isOn); + }); + } + + @ReactMethod + public void setZoomControlsEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setZoomControlsEnabled(isOn); + }); + } + + @ReactMethod + public void setZoomLevel(final Integer level, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + mMapViewController.setZoomLevel(level); + promise.resolve(true); + }); + } + + @ReactMethod + public void setTiltGesturesEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setTiltGesturesEnabled(isOn); + }); + } + + @ReactMethod + public void setZoomGesturesEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setZoomGesturesEnabled(isOn); + }); + } + + @ReactMethod + public void setBuildingsEnabled(Boolean isOn) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + mMapViewController.setBuildingsEnabled(isOn); + }); + } + + @ReactMethod + public void getCameraPosition(final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + CameraPosition cp = mMapViewController.getGoogleMap().getCameraPosition(); + + if (cp == null) { + promise.resolve(null); + return; + } + + LatLng target = cp.target; + WritableMap map = Arguments.createMap(); + map.putDouble("bearing", cp.bearing); + map.putDouble("tilt", cp.tilt); + map.putDouble("zoom", cp.zoom); + map.putMap("target", ObjectTranslationUtil.getMapFromLatLng(target)); + + promise.resolve(map); + }); + } + + @ReactMethod + public void getMyLocation(final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + try { + Location location = mMapViewController.getGoogleMap().getMyLocation(); + promise.resolve(ObjectTranslationUtil.getMapFromLocation(location)); + } catch (Exception e) { + promise.resolve(null); + return; + } + }); + } + + @ReactMethod + public void getUiSettings(final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + UiSettings settings = mMapViewController.getGoogleMap().getUiSettings(); + + if (settings == null) { + promise.resolve(null); + return; + } + + WritableMap map = Arguments.createMap(); + map.putBoolean("isCompassEnabled", settings.isCompassEnabled()); + map.putBoolean("isMapToolbarEnabled", settings.isMapToolbarEnabled()); + map.putBoolean("isIndoorLevelPickerEnabled", settings.isIndoorLevelPickerEnabled()); + map.putBoolean("isRotateGesturesEnabled", settings.isRotateGesturesEnabled()); + map.putBoolean("isScrollGesturesEnabled", settings.isScrollGesturesEnabled()); + map.putBoolean( + "isScrollGesturesEnabledDuringRotateOrZoom", + settings.isScrollGesturesEnabledDuringRotateOrZoom()); + map.putBoolean("isTiltGesturesEnabled", settings.isTiltGesturesEnabled()); + map.putBoolean("isZoomControlsEnabled", settings.isZoomControlsEnabled()); + map.putBoolean("isZoomGesturesEnabled", settings.isZoomGesturesEnabled()); + + promise.resolve(map); + }); + } + + @ReactMethod + public void isMyLocationEnabled(final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + promise.resolve(mMapViewController.getGoogleMap().isMyLocationEnabled()); + }); + } + + @ReactMethod + public void moveCamera(ReadableMap map) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + return; + } + + mMapViewController.moveCamera(map.toHashMap()); + }); + } + + @ReactMethod + public void isAutoScreenAvailable(final Promise promise) { + promise.resolve(mMapViewController != null); + } + + public void sendScreenState(boolean available) { + WritableNativeArray params = new WritableNativeArray(); + params.pushBoolean(available); + + sendCommandToReactNative("onAutoScreenAvailabilityChanged", params); + } + + @Override + public void onCustomNavigationAutoEvent(String type, ReadableMap data) { + WritableMap map = Arguments.createMap(); + map.putString("type", type); + map.putMap("data", data); + + WritableNativeArray params = new WritableNativeArray(); + params.pushMap(map); + + sendCommandToReactNative("onCustomNavigationAutoEvent", params); + } + + /** Send command to react native. */ + private void sendCommandToReactNative(String functionName, NativeArray params) { + ReactContext reactContext = getReactApplicationContext(); + + if (reactContext != null) { + CatalystInstance catalystInstance = reactContext.getCatalystInstance(); + catalystInstance.callFunction(Constants.NAV_AUTO_JAVASCRIPT_FLAG, functionName, params); + } + } +} diff --git a/android/src/main/java/com/google/android/react/navsdk/NavModule.java b/android/src/main/java/com/google/android/react/navsdk/NavModule.java index 31ca456..bcb6791 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavModule.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavModule.java @@ -19,6 +19,7 @@ import androidx.lifecycle.Observer; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.NativeArray; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; @@ -51,14 +52,17 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; /** * This exposes a series of methods that can be called diretly from the React Native code. They have * been implemented using promises as it's not recommended for them to be synchronous. */ public class NavModule extends ReactContextBaseJavaModule implements INavigationCallback { + public static final String REACT_CLASS = "NavModule"; private static final String TAG = "NavModule"; private static NavModule instance; + private static ModuleReadyListener moduleReadyListener; ReactApplicationContext reactContext; private Navigator mNavigator; @@ -66,19 +70,40 @@ public class NavModule extends ReactContextBaseJavaModule implements INavigation private ListenableResultFuture pendingRoute; private RoadSnappedLocationProvider mRoadSnappedLocationProvider; private NavViewManager mNavViewManager; + private final CopyOnWriteArrayList mNavigationReadyListeners = + new CopyOnWriteArrayList<>(); + private boolean mIsListeningRoadSnappedLocation = false; private HashMap tocParamsMap; + public interface ModuleReadyListener { + void onModuleReady(); + } + + public interface NavigationReadyListener { + void onReady(boolean ready); + } + public NavModule(ReactApplicationContext reactContext, NavViewManager navViewManager) { super(reactContext); this.reactContext = reactContext; mNavViewManager = navViewManager; instance = this; + if (moduleReadyListener != null) { + moduleReadyListener.onModuleReady(); + } + } + + public static void setModuleReadyListener(ModuleReadyListener listener) { + moduleReadyListener = listener; + if (instance != null && moduleReadyListener != null) { + moduleReadyListener.onModuleReady(); + } } public static synchronized NavModule getInstance() { if (instance == null) { - throw new IllegalStateException("NavModule instance is null"); + throw new IllegalStateException(REACT_CLASS + " instance is null"); } return instance; } @@ -89,7 +114,7 @@ public Navigator getNavigator() { @Override public String getName() { - return "NavModule"; + return REACT_CLASS; } @Override @@ -102,19 +127,15 @@ public Map getConstants() { new Navigator.ArrivalListener() { @Override public void onArrival(ArrivalEvent arrivalEvent) { - if (reactContext != null) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - - WritableMap map = Arguments.createMap(); - map.putMap( - "waypoint", ObjectTranslationUtil.getMapFromWaypoint(arrivalEvent.getWaypoint())); - map.putBoolean("isFinalDestination", arrivalEvent.isFinalDestination()); + WritableMap map = Arguments.createMap(); + map.putMap( + "waypoint", ObjectTranslationUtil.getMapFromWaypoint(arrivalEvent.getWaypoint())); + map.putBoolean("isFinalDestination", arrivalEvent.isFinalDestination()); - WritableNativeArray params = new WritableNativeArray(); - params.pushMap(map); + WritableNativeArray params = new WritableNativeArray(); + params.pushMap(map); - catalystInstance.callFunction(Constants.NAV_JAVASCRIPT_FLAG, "onArrival", params); - } + sendCommandToReactNative("onArrival", params); } }; @@ -122,23 +143,18 @@ public void onArrival(ArrivalEvent arrivalEvent) { new LocationListener() { @Override public void onLocationChanged(final Location location) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - WritableNativeArray params = new WritableNativeArray(); params.pushMap(ObjectTranslationUtil.getMapFromLocation(location)); - catalystInstance.callFunction(Constants.NAV_JAVASCRIPT_FLAG, "onLocationChanged", params); + sendCommandToReactNative("onLocationChanged", params); } @Override public void onRawLocationUpdate(final Location location) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - WritableNativeArray params = new WritableNativeArray(); params.pushMap(ObjectTranslationUtil.getMapFromLocation(location)); - catalystInstance.callFunction( - Constants.NAV_JAVASCRIPT_FLAG, "onRawLocationChanged", params); + sendCommandToReactNative("onRawLocationChanged", params); } }; @@ -146,7 +162,7 @@ public void onRawLocationUpdate(final Location location) { new Navigator.RouteChangedListener() { @Override public void onRouteChanged() { - sendCommandToReactNative("onRouteChanged", null); + sendCommandToReactNative("onRouteChanged", (NativeArray) null); } }; @@ -154,7 +170,7 @@ public void onRouteChanged() { new Navigator.TrafficUpdatedListener() { @Override public void onTrafficUpdated() { - sendCommandToReactNative("onTrafficUpdated", null); + sendCommandToReactNative("onTrafficUpdated", (NativeArray) null); } }; @@ -162,7 +178,7 @@ public void onTrafficUpdated() { new Navigator.ReroutingListener() { @Override public void onReroutingRequestedByOffRoute() { - sendCommandToReactNative("onReroutingRequestedByOffRoute", null); + sendCommandToReactNative("onReroutingRequestedByOffRoute", (NativeArray) null); } }; @@ -170,13 +186,15 @@ public void onReroutingRequestedByOffRoute() { new Navigator.RemainingTimeOrDistanceChangedListener() { @Override public void onRemainingTimeOrDistanceChanged() { - sendCommandToReactNative("onRemainingTimeOrDistanceChanged", null); + sendCommandToReactNative("onRemainingTimeOrDistanceChanged", (NativeArray) null); } }; @ReactMethod private void cleanup() { - mRoadSnappedLocationProvider.removeLocationListener(mLocationListener); + if (mIsListeningRoadSnappedLocation) { + mRoadSnappedLocationProvider.removeLocationListener(mLocationListener); + } mNavigator.unregisterServiceForNavUpdates(); mNavigator.removeArrivalListener(mArrivalListener); mNavigator.removeReroutingListener(mReroutingListener); @@ -186,6 +204,10 @@ private void cleanup() { mRemainingTimeOrDistanceChangedListener); mWaypoints.clear(); + for (NavigationReadyListener listener : mNavigationReadyListeners) { + listener.onReady(false); + } + UiThreadUtil.runOnUiThread( () -> { mNavigator.clearDestinations(); @@ -215,10 +237,24 @@ public void initializeNavigator(@Nullable ReadableMap tocParams) { private void onNavigationReady() { mNavViewManager.applyStylingOptions(); - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - WritableNativeArray params = new WritableNativeArray(); + sendCommandToReactNative("onNavigationReady", (NativeArray) null); - catalystInstance.callFunction(Constants.NAV_JAVASCRIPT_FLAG, "onNavigationReady", params); + for (NavigationReadyListener listener : mNavigationReadyListeners) { + listener.onReady(true); + } + } + + public void registerNavigationReadyListener(NavigationReadyListener listener) { + if (listener != null && !mNavigationReadyListeners.contains(listener)) { + mNavigationReadyListeners.add(listener); + if (mNavigator != null) { + listener.onReady(true); + } + } + } + + public void unRegisterNavigationReadyListener(NavigationReadyListener listener) { + mNavigationReadyListeners.remove(listener); } private void onNavigationInitError(int errorCode) { @@ -445,7 +481,7 @@ public void startGuidance() { } mNavigator.startGuidance(); - sendCommandToReactNative("onStartGuidance", null); + sendCommandToReactNative("onStartGuidance", (NativeArray) null); } @ReactMethod @@ -598,17 +634,22 @@ public void getTraveledPath(final Promise promise) { promise.resolve(arr); } - private void sendCommandToReactNative(String functionName, String args) { + /** Send command to react native with string param. */ + private void sendCommandToReactNative(String functionName, String stringParam) { + WritableNativeArray params = new WritableNativeArray(); + + if (stringParam != null) { + params.pushString("" + stringParam); + } + sendCommandToReactNative(functionName, params); + } + + /** Send command to react native. */ + private void sendCommandToReactNative(String functionName, NativeArray params) { ReactContext reactContext = getReactApplicationContext(); if (reactContext != null) { CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - WritableNativeArray params = new WritableNativeArray(); - - if (args != null) { - params.pushString("" + args); - } - catalystInstance.callFunction(Constants.NAV_JAVASCRIPT_FLAG, functionName, params); } } @@ -679,10 +720,12 @@ public void resetTermsAccepted() { @ReactMethod public void startUpdatingLocation() { mRoadSnappedLocationProvider.addLocationListener(mLocationListener); + mIsListeningRoadSnappedLocation = true; } @ReactMethod public void stopUpdatingLocation() { + mIsListeningRoadSnappedLocation = false; mRoadSnappedLocationProvider.removeLocationListener(mLocationListener); } @@ -690,8 +733,6 @@ private void showNavInfo(NavInfo navInfo) { if (navInfo == null || reactContext == null) { return; } - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - WritableMap map = Arguments.createMap(); map.putInt("navState", navInfo.getNavState()); @@ -721,7 +762,7 @@ private void showNavInfo(NavInfo navInfo) { WritableNativeArray params = new WritableNativeArray(); params.pushMap(map); - catalystInstance.callFunction(Constants.NAV_JAVASCRIPT_FLAG, "onTurnByTurn", params); + sendCommandToReactNative("onTurnByTurn", params); } @Override diff --git a/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java index c10f9f6..a1e7bc6 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java @@ -13,68 +13,40 @@ */ package com.google.android.react.navsdk; -import android.Manifest.permission; import android.annotation.SuppressLint; -import android.content.pm.PackageManager; -import android.graphics.Color; import android.os.Bundle; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.UIManagerHelper; import com.facebook.react.uimanager.events.Event; import com.facebook.react.uimanager.events.EventDispatcher; -import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.OnMapReadyCallback; -import com.google.android.gms.maps.model.BitmapDescriptor; -import com.google.android.gms.maps.model.BitmapDescriptorFactory; -import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.Circle; -import com.google.android.gms.maps.model.CircleOptions; import com.google.android.gms.maps.model.GroundOverlay; -import com.google.android.gms.maps.model.GroundOverlayOptions; import com.google.android.gms.maps.model.LatLng; -import com.google.android.gms.maps.model.MapStyleOptions; import com.google.android.gms.maps.model.Marker; -import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.gms.maps.model.Polygon; -import com.google.android.gms.maps.model.PolygonOptions; import com.google.android.gms.maps.model.Polyline; -import com.google.android.gms.maps.model.PolylineOptions; import com.google.android.libraries.navigation.NavigationView; import com.google.android.libraries.navigation.StylingOptions; import com.google.android.libraries.navigation.SupportNavigationFragment; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executors; /** * A fragment that displays a navigation view with a Google Map using SupportNavigationFragment. * This fragment's lifecycle is managed by NavViewManager. */ -public class NavViewFragment extends SupportNavigationFragment implements INavViewFragment { +public class NavViewFragment extends SupportNavigationFragment + implements INavViewFragment, INavigationViewCallback { private static final String TAG = "NavViewFragment"; + private MapViewController mMapViewController; private GoogleMap mGoogleMap; private StylingOptions mStylingOptions; - private List markerList = new ArrayList<>(); - private List polylineList = new ArrayList<>(); - private List polygonList = new ArrayList<>(); - private List groundOverlayList = new ArrayList<>(); - private List circleList = new ArrayList<>(); private int viewTag; // React native view tag. private ReactApplicationContext reactContext; @@ -105,80 +77,26 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat public void onMapReady(GoogleMap googleMap) { mGoogleMap = googleMap; + mMapViewController = new MapViewController(); + mMapViewController.initialize(googleMap, () -> requireActivity()); + + // Setup map listeners with the provided callback + mMapViewController.setupMapListeners(NavViewFragment.this); + emitEvent("onMapReady", null); setNavigationUiEnabled(NavModule.getInstance().getNavigator() != null); - - mGoogleMap.setOnMarkerClickListener( - new GoogleMap.OnMarkerClickListener() { - @Override - public boolean onMarkerClick(Marker marker) { - emitEvent("onMarkerClick", ObjectTranslationUtil.getMapFromMarker(marker)); - return false; - } - }); - mGoogleMap.setOnPolylineClickListener( - new GoogleMap.OnPolylineClickListener() { - @Override - public void onPolylineClick(Polyline polyline) { - emitEvent( - "onPolylineClick", ObjectTranslationUtil.getMapFromPolyline(polyline)); - } - }); - mGoogleMap.setOnPolygonClickListener( - new GoogleMap.OnPolygonClickListener() { - @Override - public void onPolygonClick(Polygon polygon) { - emitEvent("onPolygonClick", ObjectTranslationUtil.getMapFromPolygon(polygon)); - } - }); - mGoogleMap.setOnCircleClickListener( - new GoogleMap.OnCircleClickListener() { - @Override - public void onCircleClick(Circle circle) { - emitEvent("onCircleClick", ObjectTranslationUtil.getMapFromCircle(circle)); - } - }); - mGoogleMap.setOnGroundOverlayClickListener( - new GoogleMap.OnGroundOverlayClickListener() { - @Override - public void onGroundOverlayClick(GroundOverlay groundOverlay) { - emitEvent( - "onGroundOverlayClick", - ObjectTranslationUtil.getMapFromGroundOverlay(groundOverlay)); - } - }); - - mGoogleMap.setOnInfoWindowClickListener( - new GoogleMap.OnInfoWindowClickListener() { - @Override - public void onInfoWindowClick(Marker marker) { - emitEvent( - "onMarkerInfoWindowTapped", ObjectTranslationUtil.getMapFromMarker(marker)); - } - }); - - mGoogleMap.setOnMapClickListener( - new GoogleMap.OnMapClickListener() { - @Override - public void onMapClick(LatLng latLng) { - emitEvent("onMapClick", ObjectTranslationUtil.getMapFromLatLng(latLng)); - } - }); + addOnRecenterButtonClickedListener(onRecenterButtonClickedListener); } }); + } - Executors.newSingleThreadExecutor() - .execute( - () -> { - requireActivity() - .runOnUiThread( - (Runnable) - () -> { - super.addOnRecenterButtonClickedListener( - onRecenterButtonClickedListener); - }); - }); + public MapViewController getMapController() { + return mMapViewController; + } + + public void setMapStyle(String url) { + mMapViewController.setMapStyle(url); } public void applyStylingOptions() { @@ -187,490 +105,57 @@ public void applyStylingOptions() { } } - public void setStylingOptions(Map stylingOptions) { - mStylingOptions = new StylingOptionsBuilder.Builder(stylingOptions).build(); - } - - @SuppressLint("MissingPermission") - public void setFollowingPerspective(int jsValue) { - if (mGoogleMap == null) { - return; - } - - mGoogleMap.followMyLocation(EnumTranslationUtil.getCameraPerspectiveFromJsValue(jsValue)); + public void setStylingOptions(StylingOptions stylingOptions) { + mStylingOptions = stylingOptions; } public void setNightModeOption(int jsValue) { super.setForceNightMode(EnumTranslationUtil.getForceNightModeFromJsValue(jsValue)); } - public void setMapType(int jsValue) { - if (mGoogleMap == null) { - return; - } - - mGoogleMap.setMapType(EnumTranslationUtil.getMapTypeFromJsValue(jsValue)); - } - - public void clearMapView() { - if (mGoogleMap == null) { - return; - } - - mGoogleMap.clear(); - } - - public void resetMinMaxZoomLevel() { - if (mGoogleMap == null) { - return; - } - - mGoogleMap.resetMinMaxZoomPreference(); - } - - public void animateCamera(Map map) { - if (mGoogleMap != null) { - int zoom = CollectionUtil.getInt("zoom", map, 0); - int tilt = CollectionUtil.getInt("tilt", map, 0); - int bearing = CollectionUtil.getInt("bearing", map, 0); - int animationDuration = CollectionUtil.getInt("duration", map, 0); - - CameraPosition cameraPosition = - new CameraPosition.Builder() - .target( - ObjectTranslationUtil.getLatLngFromMap( - (Map) map.get("target"))) // Set the target location - .zoom(zoom) // Set the desired zoom level - .tilt(tilt) // Set the desired tilt angle (0 for straight down, 90 for straight up) - .bearing(bearing) // Set the desired bearing (rotation angle in degrees) - .build(); - - mGoogleMap.animateCamera( - CameraUpdateFactory.newCameraPosition(cameraPosition), animationDuration, null); - } - } - - public Circle addCircle(Map optionsMap) { - if (mGoogleMap == null) { - return null; - } - - CircleOptions options = new CircleOptions(); - - float strokeWidth = - Double.valueOf(CollectionUtil.getDouble("strokeWidth", optionsMap, 0)).floatValue(); - options.strokeWidth(strokeWidth); - - double radius = CollectionUtil.getDouble("radius", optionsMap, 0.0); - options.radius(radius); - - boolean visible = CollectionUtil.getBool("visible", optionsMap, true); - options.visible(visible); - - options.center(ObjectTranslationUtil.getLatLngFromMap((Map) optionsMap.get("center"))); - - boolean clickable = CollectionUtil.getBool("clickable", optionsMap, false); - options.clickable(clickable); - - String strokeColor = CollectionUtil.getString("strokeColor", optionsMap); - if (strokeColor != null) { - options.strokeColor(Color.parseColor(strokeColor)); - } - - String fillColor = CollectionUtil.getString("fillColor", optionsMap); - if (fillColor != null) { - options.fillColor(Color.parseColor(fillColor)); - } - - Circle circle = mGoogleMap.addCircle(options); - circleList.add(circle); - - return circle; - } - - public Marker addMarker(Map optionsMap) { - if (mGoogleMap == null) { - return null; - } - - String imagePath = CollectionUtil.getString("imgPath", optionsMap); - String title = CollectionUtil.getString("title", optionsMap); - String snippet = CollectionUtil.getString("snippet", optionsMap); - float alpha = Double.valueOf(CollectionUtil.getDouble("alpha", optionsMap, 1)).floatValue(); - float rotation = - Double.valueOf(CollectionUtil.getDouble("rotation", optionsMap, 0)).floatValue(); - boolean draggable = CollectionUtil.getBool("draggable", optionsMap, false); - boolean flat = CollectionUtil.getBool("flat", optionsMap, false); - boolean visible = CollectionUtil.getBool("visible", optionsMap, true); - - MarkerOptions options = new MarkerOptions(); - if (imagePath != null && !imagePath.isEmpty()) { - BitmapDescriptor icon = BitmapDescriptorFactory.fromPath(imagePath); - options.icon(icon); - } - - options.position(ObjectTranslationUtil.getLatLngFromMap((Map) optionsMap.get("position"))); - - if (title != null) { - options.title(title); - } - - if (snippet != null) { - options.snippet(snippet); - } - - options.flat(flat); - options.alpha(alpha); - options.rotation(rotation); - options.draggable(draggable); - options.visible(visible); - - Marker marker = mGoogleMap.addMarker(options); - - markerList.add(marker); - - return marker; - } - - public Polyline addPolyline(Map optionsMap) { - if (mGoogleMap == null) { - return null; - } - - float width = Double.valueOf(CollectionUtil.getDouble("width", optionsMap, 0)).floatValue(); - boolean clickable = CollectionUtil.getBool("clickable", optionsMap, false); - boolean visible = CollectionUtil.getBool("visible", optionsMap, true); - - ArrayList latLngArr = (ArrayList) optionsMap.get("points"); - - PolylineOptions options = new PolylineOptions(); - for (int i = 0; i < latLngArr.size(); i++) { - Map latLngMap = (Map) latLngArr.get(i); - LatLng latLng = createLatLng(latLngMap); - options.add(latLng); - } - - String color = CollectionUtil.getString("color", optionsMap); - if (color != null) { - options.color(Color.parseColor(color)); - } - - options.width(width); - options.clickable(clickable); - options.visible(visible); - - Polyline polyline = mGoogleMap.addPolyline(options); - polylineList.add(polyline); - - return polyline; - } - - public Polygon addPolygon(Map optionsMap) { - if (mGoogleMap == null) { - return null; - } - - String strokeColor = CollectionUtil.getString("strokeColor", optionsMap); - String fillColor = CollectionUtil.getString("fillColor", optionsMap); - float strokeWidth = - Double.valueOf(CollectionUtil.getDouble("strokeWidth", optionsMap, 0)).floatValue(); - boolean clickable = CollectionUtil.getBool("clickable", optionsMap, false); - boolean geodesic = CollectionUtil.getBool("geodesic", optionsMap, false); - boolean visible = CollectionUtil.getBool("visible", optionsMap, true); - - ArrayList latLngArr = (ArrayList) optionsMap.get("points"); - - PolygonOptions options = new PolygonOptions(); - for (int i = 0; i < latLngArr.size(); i++) { - Map latLngMap = (Map) latLngArr.get(i); - LatLng latLng = createLatLng(latLngMap); - options.add(latLng); - } - - ArrayList holesArr = (ArrayList) optionsMap.get("holes"); - - for (int i = 0; i < holesArr.size(); i++) { - ArrayList arr = (ArrayList) holesArr.get(i); - - List listHoles = new ArrayList<>(); - - for (int j = 0; j < arr.size(); j++) { - Map latLngMap = (Map) arr.get(j); - LatLng latLng = createLatLng(latLngMap); - - listHoles.add(latLng); - } - - options.addHole(listHoles); - } - - if (fillColor != null) { - options.fillColor(Color.parseColor(fillColor)); - } - - if (strokeColor != null) { - options.strokeColor(Color.parseColor(strokeColor)); - } - - options.strokeWidth(strokeWidth); - options.visible(visible); - options.geodesic(geodesic); - options.clickable(clickable); - - Polygon polygon = mGoogleMap.addPolygon(options); - polygonList.add(polygon); - - return polygon; - } - - public void removeMarker(String id) { - UiThreadUtil.runOnUiThread( - () -> { - for (Marker m : markerList) { - if (m.getId().equals(id)) { - m.remove(); - markerList.remove(m); - return; - } - } - }); - } - - public void removePolyline(String id) { - for (Polyline p : polylineList) { - if (p.getId().equals(id)) { - p.remove(); - polylineList.remove(p); - return; - } - } - } - - public void removePolygon(String id) { - for (Polygon p : polygonList) { - if (p.getId().equals(id)) { - p.remove(); - polygonList.remove(p); - return; - } - } - } - - public void removeCircle(String id) { - for (Circle c : circleList) { - if (c.getId().equals(id)) { - c.remove(); - circleList.remove(c); - return; - } - } - } - - public void removeGroundOverlay(String id) { - for (GroundOverlay g : groundOverlayList) { - if (g.getId().equals(id)) { - g.remove(); - groundOverlayList.remove(g); - return; - } - } - } - - private LatLng createLatLng(Map map) { - Double lat = null; - Double lng = null; - if (map.containsKey("lat") && map.containsKey("lng")) { - if (map.get("lat") != null) lat = Double.parseDouble(map.get("lat").toString()); - if (map.get("lng") != null) lng = Double.parseDouble(map.get("lng").toString()); - } - - return new LatLng(lat, lng); - } - - public GroundOverlay addGroundOverlay(Map map) { - if (mGoogleMap == null) { - return null; - } - - String imagePath = CollectionUtil.getString("imgPath", map); - float width = Double.valueOf(CollectionUtil.getDouble("width", map, 0)).floatValue(); - float height = Double.valueOf(CollectionUtil.getDouble("height", map, 0)).floatValue(); - float transparency = - Double.valueOf(CollectionUtil.getDouble("transparency", map, 0)).floatValue(); - boolean clickable = CollectionUtil.getBool("clickable", map, false); - boolean visible = CollectionUtil.getBool("visible", map, true); - - Double lat = null; - Double lng = null; - if (map.containsKey("location")) { - Map latlng = (Map) map.get("location"); - if (latlng.get("lat") != null) lat = Double.parseDouble(latlng.get("lat").toString()); - if (latlng.get("lng") != null) lng = Double.parseDouble(latlng.get("lng").toString()); - } - - GroundOverlayOptions options = new GroundOverlayOptions(); - if (imagePath != null && !imagePath.isEmpty()) { - BitmapDescriptor bitmapDescriptor = BitmapDescriptorFactory.fromPath(imagePath); - options.image(bitmapDescriptor); - } - options.position(new LatLng(lat, lng), width, height); - options.transparency(transparency); - options.clickable(clickable); - options.visible(visible); - GroundOverlay groundOverlay = mGoogleMap.addGroundOverlay(options); - groundOverlayList.add(groundOverlay); - return groundOverlay; - } - - public void setMapStyle(String url) { - Executors.newSingleThreadExecutor() - .execute( - () -> { - try { - style = fetchJsonFromUrl(url); - } catch (IOException e) { - throw new RuntimeException(e); - } - requireActivity() - .runOnUiThread( - (Runnable) - () -> { - MapStyleOptions options = new MapStyleOptions(style); - mGoogleMap.setMapStyle(options); - }); - }); - } - - public String fetchJsonFromUrl(String urlString) throws IOException { - URL url = new URL(urlString); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - - int responseCode = connection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_OK) { - InputStream inputStream = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - StringBuilder stringBuilder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - stringBuilder.append(line); - } - reader.close(); - inputStream.close(); - return stringBuilder.toString(); - } else { - // Handle error response - throw new IOException("Error response: " + responseCode); - } - } - - /** Moves the position of the camera to hover over Melbourne. */ - public void moveCamera(Map map) { - LatLng latLng = ObjectTranslationUtil.getLatLngFromMap((Map) map.get("target")); - - float zoom = (float) CollectionUtil.getDouble("zoom", map, 0); - float tilt = (float) CollectionUtil.getDouble("tilt", map, 0); - float bearing = (float) CollectionUtil.getDouble("bearing", map, 0); - - CameraPosition cameraPosition = - CameraPosition.builder().target(latLng).zoom(zoom).tilt(tilt).bearing(bearing).build(); - - mGoogleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); - } - - public void setZoomLevel(int level) { - if (mGoogleMap != null) { - mGoogleMap.animateCamera(CameraUpdateFactory.zoomTo(level)); - } - } - - public void setIndoorEnabled(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.setIndoorEnabled(isOn); - } - } - - public void setTrafficEnabled(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.setTrafficEnabled(isOn); - } - } - - public void setCompassEnabled(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.getUiSettings().setCompassEnabled(isOn); - } - } - - public void setRotateGesturesEnabled(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.getUiSettings().setRotateGesturesEnabled(isOn); - } - } - - public void setScrollGesturesEnabled(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.getUiSettings().setScrollGesturesEnabled(isOn); - } + @Override + public void onMapReady() { + emitEvent("onMapReady", null); } - public void setScrollGesturesEnabledDuringRotateOrZoom(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.getUiSettings().setScrollGesturesEnabledDuringRotateOrZoom(isOn); - } + @Override + public void onRecenterButtonClick() { + emitEvent("onRecenterButtonClick", null); } - public void setTiltGesturesEnabled(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.getUiSettings().setTiltGesturesEnabled(isOn); - } + @Override + public void onMarkerClick(Marker marker) { + emitEvent("onMapReady", ObjectTranslationUtil.getMapFromMarker(marker)); } - public void setZoomControlsEnabled(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.getUiSettings().setZoomControlsEnabled(isOn); - } + @Override + public void onPolylineClick(Polyline polyline) { + emitEvent("onPolylineClick", ObjectTranslationUtil.getMapFromPolyline(polyline)); } - public void setZoomGesturesEnabled(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.getUiSettings().setZoomGesturesEnabled(isOn); - } + @Override + public void onPolygonClick(Polygon polygon) { + emitEvent("onPolygonClick", ObjectTranslationUtil.getMapFromPolygon(polygon)); } - public void setBuildingsEnabled(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.setBuildingsEnabled(isOn); - } + @Override + public void onCircleClick(Circle circle) { + emitEvent("onCircleClick", ObjectTranslationUtil.getMapFromCircle(circle)); } - public void setMyLocationEnabled(boolean isOn) { - if (mGoogleMap != null) { - if (ActivityCompat.checkSelfPermission(getActivity(), permission.ACCESS_FINE_LOCATION) - == PackageManager.PERMISSION_GRANTED - && ActivityCompat.checkSelfPermission(getActivity(), permission.ACCESS_COARSE_LOCATION) - == PackageManager.PERMISSION_GRANTED) { - mGoogleMap.setMyLocationEnabled(isOn); - } - } + @Override + public void onGroundOverlayClick(GroundOverlay groundOverlay) { + emitEvent("onGroundOverlayClick", ObjectTranslationUtil.getMapFromGroundOverlay(groundOverlay)); } - public void setMapToolbarEnabled(boolean isOn) { - if (mGoogleMap != null) { - mGoogleMap.getUiSettings().setMapToolbarEnabled(isOn); - } + @Override + public void onMarkerInfoWindowTapped(Marker marker) { + emitEvent("onInfoWindowClick", ObjectTranslationUtil.getMapFromMarker(marker)); } - /** Toggles whether the location marker is enabled. */ - public void setMyLocationButtonEnabled(boolean isOn) { - if (mGoogleMap == null) { - return; - } - - UiThreadUtil.runOnUiThread( - () -> { - mGoogleMap.getUiSettings().setMyLocationButtonEnabled(isOn); - }); + @Override + public void onMapClick(LatLng latLng) { + emitEvent("onMapClick", ObjectTranslationUtil.getMapFromLatLng(latLng)); } @Override diff --git a/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java b/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java index 25d7864..cdd7f03 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java @@ -188,7 +188,7 @@ public void receiveCommand( } break; case MOVE_CAMERA: - getFragmentForRoot(root).moveCamera(args.getMap(0).toHashMap()); + getFragmentForRoot(root).getMapController().moveCamera(args.getMap(0).toHashMap()); break; case SET_TRIP_PROGRESS_BAR_ENABLED: getNavFragmentForRoot(root).setTripProgressBarEnabled(args.getBoolean(0)); @@ -197,10 +197,10 @@ public void receiveCommand( getNavFragmentForRoot(root).setNavigationUiEnabled(args.getBoolean(0)); break; case SET_FOLLOWING_PERSPECTIVE: - getNavFragmentForRoot(root).setFollowingPerspective(args.getInt(0)); + getNavFragmentForRoot(root).getMapController().setFollowingPerspective(args.getInt(0)); break; case SET_NIGHT_MODE: - getFragmentForRoot(root).setNightModeOption(args.getInt(0)); + getNavFragmentForRoot(root).setNightModeOption(args.getInt(0)); break; case SET_SPEEDOMETER_ENABLED: getNavFragmentForRoot(root).setSpeedometerEnabled(args.getBoolean(0)); @@ -210,61 +210,63 @@ public void receiveCommand( break; case SET_ZOOM_LEVEL: int level = args.getInt(0); - getFragmentForRoot(root).setZoomLevel(level); + getFragmentForRoot(root).getMapController().setZoomLevel(level); break; case SET_INDOOR_ENABLED: - getFragmentForRoot(root).setIndoorEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setIndoorEnabled(args.getBoolean(0)); break; case SET_TRAFFIC_ENABLED: - getFragmentForRoot(root).setTrafficEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setTrafficEnabled(args.getBoolean(0)); break; case SET_COMPASS_ENABLED: - getFragmentForRoot(root).setCompassEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setCompassEnabled(args.getBoolean(0)); break; case SET_MY_LOCATION_BUTTON_ENABLED: - getFragmentForRoot(root).setMyLocationButtonEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setCompassEnabled(args.getBoolean(0)); break; case SET_MY_LOCATION_ENABLED: - getFragmentForRoot(root).setMyLocationEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setMyLocationEnabled(args.getBoolean(0)); break; case SET_ROTATE_GESTURES_ENABLED: - getFragmentForRoot(root).setRotateGesturesEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setRotateGesturesEnabled(args.getBoolean(0)); break; case SET_SCROLL_GESTURES_ENABLED: - getFragmentForRoot(root).setScrollGesturesEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setScrollGesturesEnabled(args.getBoolean(0)); break; case SET_SCROLL_GESTURES_ENABLED_DURING_ROTATE_OR_ZOOM: - getFragmentForRoot(root).setScrollGesturesEnabledDuringRotateOrZoom(args.getBoolean(0)); + getFragmentForRoot(root) + .getMapController() + .setScrollGesturesEnabledDuringRotateOrZoom(args.getBoolean(0)); break; case SET_TILT_GESTURES_ENABLED: - getFragmentForRoot(root).setTiltGesturesEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setTiltGesturesEnabled(args.getBoolean(0)); break; case SET_ZOOM_CONTROLS_ENABLED: - getFragmentForRoot(root).setZoomControlsEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setZoomControlsEnabled(args.getBoolean(0)); break; case SET_ZOOM_GESTURES_ENABLED: - getFragmentForRoot(root).setZoomGesturesEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setZoomGesturesEnabled(args.getBoolean(0)); break; case SET_BUILDINGS_ENABLED: - getFragmentForRoot(root).setBuildingsEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setBuildingsEnabled(args.getBoolean(0)); break; case SET_MAP_TYPE: - getFragmentForRoot(root).setMapType(args.getInt(0)); + getFragmentForRoot(root).getMapController().setMapType(args.getInt(0)); break; case SET_MAP_TOOLBAR_ENABLED: - getFragmentForRoot(root).setMapToolbarEnabled(args.getBoolean(0)); + getFragmentForRoot(root).getMapController().setMapToolbarEnabled(args.getBoolean(0)); break; case CLEAR_MAP_VIEW: - getFragmentForRoot(root).clearMapView(); + getFragmentForRoot(root).getMapController().clearMapView(); break; case RESET_MIN_MAX_ZOOM_LEVEL: - getFragmentForRoot(root).resetMinMaxZoomLevel(); + getFragmentForRoot(root).getMapController().resetMinMaxZoomLevel(); break; case SET_MAP_STYLE: getFragmentForRoot(root).setMapStyle(args.getString(0)); break; case ANIMATE_CAMERA: - getFragmentForRoot(root).animateCamera(args.getMap(0).toHashMap()); + getFragmentForRoot(root).getMapController().animateCamera(args.getMap(0).toHashMap()); break; case SET_TRAFFIC_INCIDENT_CARDS_ENABLED: getNavFragmentForRoot(root).setTrafficIncidentCardsEnabled(args.getBoolean(0)); @@ -282,19 +284,19 @@ public void receiveCommand( getNavFragmentForRoot(root).showRouteOverview(); break; case REMOVE_MARKER: - getFragmentForRoot(root).removeMarker(args.getString(0)); + getFragmentForRoot(root).getMapController().removeMarker(args.getString(0)); break; case REMOVE_POLYLINE: - getFragmentForRoot(root).removePolyline(args.getString(0)); + getFragmentForRoot(root).getMapController().removePolyline(args.getString(0)); break; case REMOVE_POLYGON: - getFragmentForRoot(root).removePolygon(args.getString(0)); + getFragmentForRoot(root).getMapController().removePolygon(args.getString(0)); break; case REMOVE_CIRCLE: - getFragmentForRoot(root).removeCircle(args.getString(0)); + getFragmentForRoot(root).getMapController().removeCircle(args.getString(0)); break; case REMOVE_GROUND_OVERLAY: - getFragmentForRoot(root).removeGroundOverlay(args.getString(0)); + getFragmentForRoot(root).getMapController().removeGroundOverlay(args.getString(0)); break; } } @@ -343,7 +345,7 @@ public void createFragment( fragment = mapFragment; if (stylingOptions != null) { - mapFragment.setStylingOptions(stylingOptions); + mapFragment.setStylingOptions(new StylingOptionsBuilder.Builder(stylingOptions).build()); } } else { NavViewFragment navFragment = new NavViewFragment(reactContext, root.getId()); @@ -351,7 +353,7 @@ public void createFragment( fragment = navFragment; if (stylingOptions != null) { - navFragment.setStylingOptions(stylingOptions); + navFragment.setStylingOptions(new StylingOptionsBuilder.Builder(stylingOptions).build()); } } activity diff --git a/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java b/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java index e58d953..428599a 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java @@ -164,6 +164,7 @@ public void addMarker(int viewId, ReadableMap markerOptionsMap, final Promise pr Marker marker = mNavViewManager .getFragmentForViewId(viewId) + .getMapController() .addMarker(markerOptionsMap.toHashMap()); promise.resolve(ObjectTranslationUtil.getMapFromMarker(marker)); @@ -182,6 +183,7 @@ public void addPolyline(int viewId, ReadableMap polylineOptionsMap, final Promis Polyline polyline = mNavViewManager .getFragmentForViewId(viewId) + .getMapController() .addPolyline(polylineOptionsMap.toHashMap()); promise.resolve(ObjectTranslationUtil.getMapFromPolyline(polyline)); @@ -199,6 +201,7 @@ public void addPolygon(int viewId, ReadableMap polygonOptionsMap, final Promise Polygon polygon = mNavViewManager .getFragmentForViewId(viewId) + .getMapController() .addPolygon(polygonOptionsMap.toHashMap()); promise.resolve(ObjectTranslationUtil.getMapFromPolygon(polygon)); @@ -214,7 +217,10 @@ public void addCircle(int viewId, ReadableMap circleOptionsMap, final Promise pr return; } Circle circle = - mNavViewManager.getFragmentForViewId(viewId).addCircle(circleOptionsMap.toHashMap()); + mNavViewManager + .getFragmentForViewId(viewId) + .getMapController() + .addCircle(circleOptionsMap.toHashMap()); promise.resolve(ObjectTranslationUtil.getMapFromCircle(circle)); }); @@ -231,6 +237,7 @@ public void addGroundOverlay(int viewId, ReadableMap overlayOptionsMap, final Pr GroundOverlay overlay = mNavViewManager .getFragmentForViewId(viewId) + .getMapController() .addGroundOverlay(overlayOptionsMap.toHashMap()); promise.resolve(ObjectTranslationUtil.getMapFromGroundOverlay(overlay)); diff --git a/android/src/main/java/com/google/android/react/navsdk/Package.java b/android/src/main/java/com/google/android/react/navsdk/Package.java index dbb40b0..e7a9a2b 100644 --- a/android/src/main/java/com/google/android/react/navsdk/Package.java +++ b/android/src/main/java/com/google/android/react/navsdk/Package.java @@ -35,6 +35,7 @@ public List createNativeModules(ReactApplicationContext reactConte List modules = new ArrayList<>(); NavViewManager viewManager = NavViewManager.getInstance(reactContext); modules.add(new NavModule(reactContext, viewManager)); + modules.add(new NavAutoModule(reactContext)); modules.add(new NavViewModule(reactContext, viewManager)); return modules; diff --git a/example/README.md b/example/README.md index 338d601..50c27d0 100644 --- a/example/README.md +++ b/example/README.md @@ -6,26 +6,32 @@ This contains a sample application to showcase the functionality of the NavSDK l ## Setup -1. First, make sure you go through the setup from the main [README](../README.md). +First, make sure you go through the setup from the main [README](../README.md). -2. Open the example/android folder in Android Studio and add your api key in local.properties by adding a line like this: +### Android + +1. Open the example/android folder in Android Studio and add your api key in local.properties by adding a line like this: * ```MAPS_API_KEY=YOUR_API_KEY``` - make sure that this key is pointing to a Google Cloud project which had Nav SDK enabled. * To enable Nav SDK in your project follow these guides: * **Android**: https://developers.google.com/maps/documentation/navigation/android-sdk/set-up-project * **iOS**: https://developers.google.com/maps/documentation/navigation/ios-sdk/config -3. Using your preferred terminal, go to example/ios folder and run the command below. +### iOS + +1. Using your preferred terminal, go to example/ios folder and run the command below. - `pod install` + `pod install` or `yarn prepare` -4. Copy the `Keys.plist.sample` file located in `example/ios/SampleApp/` to a new file named `Keys.plist`. This file is git ignored and won't be accidentally committed. In your Google cloud console, add the Google API key to the project and add this newly created API key to the `Keys.plist` file. +2. Copy the `Keys.plist.sample` file located in `example/ios/SampleApp/` to a new file named `Keys.plist`. This file is git ignored and won't be accidentally committed. In your Google cloud console, add the Google API key to the project and add this newly created API key to the `Keys.plist` file. ```xml API_KEY Your API KEY ``` -5. To run the sample app, navigate to the `example` folder in the root directory and use the following commands for your platform in the terminal. +## Running the app + +1. To run the sample app, navigate to the `example` folder in the root directory and use the following commands for your platform in the terminal. 1. Ensure all workspace dependencies are installed: `yarn install` @@ -37,8 +43,7 @@ This contains a sample application to showcase the functionality of the NavSDK l `npx react-native run-ios` -6. After the app initializes, accept the terms of services. You should see a map loaded in background if you have used the right API key. - +2. After the app initializes, accept the terms of services. You should see a map loaded in background if you have used the right API key. ### Android 1. On your Emulator, go to App Info for the installed app, then Permissions > Location and allow location for the app. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 11f2abb..e999c47 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,11 +1,11 @@ // Copyright 2023 Google LLC -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -127,6 +127,11 @@ dependencies { } else { implementation jscFlavor } + + // For Android Auto Sample support + implementation "androidx.car.app:app:1.4.0" + implementation "androidx.car.app:app-projected:1.4.0" + implementation 'com.google.android.libraries.navigation:navigation:5.3.1' } secrets { @@ -137,12 +142,12 @@ secrets { // https://developers.google.com/maps/documentation/android-sdk/secrets-gradle-plugin propertiesFileName = "local.properties" - // For CI/CD, you can have a file with default keys that can be + // For CI/CD, you can have a file with default keys that can be // safely checked in to your source code version control. // defaultPropertiesFileName = 'local.defaults.properties' // Ignore all keys matching the regexp "sdk.*" - ignoreList.add("sdk.*") + ignoreList.add("sdk.*") } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 375107a..5db65dd 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -16,30 +16,62 @@ - + + + + - + + + + + + + + + + + + + + - - - - - - - - - - + android:icon="@mipmap/ic_launcher"> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/example/android/app/src/main/java/com/sampleapp/ManeuverConverter.java b/example/android/app/src/main/java/com/sampleapp/ManeuverConverter.java new file mode 100644 index 0000000..b99846b --- /dev/null +++ b/example/android/app/src/main/java/com/sampleapp/ManeuverConverter.java @@ -0,0 +1,265 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.sampleapp; + +import com.google.android.libraries.mapsplatform.turnbyturn.model.Maneuver; +import com.google.common.collect.ImmutableMap; +import javax.annotation.Nullable; + +/** Converter that converts between turn-by-turn and Android Auto Maneuvers. */ +public final class ManeuverConverter { + private ManeuverConverter() {} + + // Map from turn-by-turn Maneuver to Android Auto Maneuver.Type. + private static final ImmutableMap MANEUVER_TO_ANDROID_AUTO_MANEUVER_TYPE = + ImmutableMap.builder() + .put(Maneuver.DEPART, androidx.car.app.navigation.model.Maneuver.TYPE_DEPART) + .put(Maneuver.DESTINATION, androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION) + .put( + Maneuver.DESTINATION_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT) + .put( + Maneuver.DESTINATION_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_RIGHT) + .put(Maneuver.STRAIGHT, androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT) + .put(Maneuver.TURN_LEFT, androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT) + .put( + Maneuver.TURN_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT) + .put(Maneuver.TURN_KEEP_LEFT, androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_LEFT) + .put(Maneuver.TURN_KEEP_RIGHT, androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_RIGHT) + .put( + Maneuver.TURN_SLIGHT_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT) + .put( + Maneuver.TURN_SLIGHT_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT) + .put( + Maneuver.TURN_SHARP_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_LEFT) + .put( + Maneuver.TURN_SHARP_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_SHARP_RIGHT) + .put( + Maneuver.TURN_U_TURN_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver.TYPE_U_TURN_RIGHT) + .put( + Maneuver.TURN_U_TURN_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver.TYPE_U_TURN_LEFT) + .put( + Maneuver.MERGE_UNSPECIFIED, + androidx.car.app.navigation.model.Maneuver.TYPE_MERGE_SIDE_UNSPECIFIED) + .put(Maneuver.MERGE_LEFT, androidx.car.app.navigation.model.Maneuver.TYPE_MERGE_LEFT) + .put(Maneuver.MERGE_RIGHT, androidx.car.app.navigation.model.Maneuver.TYPE_MERGE_RIGHT) + .put(Maneuver.FORK_LEFT, androidx.car.app.navigation.model.Maneuver.TYPE_FORK_LEFT) + .put(Maneuver.FORK_RIGHT, androidx.car.app.navigation.model.Maneuver.TYPE_FORK_RIGHT) + .put( + Maneuver.ON_RAMP_UNSPECIFIED, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_NORMAL_RIGHT) + .put( + Maneuver.ON_RAMP_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_NORMAL_LEFT) + .put( + Maneuver.ON_RAMP_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_NORMAL_RIGHT) + .put( + Maneuver.ON_RAMP_KEEP_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_NORMAL_LEFT) + .put( + Maneuver.ON_RAMP_KEEP_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_NORMAL_RIGHT) + .put( + Maneuver.ON_RAMP_SLIGHT_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_SLIGHT_LEFT) + .put( + Maneuver.ON_RAMP_SLIGHT_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_SLIGHT_RIGHT) + .put( + Maneuver.ON_RAMP_SHARP_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_SHARP_LEFT) + .put( + Maneuver.ON_RAMP_SHARP_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_SHARP_RIGHT) + .put( + Maneuver.ON_RAMP_U_TURN_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_U_TURN_RIGHT) + .put( + Maneuver.ON_RAMP_U_TURN_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver.TYPE_ON_RAMP_U_TURN_LEFT) + .put( + Maneuver.OFF_RAMP_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_OFF_RAMP_NORMAL_LEFT) + .put( + Maneuver.OFF_RAMP_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_OFF_RAMP_NORMAL_RIGHT) + .put( + Maneuver.OFF_RAMP_KEEP_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT) + .put( + Maneuver.OFF_RAMP_KEEP_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT) + .put( + Maneuver.OFF_RAMP_SLIGHT_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT) + .put( + Maneuver.OFF_RAMP_SLIGHT_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT) + .put( + Maneuver.OFF_RAMP_SHARP_LEFT, + androidx.car.app.navigation.model.Maneuver.TYPE_OFF_RAMP_NORMAL_LEFT) + .put( + Maneuver.OFF_RAMP_SHARP_RIGHT, + androidx.car.app.navigation.model.Maneuver.TYPE_OFF_RAMP_NORMAL_RIGHT) + .put( + Maneuver.ROUNDABOUT_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW) + .put( + Maneuver.ROUNDABOUT_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW) + .put( + Maneuver.ROUNDABOUT_STRAIGHT_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CW) + .put( + Maneuver.ROUNDABOUT_STRAIGHT_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CCW) + .put( + Maneuver.ROUNDABOUT_LEFT_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_LEFT_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_RIGHT_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_RIGHT_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_SLIGHT_LEFT_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_SLIGHT_LEFT_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_SLIGHT_RIGHT_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_SLIGHT_RIGHT_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_SHARP_LEFT_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_SHARP_LEFT_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_SHARP_RIGHT_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_SHARP_RIGHT_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_U_TURN_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_U_TURN_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver + .TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE) + .put( + Maneuver.ROUNDABOUT_EXIT_CLOCKWISE, + androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_EXIT_CW) + .put( + Maneuver.ROUNDABOUT_EXIT_COUNTERCLOCKWISE, + androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_EXIT_CCW) + .put(Maneuver.FERRY_BOAT, androidx.car.app.navigation.model.Maneuver.TYPE_FERRY_BOAT) + .put(Maneuver.FERRY_TRAIN, androidx.car.app.navigation.model.Maneuver.TYPE_FERRY_TRAIN) + .put(Maneuver.NAME_CHANGE, androidx.car.app.navigation.model.Maneuver.TYPE_NAME_CHANGE) + .buildOrThrow(); + + /** Represents the roundabout turn angle for a slight turn in either right or left directions. */ + private static final int ROUNDABOUT_ANGLE_SLIGHT = 10; + + /** Represents the roundabout turn angle for a normal turn in either right or left directions. */ + private static final int ROUNDABOUT_ANGLE_NORMAL = 45; + + /** Represents the roundabout turn angle for a sharp turn in either right or left directions. */ + private static final int ROUNDABOUT_ANGLE_SHARP = 135; + + /** Represents the roundabout turn angle for a u-turn in either right or left directions. */ + private static final int ROUNDABOUT_ANGLE_U_TURN = 180; + + /** + * Returns the corresponding {@link androidx.car.app.navigation.model.Maneuver.Type} for the given + * direction {@link Maneuver} + * + * @throws {@link IllegalArgumentException} if the given maneuver does not have a corresponding + * Android Auto Maneuver type. + */ + public static int getAndroidAutoManeuverType(@Maneuver int maneuver) { + if (MANEUVER_TO_ANDROID_AUTO_MANEUVER_TYPE.containsKey(maneuver)) { + return MANEUVER_TO_ANDROID_AUTO_MANEUVER_TYPE.get(maneuver); + } + throw new IllegalArgumentException( + String.format( + "Given turn-by-turn Maneuver %d cannot be converted to an Android Auto equivalent.", + maneuver)); + } + + /** + * Returns the corresponding Android Auto roundabout angle for the given turn {@link Maneuver}. + * Returns {@code null} if given maneuver does not involve a roundabout with a turn. + */ + @Nullable + public static Integer getAndroidAutoRoundaboutAngle(@Maneuver int maneuver) { + if (maneuver == Maneuver.ROUNDABOUT_LEFT_CLOCKWISE + || maneuver == Maneuver.ROUNDABOUT_RIGHT_CLOCKWISE + || maneuver == Maneuver.ROUNDABOUT_LEFT_COUNTERCLOCKWISE + || maneuver == Maneuver.ROUNDABOUT_RIGHT_COUNTERCLOCKWISE) { + return ROUNDABOUT_ANGLE_NORMAL; + } + if (maneuver == Maneuver.ROUNDABOUT_SHARP_LEFT_CLOCKWISE + || maneuver == Maneuver.ROUNDABOUT_SHARP_RIGHT_CLOCKWISE + || maneuver == Maneuver.ROUNDABOUT_SHARP_LEFT_COUNTERCLOCKWISE + || maneuver == Maneuver.ROUNDABOUT_SHARP_RIGHT_COUNTERCLOCKWISE) { + return ROUNDABOUT_ANGLE_SHARP; + } + if (maneuver == Maneuver.ROUNDABOUT_SLIGHT_LEFT_CLOCKWISE + || maneuver == Maneuver.ROUNDABOUT_SLIGHT_RIGHT_CLOCKWISE + || maneuver == Maneuver.ROUNDABOUT_SLIGHT_LEFT_COUNTERCLOCKWISE + || maneuver == Maneuver.ROUNDABOUT_SLIGHT_RIGHT_COUNTERCLOCKWISE) { + return ROUNDABOUT_ANGLE_SLIGHT; + } + if (maneuver == Maneuver.ROUNDABOUT_U_TURN_CLOCKWISE + || maneuver == Maneuver.ROUNDABOUT_U_TURN_COUNTERCLOCKWISE) { + return ROUNDABOUT_ANGLE_U_TURN; + } + return null; + } +} diff --git a/example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoScreen.java b/example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoScreen.java new file mode 100644 index 0000000..e71d6c6 --- /dev/null +++ b/example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoScreen.java @@ -0,0 +1,150 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.sampleapp; + +import static java.lang.Double.max; + +import android.annotation.SuppressLint; +import androidx.annotation.NonNull; +import androidx.car.app.CarContext; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.Distance; +import androidx.car.app.model.Pane; +import androidx.car.app.model.PaneTemplate; +import androidx.car.app.model.Row; +import androidx.car.app.model.Template; +import androidx.car.app.navigation.model.Maneuver; +import androidx.car.app.navigation.model.NavigationTemplate; +import androidx.car.app.navigation.model.RoutingInfo; +import androidx.car.app.navigation.model.Step; +import androidx.core.graphics.drawable.IconCompat; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.libraries.mapsplatform.turnbyturn.model.NavInfo; +import com.google.android.libraries.mapsplatform.turnbyturn.model.StepInfo; +import com.google.android.react.navsdk.AndroidAutoBaseScreen; +import com.google.android.react.navsdk.NavInfoReceivingService; + +public class SampleAndroidAutoScreen extends AndroidAutoBaseScreen { + protected RoutingInfo mNavInfo; + + public SampleAndroidAutoScreen(@NonNull CarContext carContext) { + super(carContext); + + // Connect to the Turn-by-Turn Navigation service to receive navigation data. + NavInfoReceivingService.getNavInfoLiveData().observe(this, this::buildNavInfo); + } + + private void buildNavInfo(NavInfo navInfo) { + if (navInfo == null || navInfo.getCurrentStep() == null) { + return; + } + + /** + * Converts data received from the Navigation data feed into Android-Auto compatible data + * structures. + */ + Step currentStep = buildStepFromStepInfo(navInfo.getCurrentStep()); + Distance distanceToStep = + Distance.create(max(navInfo.getDistanceToCurrentStepMeters(), 0), Distance.UNIT_METERS); + + mNavInfo = new RoutingInfo.Builder().setCurrentStep(currentStep, distanceToStep).build(); + + // Invalidate the current template which leads to another onGetTemplate call. + invalidate(); + } + + @Override + public void onNavigationReady(boolean ready) { + super.onNavigationReady(ready); + // Invalidate template layout because of conditional rendering in the + // onGetTemplate method. + invalidate(); + } + + private Step buildStepFromStepInfo(StepInfo stepInfo) { + int maneuver = ManeuverConverter.getAndroidAutoManeuverType(stepInfo.getManeuver()); + Maneuver.Builder maneuverBuilder = new Maneuver.Builder(maneuver); + IconCompat maneuverIcon = IconCompat.createWithBitmap(stepInfo.getManeuverBitmap()); + CarIcon maneuverCarIcon = new CarIcon.Builder(maneuverIcon).build(); + maneuverBuilder.setIcon(maneuverCarIcon); + Step.Builder stepBuilder = + new Step.Builder() + .setRoad(stepInfo.getFullRoadName()) + .setCue(stepInfo.getFullInstructionText()) + .setManeuver(maneuverBuilder.build()); + return stepBuilder.build(); + } + + @NonNull + @Override + public Template onGetTemplate() { + if (!mNavigationInitialized) { + return new PaneTemplate.Builder( + new Pane.Builder() + .addRow( + new Row.Builder() + .setTitle("Nav SampleApp") + .addText( + "Initialize navigation to see navigation view on the Android Auto" + + " screen") + .build()) + .build()) + .build(); + } + + // Suppresses the missing permission check for the followMyLocation method, which requires + // "android.permission.ACCESS_COARSE_LOCATION" or "android.permission.ACCESS_FINE_LOCATION", as + // these permissions are already handled elsewhere. + @SuppressLint("MissingPermission") + NavigationTemplate.Builder navigationTemplateBuilder = + new NavigationTemplate.Builder() + .setActionStrip( + new ActionStrip.Builder() + .addAction( + new Action.Builder() + .setTitle("Re-center") + .setOnClickListener( + () -> { + if (mGoogleMap == null) return; + mGoogleMap.followMyLocation(GoogleMap.CameraPerspective.TILTED); + }) + .build()) + .addAction( + new Action.Builder() + .setTitle("Custom event") + .setOnClickListener( + () -> { + WritableMap map = Arguments.createMap(); + map.putString("sampleKey", "sampleValue"); + sendCustomEvent("sampleEvent", map); + }) + .build()) + .build()) + .setMapActionStrip(new ActionStrip.Builder().addAction(Action.PAN).build()); + + // Show turn-by-turn navigation information if available. + if (mNavInfo != null) { + navigationTemplateBuilder.setNavigationInfo(mNavInfo); + } + + return navigationTemplateBuilder.build(); + } +} diff --git a/example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoService.java b/example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoService.java new file mode 100644 index 0000000..f88a74f --- /dev/null +++ b/example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoService.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.sampleapp; + +import androidx.annotation.NonNull; +import androidx.car.app.CarAppService; +import androidx.car.app.Session; +import androidx.car.app.SessionInfo; +import androidx.car.app.validation.HostValidator; + +public final class SampleAndroidAutoService extends CarAppService { + @NonNull + @Override + public HostValidator createHostValidator() { + // This sample allows all hosts to connect to the app. + return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR; + } + + @Override + @NonNull + public Session onCreateSession(@NonNull SessionInfo sessionInfo) { + return new SampleAndroidAutoSession(sessionInfo); + } +} diff --git a/example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoSession.java b/example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoSession.java new file mode 100644 index 0000000..81c664b --- /dev/null +++ b/example/android/app/src/main/java/com/sampleapp/SampleAndroidAutoSession.java @@ -0,0 +1,114 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.sampleapp; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.car.app.CarContext; +import androidx.car.app.CarToast; +import androidx.car.app.Screen; +import androidx.car.app.Session; +import androidx.car.app.SessionInfo; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; + +public class SampleAndroidAutoSession extends Session { + static final String TAG = SampleAndroidAutoSession.class.getSimpleName(); + + public SampleAndroidAutoSession(SessionInfo sessionInfo) { + if (sessionInfo.getDisplayType() == SessionInfo.DISPLAY_TYPE_MAIN) { + Lifecycle lifecycle = getLifecycle(); + lifecycle.addObserver(mLifeCycleObserver); + } + } + + private final LifecycleObserver mLifeCycleObserver = + new DefaultLifecycleObserver() { + + @Override + public void onCreate(@NonNull LifecycleOwner lifecycleOwner) { + Log.i(TAG, "In onCreate()"); + } + + @Override + public void onStart(@NonNull LifecycleOwner lifecycleOwner) { + Log.i(TAG, "In onStart()"); + getCarContext() + .bindService( + new Intent(getCarContext(), SampleAndroidAutoService.class), + mServiceConnection, + Context.BIND_AUTO_CREATE); + } + + @Override + public void onResume(@NonNull LifecycleOwner lifecycleOwner) { + Log.i(TAG, "In onResume()"); + } + + @Override + public void onPause(@NonNull LifecycleOwner lifecycleOwner) { + Log.i(TAG, "In onPause()"); + } + + @Override + public void onStop(@NonNull LifecycleOwner lifecycleOwner) { + Log.i(TAG, "In onStop()"); + getCarContext().unbindService(mServiceConnection); + } + + @Override + public void onDestroy(@NonNull LifecycleOwner lifecycleOwner) { + Log.i(TAG, "In onDestroy()"); + } + }; + + // Monitors the state of the connection to the Navigation service. + final ServiceConnection mServiceConnection = + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.i(TAG, "In onServiceConnected() component:" + name); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.i(TAG, "In onServiceDisconnected() component:" + name); + } + }; + + @Override + @NonNull + public Screen onCreateScreen(@NonNull Intent intent) { + Log.i(TAG, "In onCreateScreen()"); + + String action = intent.getAction(); + if (action != null && CarContext.ACTION_NAVIGATE.equals(action)) { + CarToast.makeText( + getCarContext(), "Navigation intent: " + intent.getDataString(), CarToast.LENGTH_LONG) + .show(); + } + + return new SampleAndroidAutoScreen(getCarContext()); + } +} diff --git a/example/android/app/src/main/res/xml/automotive_app_desc.xml b/example/android/app/src/main/res/xml/automotive_app_desc.xml new file mode 100644 index 0000000..cc83882 --- /dev/null +++ b/example/android/app/src/main/res/xml/automotive_app_desc.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/example/android/build.gradle b/example/android/build.gradle index 9484dbf..bf9dc9e 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -25,7 +25,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle") + classpath("com.android.tools.build:gradle:7.2.1") classpath("com.facebook.react:react-native-gradle-plugin") classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } diff --git a/example/ios/Podfile b/example/ios/Podfile index 686ca89..76a93f6 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -26,26 +26,33 @@ if linkage != nil use_frameworks! :linkage => linkage.to_sym end +config = use_native_modules! + target 'SampleApp' do - config = use_native_modules! + use_react_native!( + :path => config[:reactNativePath], + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/.." + ) +end +target 'SampleAppCarPlay' do use_react_native!( :path => config[:reactNativePath], # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) +end - target 'SampleAppTests' do - inherit! :complete - # Pods for testing - end +target 'SampleAppTests' do + inherit! :complete + # Pods for testing, shared between both SampleApp and SampleAppCarPlay +end - post_install do |installer| - # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 +post_install do |installer| react_native_post_install( installer, config[:reactNativePath], :mac_catalyst_enabled => false ) - end -end +end \ No newline at end of file diff --git a/example/ios/SampleApp.xcodeproj/project.pbxproj b/example/ios/SampleApp.xcodeproj/project.pbxproj index eaeecff..7fadfe9 100644 --- a/example/ios/SampleApp.xcodeproj/project.pbxproj +++ b/example/ios/SampleApp.xcodeproj/project.pbxproj @@ -8,14 +8,25 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* SampleAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* SampleAppTests.m */; }; - 0C80B921A6F3F58F76C31292 /* libPods-SampleApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-SampleApp.a */; }; - 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; + 062AC10D618662A05665211C /* libPods-SampleAppCarPlay.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F7A476D4E03B73E48762E4D4 /* libPods-SampleAppCarPlay.a */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 29F89EA4FF546EABE968E1D2 /* libPods-SampleApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 354226CC8A58229ECC025410 /* libPods-SampleApp.a */; }; + 2A20E8122C8994DB00DB7ADA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 2A20E8142C8994DB00DB7ADA /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + 2A20E8282C899A1400DB7ADA /* Info-CarPlay.plist in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB61A68108700A75B9A /* Info-CarPlay.plist */; }; + 2AB8C27A2C89A07000250560 /* Keys.plist in Resources */ = {isa = PBXBuildFile; fileRef = 52D4271C2C81D3F300C7FB36 /* Keys.plist */; }; + 2AB8C27E2C89A0B400250560 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + 2AB8C27F2C89A0C800250560 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 5205C1232C8B314F00D0FC6B /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5205C1212C8B314F00D0FC6B /* AppDelegate.m */; }; + 52206E7F2C8B2F9E00B34D22 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 52206E7E2C8B2F9E00B34D22 /* PrivacyInfo.xcprivacy */; }; + 52206E802C8B2F9E00B34D22 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 52206E7E2C8B2F9E00B34D22 /* PrivacyInfo.xcprivacy */; }; + 52206E852C8B2FEC00B34D22 /* CarSceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 52206E812C8B2FEC00B34D22 /* CarSceneDelegate.m */; }; + 52206E862C8B2FEC00B34D22 /* PhoneSceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 52206E822C8B2FEC00B34D22 /* PhoneSceneDelegate.m */; }; + 52206E892C8B303700B34D22 /* AppDelegateCarPlay.m in Sources */ = {isa = PBXBuildFile; fileRef = 52206E882C8B303700B34D22 /* AppDelegateCarPlay.m */; }; 52D4271D2C81D3F300C7FB36 /* Keys.plist in Resources */ = {isa = PBXBuildFile; fileRef = 52D4271C2C81D3F300C7FB36 /* Keys.plist */; }; - 7699B88040F8A987B510C191 /* libPods-SampleApp-SampleAppTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-SampleApp-SampleAppTests.a */; }; + 77BA6ED186FC355B21D3A62F /* libPods-SampleAppTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 36A733F29B6732CA2CF27A17 /* libPods-SampleAppTests.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; - E72EC806AB287A94E8141AD5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E049A7A79160D0D74CDA8CD1 /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -32,23 +43,36 @@ 00E356EE1AD99517003FC87E /* SampleAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* SampleAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleAppTests.m; sourceTree = ""; }; + 011A22ED61B5A84DFF838AB2 /* Pods-SampleAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleAppTests.debug.xcconfig"; path = "Target Support Files/Pods-SampleAppTests/Pods-SampleAppTests.debug.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* SampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = SampleApp/AppDelegate.h; sourceTree = ""; }; - 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = SampleApp/AppDelegate.mm; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = SampleApp/Images.xcassets; sourceTree = ""; }; - 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = SampleApp/Info.plist; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info-CarPlay.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Info-CarPlay.plist"; path = "SampleApp/Info-CarPlay.plist"; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = SampleApp/main.m; sourceTree = ""; }; - 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = SampleApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 19F6CBCC0A4E27FBF8BF4A61 /* libPods-SampleApp-SampleAppTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SampleApp-SampleAppTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A20E8202C8994DB00DB7ADA /* SampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2AB8C27C2C89A09D00250560 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = SampleApp/Info.plist; sourceTree = ""; }; + 2AEB719D2C7C80F9002224A5 /* SampleApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = SampleApp.entitlements; path = SampleApp/SampleApp.entitlements; sourceTree = ""; }; + 354226CC8A58229ECC025410 /* libPods-SampleApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SampleApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 36A733F29B6732CA2CF27A17 /* libPods-SampleAppTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SampleAppTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B4392A12AC88292D35C810B /* Pods-SampleApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleApp.debug.xcconfig"; path = "Target Support Files/Pods-SampleApp/Pods-SampleApp.debug.xcconfig"; sourceTree = ""; }; + 4735EA9AB0E8831D741A3A3E /* Pods-SampleAppCarPlay.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleAppCarPlay.debug.xcconfig"; path = "Target Support Files/Pods-SampleAppCarPlay/Pods-SampleAppCarPlay.debug.xcconfig"; sourceTree = ""; }; + 5205C1212C8B314F00D0FC6B /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = SampleApp/AppDelegate.m; sourceTree = ""; }; + 5205C1222C8B314F00D0FC6B /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = SampleApp/AppDelegate.h; sourceTree = ""; }; + 52206E7E2C8B2F9E00B34D22 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = SampleApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 52206E812C8B2FEC00B34D22 /* CarSceneDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CarSceneDelegate.m; path = SampleApp/CarSceneDelegate.m; sourceTree = ""; }; + 52206E822C8B2FEC00B34D22 /* PhoneSceneDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PhoneSceneDelegate.m; path = SampleApp/PhoneSceneDelegate.m; sourceTree = ""; }; + 52206E832C8B2FEC00B34D22 /* CarSceneDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CarSceneDelegate.h; path = SampleApp/CarSceneDelegate.h; sourceTree = ""; }; + 52206E842C8B2FEC00B34D22 /* PhoneSceneDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PhoneSceneDelegate.h; path = SampleApp/PhoneSceneDelegate.h; sourceTree = ""; }; + 52206E872C8B303700B34D22 /* AppDelegateCarPlay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegateCarPlay.h; path = SampleApp/AppDelegateCarPlay.h; sourceTree = ""; }; + 52206E882C8B303700B34D22 /* AppDelegateCarPlay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegateCarPlay.m; path = SampleApp/AppDelegateCarPlay.m; sourceTree = ""; }; 52D4271C2C81D3F300C7FB36 /* Keys.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Keys.plist; path = SampleApp/Keys.plist; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-SampleApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleApp.release.xcconfig"; path = "Target Support Files/Pods-SampleApp/Pods-SampleApp.release.xcconfig"; sourceTree = ""; }; 5B7EB9410499542E8C5724F5 /* Pods-SampleApp-SampleAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleApp-SampleAppTests.debug.xcconfig"; path = "Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests.debug.xcconfig"; sourceTree = ""; }; - 5DCACB8F33CDC322A6C60F78 /* libPods-SampleApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SampleApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 750C10660A3F190F6072C5DA /* Pods-SampleAppCarPlay.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleAppCarPlay.release.xcconfig"; path = "Target Support Files/Pods-SampleAppCarPlay/Pods-SampleAppCarPlay.release.xcconfig"; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = SampleApp/LaunchScreen.storyboard; sourceTree = ""; }; 89C6BE57DB24E9ADA2F236DE /* Pods-SampleApp-SampleAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleApp-SampleAppTests.release.xcconfig"; path = "Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests.release.xcconfig"; sourceTree = ""; }; - E049A7A79160D0D74CDA8CD1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = SampleApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; + A9D547919643FC330CF84EC3 /* Pods-SampleAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleAppTests.release.xcconfig"; path = "Target Support Files/Pods-SampleAppTests/Pods-SampleAppTests.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + F7A476D4E03B73E48762E4D4 /* libPods-SampleAppCarPlay.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SampleAppCarPlay.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,7 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7699B88040F8A987B510C191 /* libPods-SampleApp-SampleAppTests.a in Frameworks */, + 77BA6ED186FC355B21D3A62F /* libPods-SampleAppTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -64,7 +88,16 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0C80B921A6F3F58F76C31292 /* libPods-SampleApp.a in Frameworks */, + 062AC10D618662A05665211C /* libPods-SampleAppCarPlay.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A20E8132C8994DB00DB7ADA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A20E8142C8994DB00DB7ADA /* BuildFile in Frameworks */, + 29F89EA4FF546EABE968E1D2 /* libPods-SampleApp.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -91,15 +124,22 @@ 13B07FAE1A68108700A75B9A /* SampleApp */ = { isa = PBXGroup; children = ( - 13B07FAF1A68108700A75B9A /* AppDelegate.h */, - 13B07FB01A68108700A75B9A /* AppDelegate.mm */, + 5205C1222C8B314F00D0FC6B /* AppDelegate.h */, + 5205C1212C8B314F00D0FC6B /* AppDelegate.m */, + 52206E872C8B303700B34D22 /* AppDelegateCarPlay.h */, + 52206E882C8B303700B34D22 /* AppDelegateCarPlay.m */, + 2AEB719D2C7C80F9002224A5 /* SampleApp.entitlements */, + 13B07FB71A68108700A75B9A /* main.m */, + 52206E832C8B2FEC00B34D22 /* CarSceneDelegate.h */, + 52206E812C8B2FEC00B34D22 /* CarSceneDelegate.m */, + 52206E842C8B2FEC00B34D22 /* PhoneSceneDelegate.h */, + 52206E822C8B2FEC00B34D22 /* PhoneSceneDelegate.m */, 13B07FB51A68108700A75B9A /* Images.xcassets */, - 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB61A68108700A75B9A /* Info-CarPlay.plist */, + 2AB8C27C2C89A09D00250560 /* Info.plist */, 52D4271C2C81D3F300C7FB36 /* Keys.plist */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, - 13B07FB71A68108700A75B9A /* main.m */, - 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, - E049A7A79160D0D74CDA8CD1 /* PrivacyInfo.xcprivacy */, + 52206E7E2C8B2F9E00B34D22 /* PrivacyInfo.xcprivacy */, ); name = SampleApp; sourceTree = ""; @@ -108,8 +148,9 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 5DCACB8F33CDC322A6C60F78 /* libPods-SampleApp.a */, - 19F6CBCC0A4E27FBF8BF4A61 /* libPods-SampleApp-SampleAppTests.a */, + F7A476D4E03B73E48762E4D4 /* libPods-SampleAppCarPlay.a */, + 36A733F29B6732CA2CF27A17 /* libPods-SampleAppTests.a */, + 354226CC8A58229ECC025410 /* libPods-SampleApp.a */, ); name = Frameworks; sourceTree = ""; @@ -141,6 +182,7 @@ children = ( 13B07F961A680F5B00A75B9A /* SampleApp.app */, 00E356EE1AD99517003FC87E /* SampleAppTests.xctest */, + 2A20E8202C8994DB00DB7ADA /* SampleApp.app */, ); name = Products; sourceTree = ""; @@ -152,6 +194,10 @@ 5709B34CF0A7D63546082F79 /* Pods-SampleApp.release.xcconfig */, 5B7EB9410499542E8C5724F5 /* Pods-SampleApp-SampleAppTests.debug.xcconfig */, 89C6BE57DB24E9ADA2F236DE /* Pods-SampleApp-SampleAppTests.release.xcconfig */, + 4735EA9AB0E8831D741A3A3E /* Pods-SampleAppCarPlay.debug.xcconfig */, + 750C10660A3F190F6072C5DA /* Pods-SampleAppCarPlay.release.xcconfig */, + 011A22ED61B5A84DFF838AB2 /* Pods-SampleAppTests.debug.xcconfig */, + A9D547919643FC330CF84EC3 /* Pods-SampleAppTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -167,8 +213,8 @@ 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, - C59DA0FBD6956966B86A3779 /* [CP] Embed Pods Frameworks */, - F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */, + 7CDED63BA41A8102480A87C8 /* [CP] Embed Pods Frameworks */, + C6773F2F6DC462D1292B9F42 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -180,9 +226,9 @@ productReference = 00E356EE1AD99517003FC87E /* SampleAppTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 13B07F861A680F5B00A75B9A /* SampleApp */ = { + 13B07F861A680F5B00A75B9A /* SampleAppCarPlay */ = { isa = PBXNativeTarget; - buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SampleApp" */; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SampleAppCarPlay" */; buildPhases = ( C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, @@ -196,11 +242,32 @@ ); dependencies = ( ); - name = SampleApp; + name = SampleAppCarPlay; productName = SampleApp; productReference = 13B07F961A680F5B00A75B9A /* SampleApp.app */; productType = "com.apple.product-type.application"; }; + 2A20E80C2C8994DB00DB7ADA /* SampleApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A20E81D2C8994DB00DB7ADA /* Build configuration list for PBXNativeTarget "SampleApp" */; + buildPhases = ( + 2A20E80D2C8994DB00DB7ADA /* [CP] Check Pods Manifest.lock */, + 2A20E80E2C8994DB00DB7ADA /* Sources */, + 2A20E8132C8994DB00DB7ADA /* Frameworks */, + 2AB8C2792C89A06200250560 /* Resources */, + 2A20E81A2C8994DB00DB7ADA /* Bundle React Native code and images */, + 2A20E81B2C8994DB00DB7ADA /* [CP] Embed Pods Frameworks */, + 2A20E81C2C8994DB00DB7ADA /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SampleApp; + productName = SampleApp; + productReference = 2A20E8202C8994DB00DB7ADA /* SampleApp.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -231,7 +298,8 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 13B07F861A680F5B00A75B9A /* SampleApp */, + 2A20E80C2C8994DB00DB7ADA /* SampleApp */, + 13B07F861A680F5B00A75B9A /* SampleAppCarPlay */, 00E356ED1AD99517003FC87E /* SampleAppTests */, ); }; @@ -250,9 +318,21 @@ buildActionMask = 2147483647; files = ( 52D4271D2C81D3F300C7FB36 /* Keys.plist in Resources */, + 2A20E8282C899A1400DB7ADA /* Info-CarPlay.plist in Resources */, 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, + 52206E802C8B2F9E00B34D22 /* PrivacyInfo.xcprivacy in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, - E72EC806AB287A94E8141AD5 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2AB8C2792C89A06200250560 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2AB8C27F2C89A0C800250560 /* Images.xcassets in Resources */, + 2AB8C27E2C89A0B400250560 /* LaunchScreen.storyboard in Resources */, + 52206E7F2C8B2F9E00B34D22 /* PrivacyInfo.xcprivacy in Resources */, + 2AB8C27A2C89A07000250560 /* Keys.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -281,18 +361,18 @@ files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-SampleAppCarPlay/Pods-SampleAppCarPlay-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-SampleAppCarPlay/Pods-SampleAppCarPlay-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleAppCarPlay/Pods-SampleAppCarPlay-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = { + 2A20E80D2C8994DB00DB7ADA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -307,14 +387,81 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-SampleApp-SampleAppTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-SampleApp-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = { + 2A20E81A2C8994DB00DB7ADA /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 12; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; + }; + 2A20E81B2C8994DB00DB7ADA /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 2A20E81C2C8994DB00DB7ADA /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7CDED63BA41A8102480A87C8 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SampleAppTests/Pods-SampleAppTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SampleAppTests/Pods-SampleAppTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleAppTests/Pods-SampleAppTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -329,62 +476,67 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-SampleApp-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-SampleAppTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - C59DA0FBD6956966B86A3779 /* [CP] Embed Pods Frameworks */ = { + C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SampleAppCarPlay-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { + C6773F2F6DC462D1292B9F42 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-resources-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-SampleAppTests/Pods-SampleAppTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-resources-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-SampleAppTests/Pods-SampleAppTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleAppTests/Pods-SampleAppTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; - F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */ = { + E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-resources-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-SampleAppCarPlay/Pods-SampleAppCarPlay-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-resources-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-SampleAppCarPlay/Pods-SampleAppCarPlay-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleAppCarPlay/Pods-SampleAppCarPlay-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -402,17 +554,28 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, + 52206E852C8B2FEC00B34D22 /* CarSceneDelegate.m in Sources */, + 52206E892C8B303700B34D22 /* AppDelegateCarPlay.m in Sources */, + 52206E862C8B2FEC00B34D22 /* PhoneSceneDelegate.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 2A20E80E2C8994DB00DB7ADA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5205C1232C8B314F00D0FC6B /* AppDelegate.m in Sources */, + 2A20E8122C8994DB00DB7ADA /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 13B07F861A680F5B00A75B9A /* SampleApp */; + target = 13B07F861A680F5B00A75B9A /* SampleAppCarPlay */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -420,7 +583,7 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5B7EB9410499542E8C5724F5 /* Pods-SampleApp-SampleAppTests.debug.xcconfig */; + baseConfigurationReference = 011A22ED61B5A84DFF838AB2 /* Pods-SampleAppTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -447,7 +610,7 @@ }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 89C6BE57DB24E9ADA2F236DE /* Pods-SampleApp-SampleAppTests.release.xcconfig */; + baseConfigurationReference = A9D547919643FC330CF84EC3 /* Pods-SampleAppTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; COPY_PHASE_STRIP = NO; @@ -470,28 +633,142 @@ name = Release; }; 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4735EA9AB0E8831D741A3A3E /* Pods-SampleAppCarPlay.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = SampleApp/SampleApp.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "CARPLAY=1", + ); + INFOPLIST_FILE = "SampleApp/Info-CarPlay.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_CPLUSPLUSFLAGS = ( + "$(inherited)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-Wno-comma", + "-Wno-shorten-64-to-32", + "-fmodules", + "-fcxx-modules", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.--PRODUCT-NAME-rfc1034identifier-.carplay"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = org.reactjs.native.example.SampleApp.carplay; + PRODUCT_NAME = SampleApp; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 750C10660A3F190F6072C5DA /* Pods-SampleAppCarPlay.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = SampleApp/SampleApp.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "CARPLAY=1", + ); + INFOPLIST_FILE = "SampleApp/Info-CarPlay.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_CPLUSPLUSFLAGS = ( + "$(inherited)", + "-DNDEBUG", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-Wno-comma", + "-Wno-shorten-64-to-32", + "-fmodules", + "-fcxx-modules", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.--PRODUCT-NAME-rfc1034identifier-.carplay"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = org.reactjs.native.example.SampleApp.carplay; + PRODUCT_NAME = SampleApp; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 2A20E81E2C8994DB00DB7ADA /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-SampleApp.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; ENABLE_BITCODE = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + ); INFOPLIST_FILE = SampleApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; + OTHER_CPLUSPLUSFLAGS = ( + "$(inherited)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-Wno-comma", + "-Wno-shorten-64-to-32", + "-fmodules", + "-fcxx-modules", + ); OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = SampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -499,28 +776,46 @@ }; name = Debug; }; - 13B07F951A680F5B00A75B9A /* Release */ = { + 2A20E81F2C8994DB00DB7ADA /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-SampleApp.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + ); INFOPLIST_FILE = SampleApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; + OTHER_CPLUSPLUSFLAGS = ( + "$(inherited)", + "-DNDEBUG", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-Wno-comma", + "-Wno-shorten-64-to-32", + "-fmodules", + "-fcxx-modules", + ); OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = SampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -692,7 +987,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SampleApp" */ = { + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SampleAppCarPlay" */ = { isa = XCConfigurationList; buildConfigurations = ( 13B07F941A680F5B00A75B9A /* Debug */, @@ -701,6 +996,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 2A20E81D2C8994DB00DB7ADA /* Build configuration list for PBXNativeTarget "SampleApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A20E81E2C8994DB00DB7ADA /* Debug */, + 2A20E81F2C8994DB00DB7ADA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "SampleApp" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/example/ios/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme b/example/ios/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme index 1b6ff67..84ce098 100644 --- a/example/ios/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme +++ b/example/ios/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme @@ -1,10 +1,11 @@ + LastUpgradeVersion = "1530" + version = "1.7"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> @@ -26,19 +27,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> @@ -71,7 +61,7 @@ runnableDebuggingMode = "0"> diff --git a/example/ios/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleAppCarPlay.xcscheme b/example/ios/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleAppCarPlay.xcscheme new file mode 100644 index 0000000..a69286e --- /dev/null +++ b/example/ios/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleAppCarPlay.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/SampleApp/AppDelegate.mm b/example/ios/SampleApp/AppDelegate.m similarity index 84% rename from example/ios/SampleApp/AppDelegate.mm rename to example/ios/SampleApp/AppDelegate.m index e029e96..09cb90d 100644 --- a/example/ios/SampleApp/AppDelegate.mm +++ b/example/ios/SampleApp/AppDelegate.m @@ -26,7 +26,7 @@ - (BOOL)application:(UIApplication *)application // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; - + // Note: Ensure that you have copied the Keys.plist.sample to Keys.plist // and have added the correct API_KEY value to the file. // @@ -34,11 +34,10 @@ - (BOOL)application:(UIApplication *)application NSString *path = [[NSBundle mainBundle] pathForResource:@"Keys" ofType:@"plist"]; NSDictionary *keysDictionary = [NSDictionary dictionaryWithContentsOfFile:path]; NSString *api_key = [keysDictionary objectForKey:@"API_KEY"]; - + [GMSServices provideAPIKey:api_key]; [GMSServices setMetalRendererEnabled:YES]; - return [super application:application - didFinishLaunchingWithOptions:launchOptions]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; } - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { @@ -47,11 +46,9 @@ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { - (NSURL *)bundleURL { #if DEBUG - return - [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; #else - return [[NSBundle mainBundle] URLForResource:@"main" - withExtension:@"jsbundle"]; + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif } diff --git a/example/ios/SampleApp/AppDelegateCarPlay.h b/example/ios/SampleApp/AppDelegateCarPlay.h new file mode 100644 index 0000000..e25833c --- /dev/null +++ b/example/ios/SampleApp/AppDelegateCarPlay.h @@ -0,0 +1,23 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#import +#import + +@interface AppDelegateCarPlay : RCTAppDelegate +@property(nonatomic, strong) UIWindow *window; +@property(nonatomic, strong) RCTBridge *bridge; +@property(nonatomic, strong) RCTRootView *rootView; +@end diff --git a/example/ios/SampleApp/AppDelegateCarPlay.m b/example/ios/SampleApp/AppDelegateCarPlay.m new file mode 100644 index 0000000..9e75dbe --- /dev/null +++ b/example/ios/SampleApp/AppDelegateCarPlay.m @@ -0,0 +1,83 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#import "AppDelegateCarPlay.h" + +#import +#import +#import +#import +#import "CarSceneDelegate.h" +#import "PhoneSceneDelegate.h" + +@implementation AppDelegateCarPlay + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + self.moduleName = @"SampleApp"; + + // Note: Ensure that you have copied the Keys.plist.sample to Keys.plist + // and have added the correct API_KEY value to the file. + // + // Get the path for the Keys.plist file in the main bundle and read API_KEY. + NSString *path = [[NSBundle mainBundle] pathForResource:@"Keys" ofType:@"plist"]; + NSDictionary *keysDictionary = [NSDictionary dictionaryWithContentsOfFile:path]; + NSString *api_key = [keysDictionary objectForKey:@"API_KEY"]; + + [GMSServices provideAPIKey:api_key]; + [GMSServices setMetalRendererEnabled:YES]; + RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; + self.rootView = [[RCTRootView alloc] initWithBridge:bridge + moduleName:self.moduleName + initialProperties:nil]; + return YES; +} + +- (UISceneConfiguration *)application:(UIApplication *)application + configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession + options:(UISceneConnectionOptions *)options { + if ([connectingSceneSession.role + isEqualToString:@"CPTemplateApplicationSceneSessionRoleApplication"]) { + UISceneConfiguration *scene = + [[UISceneConfiguration alloc] initWithName:@"CarPlay" + sessionRole:connectingSceneSession.role]; + scene.delegateClass = [CarSceneDelegate class]; + return scene; + } else { + UISceneConfiguration *scene = + [[UISceneConfiguration alloc] initWithName:@"Phone" + sessionRole:connectingSceneSession.role]; + scene.delegateClass = [PhoneSceneDelegate class]; + return scene; + } +} + +- (void)application:(UIApplication *)application + didDiscardSceneSessions:(NSSet *)sceneSessions { +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { + return [self bundleURL]; +} + +- (NSURL *)bundleURL { +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +@end diff --git a/example/ios/SampleApp/CarSceneDelegate.h b/example/ios/SampleApp/CarSceneDelegate.h new file mode 100644 index 0000000..c43a3d8 --- /dev/null +++ b/example/ios/SampleApp/CarSceneDelegate.h @@ -0,0 +1,20 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#import +#import "BaseCarSceneDelegate.h" + +@interface CarSceneDelegate : BaseCarSceneDelegate +@end diff --git a/example/ios/SampleApp/CarSceneDelegate.m b/example/ios/SampleApp/CarSceneDelegate.m new file mode 100644 index 0000000..af87193 --- /dev/null +++ b/example/ios/SampleApp/CarSceneDelegate.m @@ -0,0 +1,42 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#import "CarSceneDelegate.h" +#import +#import +#import "NavAutoModule.h" +#import "NavModule.h" + +@implementation CarSceneDelegate + +- (CPMapTemplate *)getTemplate { + CPMapTemplate *template = [[CPMapTemplate alloc] init]; + [template showPanningInterfaceAnimated:YES]; + + CPBarButton *customButton = [[CPBarButton alloc] + initWithTitle:@"Custom Event" + handler:^(CPBarButton *_Nonnull button) { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + dictionary[@"sampleDataKey"] = @"sampleDataContent"; + [[NavAutoModule getOrCreateSharedInstance] onCustomNavigationAutoEvent:@"sampleEvent" + data:dictionary]; + }]; + + template.leadingNavigationBarButtons = @[ customButton ]; + template.trailingNavigationBarButtons = @[]; + return template; +} + +@end diff --git a/example/ios/SampleApp/Info-CarPlay.plist b/example/ios/SampleApp/Info-CarPlay.plist new file mode 100644 index 0000000..09ed6a7 --- /dev/null +++ b/example/ios/SampleApp/Info-CarPlay.plist @@ -0,0 +1,98 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + samplenavsdk-carplay + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + NSLocationAlwaysAndWhenInUseUsageDescription + [Enter any description related to the key] + NSLocationWhenInUseUsageDescription + [Add your description here] + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + CPTemplateApplicationSceneSessionRoleApplication + + + UISceneClassName + CPTemplateApplicationScene + UISceneConfigurationName + CarPlay + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).CarSceneDelegate + + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + Phone + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).PhoneSceneDelegate + + + + + UIBackgroundModes + + location + audio + remote-notification + fetch + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + [NSLocationAlwaysUsageDescription] + Enter any description related to the key + [NSLocationWhenInUseUsageDescription] + Enter any description related to the key + + diff --git a/example/ios/SampleApp/PhoneSceneDelegate.h b/example/ios/SampleApp/PhoneSceneDelegate.h new file mode 100644 index 0000000..860d1a2 --- /dev/null +++ b/example/ios/SampleApp/PhoneSceneDelegate.h @@ -0,0 +1,24 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#import +#import +#import "AppDelegateCarPlay.h" + +@interface PhoneSceneDelegate : UIResponder + +@property(nonatomic, strong) UIWindow *window; + +@end diff --git a/example/ios/SampleApp/PhoneSceneDelegate.m b/example/ios/SampleApp/PhoneSceneDelegate.m new file mode 100644 index 0000000..5e552d2 --- /dev/null +++ b/example/ios/SampleApp/PhoneSceneDelegate.m @@ -0,0 +1,47 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#import "PhoneSceneDelegate.h" +#import +#import +#import "AppDelegateCarPlay.h" + +@implementation PhoneSceneDelegate + +- (void)scene:(UIScene *)scene + willConnectToSession:(UISceneSession *)session + options:(UISceneConnectionOptions *)connectionOptions { + AppDelegateCarPlay *appDelegate = + (AppDelegateCarPlay *)[UIApplication sharedApplication].delegate; + if (!appDelegate) { + return; + } + + UIWindowScene *windowScene = (UIWindowScene *)scene; + if (!windowScene) { + return; + } + + UIViewController *rootViewController = [[UIViewController alloc] init]; + rootViewController.view = appDelegate.rootView; + + UIWindow *window = [[UIWindow alloc] initWithWindowScene:windowScene]; + window.rootViewController = rootViewController; + self.window = window; + [appDelegate setWindow:window]; + [window makeKeyAndVisible]; +} + +@end diff --git a/example/ios/SampleApp/SampleApp.entitlements b/example/ios/SampleApp/SampleApp.entitlements new file mode 100644 index 0000000..d74cc08 --- /dev/null +++ b/example/ios/SampleApp/SampleApp.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.developer.carplay-maps + + + diff --git a/example/ios/SampleApp/main.m b/example/ios/SampleApp/main.m index 3465d6b..9fd1998 100644 --- a/example/ios/SampleApp/main.m +++ b/example/ios/SampleApp/main.m @@ -14,10 +14,18 @@ #import +#if defined(CARPLAY) +#import "AppDelegateCarPlay.h" +#else #import "AppDelegate.h" +#endif int main(int argc, char *argv[]) { @autoreleasepool { +#if defined(CARPLAY) + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegateCarPlay class])); +#else return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); +#endif } } diff --git a/example/package.json b/example/package.json index 0a36b91..4eb3b64 100644 --- a/example/package.json +++ b/example/package.json @@ -8,6 +8,7 @@ "ios": "react-native run-ios", "lint": "eslint .", "test": "jest", + "prepare": "cd ios && pod install", "build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a", "build:ios": "cd ios && xcodebuild -workspace SampleApp.xcworkspace -scheme SampleApp -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" }, diff --git a/example/src/App.tsx b/example/src/App.tsx index a4389ea..66364e8 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -22,8 +22,8 @@ import { } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import { View, Button, StyleSheet } from 'react-native'; -import NavigationScreen from './NavigationScreen'; -import MultipleMapsScreen from './MultipleMapsScreen'; +import NavigationScreen from './screens/NavigationScreen'; +import MultipleMapsScreen from './screens/MultipleMapsScreen'; import { NavigationProvider, type TermsAndConditionsDialogOptions, diff --git a/example/src/mapsControls.tsx b/example/src/controls/mapsControls.tsx similarity index 99% rename from example/src/mapsControls.tsx rename to example/src/controls/mapsControls.tsx index 3c4bed7..b1fc031 100644 --- a/example/src/mapsControls.tsx +++ b/example/src/controls/mapsControls.tsx @@ -19,7 +19,7 @@ import React, { useEffect, useState } from 'react'; import { Button, Switch, Text, TextInput, View } from 'react-native'; import SelectDropdown from 'react-native-select-dropdown'; -import styles from './styles'; +import styles from '../styles'; import { type MapViewController, MapType, diff --git a/example/src/navigationControls.tsx b/example/src/controls/navigationControls.tsx similarity index 99% rename from example/src/navigationControls.tsx rename to example/src/controls/navigationControls.tsx index 9a18795..012c970 100644 --- a/example/src/navigationControls.tsx +++ b/example/src/controls/navigationControls.tsx @@ -35,7 +35,7 @@ import { } from '@googlemaps/react-native-navigation-sdk'; import SelectDropdown from 'react-native-select-dropdown'; -import styles from './styles'; +import styles from '../styles'; export interface NavigationControlsProps { readonly navigationController: NavigationController; diff --git a/example/src/overlayModal.tsx b/example/src/helpers/overlayModal.tsx similarity index 100% rename from example/src/overlayModal.tsx rename to example/src/helpers/overlayModal.tsx diff --git a/example/src/MultipleMapsScreen.tsx b/example/src/screens/MultipleMapsScreen.tsx similarity index 97% rename from example/src/MultipleMapsScreen.tsx rename to example/src/screens/MultipleMapsScreen.tsx index 2300a3f..1a06ec5 100644 --- a/example/src/MultipleMapsScreen.tsx +++ b/example/src/screens/MultipleMapsScreen.tsx @@ -17,31 +17,33 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { Button, View } from 'react-native'; import Snackbar from 'react-native-snackbar'; -import MapsControls from './mapsControls'; -import NavigationControls from './navigationControls'; -import styles from './styles'; + import { - type MapViewController, - type NavigationViewController, - type ArrivalEvent, NavigationInitErrorCode, - type Location, + NavigationView, RouteStatus, + type ArrivalEvent, + type Circle, + type LatLng, + type Location, + type MapViewCallbacks, + type MapViewController, type Marker, + type NavigationCallbacks, type NavigationViewCallbacks, - type MapViewCallbacks, + type NavigationViewController, type Polygon, - type Circle, type Polyline, - type LatLng, - type NavigationCallbacks, useNavigation, MapView, - NavigationView, } from '@googlemaps/react-native-navigation-sdk'; -import usePermissions from './checkPermissions'; -import OverlayModal from './overlayModal'; +import MapsControls from '../controls/mapsControls'; +import NavigationControls from '../controls/navigationControls'; +import OverlayModal from '../helpers/overlayModal'; +import styles from '../styles'; +import usePermissions from '../checkPermissions'; +// Utility function for showing Snackbar const showSnackbar = (text: string, duration = Snackbar.LENGTH_SHORT) => { Snackbar.show({ text, duration }); }; diff --git a/example/src/NavigationScreen.tsx b/example/src/screens/NavigationScreen.tsx similarity index 83% rename from example/src/NavigationScreen.tsx rename to example/src/screens/NavigationScreen.tsx index 2e2cdb8..cd14d7c 100644 --- a/example/src/NavigationScreen.tsx +++ b/example/src/screens/NavigationScreen.tsx @@ -17,29 +17,33 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { Button, Switch, Text, View } from 'react-native'; import Snackbar from 'react-native-snackbar'; -import styles from './styles'; + import { + NavigationInitErrorCode, + NavigationView, + RouteStatus, + type ArrivalEvent, + type Circle, + type LatLng, + type Location, + type MapViewCallbacks, type MapViewController, - type NavigationViewController, type Marker, + type NavigationAutoCallbacks, + type NavigationCallbacks, type NavigationViewCallbacks, - type MapViewCallbacks, + type NavigationViewController, type Polygon, - type Circle, type Polyline, - type LatLng, - type NavigationCallbacks, useNavigation, - NavigationInitErrorCode, - RouteStatus, - type ArrivalEvent, - type Location, + useNavigationAuto, + type CustomNavigationAutoEvent, } from '@googlemaps/react-native-navigation-sdk'; -import usePermissions from './checkPermissions'; -import MapsControls from './mapsControls'; -import NavigationControls from './navigationControls'; -import OverlayModal from './overlayModal'; -import { NavigationView } from '../../src/navigation/navigationView/navigationView'; +import MapsControls from '../controls/mapsControls'; +import NavigationControls from '../controls/navigationControls'; +import OverlayModal from '../helpers/overlayModal'; +import styles from '../styles'; +import usePermissions from '../checkPermissions'; // Utility function for showing Snackbar const showSnackbar = (text: string, duration = Snackbar.LENGTH_SHORT) => { @@ -50,6 +54,7 @@ enum OverlayType { None = 'None', NavControls = 'NavControls', MapControls = 'MapControls', + AutoMapControls = 'AutoMapControls', } const marginAmount = 50; @@ -62,6 +67,14 @@ const NavigationScreen = () => { const [navigationViewController, setNavigationViewController] = useState(null); + const { + mapViewAutoController, + addListeners: addAutoListener, + removeListeners: removeAutoListeners, + } = useNavigationAuto(); + const [mapViewAutoAvailable, setMapViewAutoAvailable] = + useState(false); + const { navigationController, addListeners, removeListeners } = useNavigation(); @@ -229,6 +242,27 @@ const NavigationScreen = () => { ] ); + const navigationAutoCallbacks: NavigationAutoCallbacks = useMemo( + () => ({ + onCustomNavigationAutoEvent: (event: CustomNavigationAutoEvent) => { + console.log('onCustomNavigationAutoEvent:', event); + }, + onAutoScreenAvailabilityChanged: (available: boolean) => { + console.log('onAutoScreenAvailabilityChanged:', available); + setMapViewAutoAvailable(available); + }, + }), + [] + ); + + useEffect(() => { + (async () => { + const isAvailable = await mapViewAutoController.isAutoScreenAvailable(); + console.log('isAutoScreenAvailable:', isAvailable); + setMapViewAutoAvailable(isAvailable); + })(); + }, [mapViewAutoController]); + useEffect(() => { addListeners(navigationCallbacks); return () => { @@ -236,6 +270,13 @@ const NavigationScreen = () => { }; }, [navigationCallbacks, addListeners, removeListeners]); + useEffect(() => { + addAutoListener(navigationAutoCallbacks); + return () => { + removeAutoListeners(navigationAutoCallbacks); + }; + }, [navigationAutoCallbacks, addAutoListener, removeAutoListeners]); + const onMapReady = useCallback(async () => { console.log('Map is ready, initializing navigator...'); try { @@ -258,6 +299,10 @@ const NavigationScreen = () => { setOverlayType(OverlayType.MapControls); }, [setOverlayType]); + const onShowAutoMapsControlsClick = useCallback(() => { + setOverlayType(OverlayType.AutoMapControls); + }, [setOverlayType]); + const navigationViewCallbacks: NavigationViewCallbacks = { onRecenterButtonClick, }; @@ -343,6 +388,15 @@ const NavigationScreen = () => { )} + {mapViewAutoAvailable && mapViewAutoController != null && ( + + + + )} +