> {
16 | return emptyList()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/interfaces/MapButton.ts:
--------------------------------------------------------------------------------
1 | import { ImageSourcePropType } from 'react-native';
2 |
3 | /**
4 | * A button representing an action that a map template displays on the CarPlay screen.
5 | */
6 | export interface MapButton {
7 | /**
8 | * Button ID
9 | */
10 | id: string;
11 | /**
12 | * The image to display on the button.
13 | */
14 | image?: ImageSourcePropType;
15 | /**
16 | * The image to display when focus is on the button.
17 | */
18 | focusedImage?: ImageSourcePropType;
19 | /**
20 | * A Boolean value that enables and disables the map button.
21 | */
22 | disabled?: boolean;
23 | /**
24 | * A Boolean value that hides and shows the map button.
25 | */
26 | hidden?: boolean;
27 | }
28 |
--------------------------------------------------------------------------------
/apps/example/src/AndroidAuto.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Text, View } from 'react-native';
3 | import { CarPlay } from 'react-native-carplay';
4 | import { menuTemplate } from './templates/menu.template';
5 |
6 | export function AndroidAutoModule() {
7 | CarPlay.emitter.addListener('didConnect', () => {
8 | CarPlay.setRootTemplate(menuTemplate);
9 | });
10 | CarPlay.emitter.addListener('backButtonPressed', () => {
11 | CarPlay.popTemplate();
12 | });
13 | return;
14 | }
15 |
16 | export function AndroidAuto() {
17 | return (
18 |
19 | Hello Android Auto
20 | {
23 | CarPlay.bridge.reload();
24 | }}
25 | />
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # OSX
3 | #
4 | .DS_Store
5 |
6 | # node.js
7 | #
8 | node_modules/
9 | npm-debug.log
10 | yarn-error.log
11 |
12 | # Yarn
13 | #
14 | .pnp.*
15 | .yarn/*
16 | !.yarn/patches
17 | !.yarn/plugins
18 | !.yarn/releases
19 | !.yarn/sdks
20 | !.yarn/versions
21 |
22 | # Xcode
23 | #
24 | build/
25 | *.pbxuser
26 | !default.pbxuser
27 | *.mode1v3
28 | !default.mode1v3
29 | *.mode2v3
30 | !default.mode2v3
31 | *.perspectivev3
32 | !default.perspectivev3
33 | xcuserdata
34 | *.xccheckout
35 | *.moved-aside
36 | DerivedData
37 | *.hmap
38 | *.ipa
39 | *.xcuserstate
40 | project.xcworkspace
41 | Pods/
42 |
43 | # Android/IntelliJ
44 | #
45 | build/
46 | .idea
47 | .gradle
48 | local.properties
49 | *.iml
50 |
51 | # BUCK
52 | buck-out/
53 | \.buckd/
54 | *.keystore
55 |
56 | ## Package
57 | .env
58 | lib
59 |
--------------------------------------------------------------------------------
/apps/example/android/app/src/release/java/com/rncarplayscene/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.rncarplayscene;
8 | import android.content.Context;
9 | import com.facebook.react.ReactInstanceManager;
10 | /**
11 | * Class responsible of loading Flipper inside your React Native application. This is the release
12 | * flavor of it so it's empty as we don't want to load Flipper.
13 | */
14 | public class ReactNativeFlipper {
15 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
16 | // Do nothing as we don't want to initialize Flipper on Release.
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/ios/RCTConvert+RNCarPlay.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import
4 | #import
5 |
6 | @interface RCTConvert (RNCarPlay)
7 |
8 | + (CPTripEstimateStyle)CPTripEstimateStyle:(id)json;
9 | + (CPPanDirection)CPPanDirection:(id)json;
10 | + (CPAssistantCellPosition)CPAssistantCellPosition:(id)json;
11 | + (CPAssistantCellVisibility)CPAssistantCellVisibility:(id)json;
12 | + (CPAssistantCellActionType)CPAssistantCellActionType:(id)json;
13 | + (CPMapButton*)CPMapButton:(id)json withHandler:(void (^)(CPMapButton * _Nonnull mapButton))handler;
14 | + (CPRouteChoice*)CPRouteChoice:(id)json;
15 | + (MKMapItem*)MKMapItem:(id)json;
16 | + (CPPointOfInterest*)CPPointOfInterest:(id)json;
17 | + (CPAlertActionStyle)CPAlertActionStyle:(id)json;
18 | @end
19 |
--------------------------------------------------------------------------------
/apps/example/ios/RNCarPlaySceneTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/apps/example/ios/PhoneScene.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 | import SwiftUI
4 |
5 | class PhoneSceneDelegate: UIResponder, UIWindowSceneDelegate {
6 | var window: UIWindow?
7 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
8 |
9 | if session.role != .windowApplication {
10 | return
11 | }
12 |
13 | guard let appDelegate = (UIApplication.shared.delegate as? AppDelegate) else { return }
14 | guard let windowScene = (scene as? UIWindowScene) else { return }
15 |
16 | let rootViewController = UIViewController()
17 | rootViewController.view = appDelegate.rootView;
18 |
19 | let window = UIWindow(windowScene: windowScene)
20 | window.rootViewController = rootViewController
21 | self.window = window
22 | window.makeKeyAndVisible()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/apps/example/src/screens/POI.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Text, View } from 'react-native';
3 | import { CarPlay, PointOfInterestTemplate } from 'react-native-carplay';
4 |
5 | export function POI() {
6 | useEffect(() => {
7 | const template = new PointOfInterestTemplate({
8 | title: 'Example',
9 | items: [
10 | {
11 | id: 'test',
12 | location: { latitude: 64.011, longitude: -21.66 },
13 | title: 'Testing',
14 | subtitle: 'foobar',
15 | },
16 | ],
17 | });
18 | CarPlay.pushTemplate(template, false);
19 | return () => {};
20 | }, []);
21 |
22 | return (
23 |
24 | POI
25 |
26 | );
27 | }
28 |
29 | POI.navigationOptions = {
30 | headerTitle: 'Alert Template',
31 | };
32 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/navigation/Trip.ts:
--------------------------------------------------------------------------------
1 | import { CarPlay } from '../CarPlay';
2 |
3 | export interface RouteChoice {
4 | additionalInformationVariants: string[];
5 | selectionSummaryVariants: string[];
6 | summaryVariants: string[];
7 | }
8 |
9 | export interface TripPoint {
10 | latitude: number;
11 | longitude: number;
12 | name: string;
13 | }
14 |
15 | export interface TripConfig {
16 | id?: string;
17 | origin: TripPoint;
18 | destination: TripPoint;
19 | routeChoices: RouteChoice[];
20 | }
21 |
22 | export class Trip {
23 | public id!: string;
24 |
25 | constructor(public config: TripConfig) {
26 | if (config.id) {
27 | this.id = config.id;
28 | }
29 |
30 | if (!this.id) {
31 | this.id = `trip-${Date.now()}-${Math.round(Math.random() * Number.MAX_SAFE_INTEGER)}`;
32 | }
33 |
34 | CarPlay.bridge.createTrip(this.id, config);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/VoiceControlTemplate.ts:
--------------------------------------------------------------------------------
1 | import { CarPlay } from '../CarPlay';
2 | import { VoiceControlState } from '../interfaces/VoiceControlState';
3 | import { Template } from './Template';
4 |
5 | export interface VoiceControlTemplateConfig {
6 | /**
7 | * The array of voice control states that can be used by your voice control template.
8 | */
9 | voiceControlStates: VoiceControlState[];
10 | }
11 |
12 | /**
13 | * Displays a voice control indicator on the CarPlay screen.
14 | *
15 | * CarPlay navigation apps must show the voice control template during audio input.
16 | */
17 | export class VoiceControlTemplate extends Template {
18 | public get type(): string {
19 | return 'voicecontrol';
20 | }
21 |
22 | public activateVoiceControlState(identifier: string) {
23 | CarPlay.bridge.activateVoiceControlState(this.id, identifier);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/constraints.pro:
--------------------------------------------------------------------------------
1 | % Yarn Constraints https://yarnpkg.com/features/constraints
2 | % check with "yarn constraints" (fix w/ "yarn constraints --fix")
3 | % reference for other constraints: https://github.com/babel/babel/blob/main/constraints.pro
4 |
5 | % Enforces that a dependency doesn't appear in both `dependencies` and `devDependencies`
6 | gen_enforced_dependency(WorkspaceCwd, DependencyIdent, null, 'devDependencies') :-
7 | workspace_has_dependency(WorkspaceCwd, DependencyIdent, _, 'devDependencies'),
8 | workspace_has_dependency(WorkspaceCwd, DependencyIdent, _, 'dependencies').
9 |
10 | % Force all workspace dependencies to be made explicit
11 | % https://yarnpkg.com/features/constraints#force-all-workspace-dependencies-to-be-made-explicit
12 | gen_enforced_dependency(WorkspaceCwd, DependencyIdent, 'workspace:*', DependencyType) :-
13 | workspace_ident(_, DependencyIdent),
14 | workspace_has_dependency(WorkspaceCwd, DependencyIdent, _, DependencyType).
15 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/java/org/birkir/carplay/parser/RCTPaneTemplate.kt:
--------------------------------------------------------------------------------
1 | package org.birkir.carplay.parser
2 |
3 | import androidx.car.app.CarContext
4 | import androidx.car.app.model.PaneTemplate
5 | import com.facebook.react.bridge.ReadableMap
6 | import org.birkir.carplay.screens.CarScreenContext
7 |
8 | class RCTPaneTemplate(
9 | context: CarContext,
10 | carScreenContext: CarScreenContext
11 | ) : RCTTemplate(context, carScreenContext) {
12 | override fun parse(props: ReadableMap): PaneTemplate {
13 | val pane = parsePane(props.getMap("pane")!!)
14 | return PaneTemplate.Builder(pane).apply {
15 | props.getArray("actions")?.let { setActionStrip(parseActionStrip(it)) }
16 | props.getMap("headerAction")?.let { setHeaderAction(parseAction(it)) }
17 | props.getString("title")?.let { setTitle(it) }
18 | }.build()
19 | }
20 |
21 | companion object {
22 | const val TAG = "RCTPaneTemplate"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/ios/RNCPStore.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface RNCPStore : NSObject {
5 | CPInterfaceController *interfaceController;
6 | CPWindow *window;
7 | }
8 |
9 | @property (nonatomic, retain) CPInterfaceController *interfaceController;
10 | @property (nonatomic, retain) CPWindow *window;
11 |
12 | + (id)sharedManager;
13 | - (CPTemplate*) findTemplateById: (NSString*)templateId;
14 | - (NSString*) setTemplate:(NSString*)templateId template:(CPTemplate*)carPlayTemplate;
15 | - (CPTrip*) findTripById: (NSString*)tripId;
16 | - (NSString*) setTrip:(NSString*)tripId trip:(CPTrip*)trip;
17 | - (CPNavigationSession*) findNavigationSessionById:(NSString*)navigationSessionId;
18 | - (NSString*) setNavigationSession:(NSString*)navigationSessionId navigationSession:(CPNavigationSession*)navigationSession;
19 | - (Boolean) isConnected;
20 | - (void) setConnected:(Boolean) isConnected;
21 |
22 | @end
23 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/interfaces/GridButton.ts:
--------------------------------------------------------------------------------
1 | import { ImageSourcePropType } from 'react-native';
2 |
3 | /**
4 | * A menu item button displayed on a grid template.
5 | */
6 | export interface GridButton {
7 | /**
8 | * Button ID
9 | */
10 | id: string;
11 | /**
12 | * An array of title variants for the button.
13 | *
14 | * When the system displays the button, it selects the title that best fits the available screen space, so arrange the titles from most to least preferred when creating a grid button. Also, localize each title for display to the user, and be sure to include at least one title in the array.
15 | */
16 | titleVariants: string[];
17 | /**
18 | * The image displayed on the button.
19 | *
20 | * When creating a grid button, don't provide an animated image. If you do, the button uses the first image in the animation sequence.
21 | */
22 | image: ImageSourcePropType;
23 |
24 | disabled?: boolean;
25 | }
26 |
--------------------------------------------------------------------------------
/apps/example/src/screens/VoiceControl.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Text, View } from 'react-native';
3 | import { CarPlay, VoiceControlTemplate } from 'react-native-carplay';
4 |
5 | export function VoiceControl() {
6 | useEffect(() => {
7 | const voiceControlTemplate = new VoiceControlTemplate({
8 | voiceControlStates: [
9 | {
10 | identifier: 'TEST',
11 | image: require('../images/cat.jpg'),
12 | repeats: true,
13 | titleVariants: ['Searching...'],
14 | },
15 | ],
16 | });
17 |
18 | CarPlay.presentTemplate(voiceControlTemplate, true);
19 |
20 | return () => CarPlay.dismissTemplate(true);
21 | }, []);
22 |
23 | return (
24 |
25 | Voice
26 |
27 | );
28 | }
29 |
30 | VoiceControl.navigationOptions = {
31 | headerTitle: 'Voice Control Template',
32 | };
33 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/interfaces/TravelEstimates.ts:
--------------------------------------------------------------------------------
1 | import { CarColor } from './CarColor';
2 |
3 | export type DistanceUnits = 'meters' | 'miles' | 'kilometers' | 'yards' | 'feet';
4 |
5 | export interface TravelEstimates {
6 | /**
7 | * Distance remaining
8 | */
9 | distanceRemaining: number;
10 | /**
11 | * Time remaining in seconds
12 | */
13 | timeRemaining: number;
14 | /**
15 | * unit of measurement for the
16 | * distance, defaults to kilometer
17 | * @namespace iOS
18 | */
19 | distanceUnits?: DistanceUnits;
20 |
21 | /**
22 | * Color of the distance remaining
23 | * @namespace Android
24 | */
25 | distanceRemainingColor?: CarColor;
26 | /**
27 | * Color of the time remaining
28 | * @namespace Android
29 | */
30 | timeRemainingColor?: CarColor;
31 | /**
32 | * Destination time
33 | * @namespace Android
34 | */
35 | destinationTime?: {
36 | timeSinceEpochMillis: number;
37 | id: string;
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/apps/docs/theme.config.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DocsThemeConfig } from 'nextra-theme-docs';
3 |
4 | const config: DocsThemeConfig = {
5 | logo: react-native-carplay ,
6 | project: {
7 | link: 'https://github.com/birkir/react-native-carplay',
8 | },
9 | chat: {
10 | link: 'https://discord.gg/b235pv6QHM',
11 | },
12 | docsRepositoryBase: 'https://github.com/birkir/react-native-carplay/tree/master/apps/docs',
13 | footer: {
14 | text: 'React Native for Apple CarPlay and Android Auto',
15 | },
16 | sidebar: {
17 | titleComponent({ title, type, route }) {
18 | const match = route.match(/^\/docs\/(.*)/);
19 | if (match) {
20 | title = match[1];
21 | }
22 | if (type === 'separator') {
23 | return {title} ;
24 | }
25 | return <>{title}>;
26 | },
27 | defaultMenuCollapseLevel: 1,
28 | toggleButton: true,
29 | },
30 | };
31 |
32 | export default config;
33 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/gradle.properties:
--------------------------------------------------------------------------------
1 | ## For more details on how to configure your build environment visit
2 | # http://www.gradle.org/docs/current/userguide/build_environment.html
3 | #
4 | # Specifies the JVM arguments used for the daemon process.
5 | # The setting is particularly useful for tweaking memory settings.
6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
7 | org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8 | #
9 | # When configured, Gradle will run in incubating parallel mode.
10 | # This option should only be used with decoupled projects. More details, visit
11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
12 | # org.gradle.parallel=true
13 | #Tue Oct 17 11:05:13 GMT 2023
14 | Carplay_targetSdkVersion=31
15 | Carplay_kotlinVersion=1.7.0
16 | Carplay_compileSdkVersion=31
17 | Carplay_minSdkVersion=21
18 | Carplay_ndkversion=21.4.7075529
19 | android.useAndroidX=true
20 | android.enableJetifier=true
21 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/interfaces/BarButton.ts:
--------------------------------------------------------------------------------
1 | import { ImageSourcePropType } from 'react-native';
2 |
3 | interface BarButtonBase {
4 | /**
5 | * Button ID
6 | */
7 | id: string;
8 | /**
9 | * A Boolean value that enables and disables the bar button.
10 | */
11 | disabled?: boolean;
12 | }
13 |
14 | export interface BarButtonText extends BarButtonBase {
15 | /**
16 | * A text style bar button.
17 | */
18 | type: 'text';
19 | /**
20 | * The title displayed on the button.
21 | */
22 | title: string;
23 | }
24 |
25 | export interface BarButtonImage extends BarButtonBase {
26 | /**
27 | * An image style bar button.
28 | */
29 | type: 'image';
30 | /**
31 | * The image displayed on the button.
32 | *
33 | * If you provide an animated image, the button displays only the first image in the animation sequence.
34 | */
35 | image: ImageSourcePropType;
36 | }
37 |
38 | /**
39 | * A button in a navigation bar.
40 | */
41 | export type BarButton = BarButtonImage | BarButtonText;
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots/Videos**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **CarPlay (please complete the following information):**
27 | - Device: [e.g. iPhone 13 / Simulator]
28 | - OS version [e.g. iOS 16]
29 | - RNCarPlay version [e.g. 2.4.0]
30 |
31 | **Android Auto (please complete the following information):**
32 | - Device: [e.g. Pixel 8 / emulator]
33 | - Android Version: [e.g. iOS8.1]
34 | - RNCarPlay version [e.g. 2.4.0]
35 |
36 | **Additional context**
37 | Add any other context about the problem here.
38 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/java/org/birkir/carplay/parser/RCTGridTemplate.kt:
--------------------------------------------------------------------------------
1 | package org.birkir.carplay.parser
2 |
3 | import androidx.car.app.CarContext
4 | import androidx.car.app.model.GridTemplate
5 | import com.facebook.react.bridge.ReadableMap
6 | import org.birkir.carplay.screens.CarScreenContext
7 |
8 | class RCTGridTemplate(
9 | context: CarContext,
10 | carScreenContext: CarScreenContext
11 | ) : RCTTemplate(context, carScreenContext) {
12 |
13 | override fun parse(props: ReadableMap): GridTemplate {
14 | return GridTemplate.Builder().apply {
15 | setLoading(props.isLoading())
16 | props.getString("title")?.let { setTitle(it) }
17 | props.getMap("headerAction")?.let { setHeaderAction(parseAction(it)) }
18 | props.getArray("actions")?.let { setActionStrip(parseActionStrip(it)) }
19 | this.setSingleList(
20 | parseItemList(props.getArray("buttons"), "grid")
21 | )
22 | }.build()
23 | }
24 |
25 | companion object {
26 | const val TAG = "RCTGridTemplate"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/example/src/screens/Contact.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Text, View } from 'react-native';
3 | import { CarPlay, ContactTemplate } from 'react-native-carplay';
4 |
5 | export function Contact() {
6 | useEffect(() => {
7 | const contactTemplate = new ContactTemplate({
8 | name: 'Birkir',
9 | subtitle: 'Rafn',
10 | image: require('../images/cat.jpg'),
11 | actions: [
12 | {
13 | id: 'foo',
14 | phoneOrEmail: '6184900',
15 | type: 'message',
16 | },
17 | {
18 | id: 'bar',
19 | type: 'call',
20 | },
21 | {
22 | id: 'baz',
23 | type: 'directions',
24 | },
25 | ],
26 | });
27 | CarPlay.pushTemplate(contactTemplate);
28 |
29 | return () => {};
30 | }, []);
31 |
32 | return (
33 |
34 | Contact
35 |
36 | );
37 | }
38 |
39 | Contact.navigationOptions = {
40 | headerTitle: 'Contact Template',
41 | };
42 |
--------------------------------------------------------------------------------
/apps/docs/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Shu Ding
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/PointOfInterestTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Template, TemplateConfig } from './Template';
2 |
3 | export interface PointOfInterestItem {
4 | id: string;
5 | location: {
6 | latitude: number;
7 | longitude: number;
8 | };
9 | title: string;
10 | subtitle?: string;
11 | summary?: string;
12 | detailTitle?: string;
13 | detailSubtitle?: string;
14 | detailSummary?: string;
15 | }
16 |
17 | export interface PointOfInterestTemplateConfig extends TemplateConfig {
18 | title: string;
19 | items: PointOfInterestItem[];
20 | onPointOfInterestSelect?(e: PointOfInterestItem): void;
21 | onChangeMapRegion(e: {
22 | latitude: number;
23 | longitude: number;
24 | latitudeDelta: number;
25 | longitudeDelta: number;
26 | }): void;
27 | }
28 |
29 | export class PointOfInterestTemplate extends Template {
30 | public get type(): string {
31 | return 'poi';
32 | }
33 |
34 | get eventMap() {
35 | return {
36 | didSelectPointOfInterest: 'onPointOfInterestSelect',
37 | didChangeMapRegion: 'onChangeMapRegion',
38 | };
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/apps/example/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = "33.0.0"
6 | minSdkVersion = 29
7 | compileSdkVersion = 33
8 | targetSdkVersion = 33
9 | kotlinVersion = "1.7.20"
10 |
11 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
12 | ndkVersion = "23.1.7779620"
13 | }
14 | repositories {
15 | google()
16 | mavenCentral()
17 | }
18 | dependencies {
19 | classpath("com.android.tools.build:gradle:7.3.1")
20 | classpath("com.facebook.react:react-native-gradle-plugin")
21 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
22 | }
23 | }
24 |
25 | allprojects {
26 | project.pluginManager.withPlugin("com.facebook.react") {
27 | react {
28 | reactNativeDir = rootProject.file("../../../node_modules/react-native/")
29 | codegenDir = rootProject.file("../../../node_modules/react-native-codegen/")
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/apps/example/ios/RNCarPlayScene/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@carplay/docs",
3 | "version": "0.0.1",
4 | "description": "Nextra docs template",
5 | "scripts": {
6 | "dev": "next dev",
7 | "export": "next export",
8 | "build": "yarn build:typedoc && next build",
9 | "start": "next start",
10 | "build:typedoc": "./scripts/prepare.sh && typedoc && ./scripts/cleanup.sh"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/shuding/nextra-docs-template.git"
15 | },
16 | "author": "Shu Ding ",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/shuding/nextra-docs-template/issues"
20 | },
21 | "homepage": "https://github.com/shuding/nextra-docs-template#readme",
22 | "dependencies": {
23 | "next": "^13.0.6",
24 | "nextra": "latest",
25 | "nextra-theme-docs": "latest",
26 | "react": "^18.2.0",
27 | "react-dom": "^18.2.0"
28 | },
29 | "devDependencies": {
30 | "@types/node": "18.11.10",
31 | "typedoc": "^0.25.3",
32 | "typedoc-github-wiki-theme": "^1.1.0",
33 | "typedoc-plugin-markdown": "^3.16.0",
34 | "typescript": "^4.9.3"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | Copyright © `2019` `Birkir Rafn Guðjónsson`
4 |
5 | Permission is hereby granted, free of charge, to any person
6 | obtaining a copy of this software and associated documentation
7 | files (the “Software”), to deal in the Software without
8 | restriction, including without limitation the rights to use,
9 | copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the
11 | Software is furnished to do so, subject to the following
12 | conditions:
13 |
14 | The above copyright notice and this permission notice shall be
15 | included in all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | OTHER DEALINGS IN THE SOFTWARE.
25 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/GridTemplate.ts:
--------------------------------------------------------------------------------
1 | import { GridButton } from '../interfaces/GridButton';
2 | import { BaseEvent, Template, TemplateConfig } from './Template';
3 |
4 | export interface ButtonPressedEvent extends BaseEvent {
5 | /**
6 | * Button ID
7 | */
8 | id: string;
9 | /**
10 | * Button Index
11 | */
12 | index: number;
13 | /**
14 | * template ID
15 | */
16 | templateId: string;
17 | }
18 |
19 | export interface GridTemplateConfig extends TemplateConfig {
20 | /**
21 | * The title displayed in the navigation bar while the list template is visible.
22 | */
23 | title?: string;
24 | /**
25 | * The array of grid buttons displayed on the template.
26 | */
27 | buttons: GridButton[];
28 | /**
29 | * Fired when a button is pressed
30 | */
31 | onButtonPressed?(e: ButtonPressedEvent): void;
32 | }
33 |
34 | export class GridTemplate extends Template {
35 | public get type(): string {
36 | return 'grid';
37 | }
38 |
39 | get eventMap() {
40 | return {
41 | gridButtonPressed: 'onButtonPressed',
42 | backButtonPressed: 'onBackButtonPressed',
43 | };
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/NowPlayingTemplate.ts:
--------------------------------------------------------------------------------
1 | import { ImageSourcePropType } from 'react-native';
2 | import { Template, TemplateConfig } from './Template';
3 |
4 | export type NowPlayingButton = {
5 | id: string;
6 | } & (
7 | | {
8 | type: 'shuffle' | 'add-to-library' | 'more' | 'playback' | 'repeat';
9 | }
10 | | {
11 | type: 'image';
12 | image: ImageSourcePropType;
13 | }
14 | );
15 |
16 | export interface NowPlayingTemplateConfig extends TemplateConfig {
17 | albumArtistButtonEnabled?: boolean;
18 | upNextButtonTitle?: string;
19 | upNextButtonEnabled?: boolean;
20 | onAlbumArtistButtonPressed?(): void;
21 | onUpNextButtonPressed?(): void;
22 | onButtonPressed?(e: { id: string; templateId: string }): void;
23 | buttons?: NowPlayingButton[];
24 | }
25 |
26 | export class NowPlayingTemplate extends Template {
27 | public get type(): string {
28 | return 'nowplaying';
29 | }
30 |
31 | get eventMap() {
32 | return {
33 | albumArtistButtonPressed: 'onAlbumArtistButtonPressed',
34 | upNextButtonPressed: 'onUpNextButtonPressed',
35 | buttonPressed: 'onButtonPressed',
36 | };
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/apps/example/src/screens/ActionSheet.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Text, View } from 'react-native';
3 | import { CarPlay, ActionSheetTemplate } from 'react-native-carplay';
4 |
5 | export function ActionSheet({ navigation }) {
6 | useEffect(() => {
7 | const template = new ActionSheetTemplate({
8 | title: 'Example',
9 | message: 'This is an message for you',
10 | actions: [
11 | {
12 | id: 'ok',
13 | title: 'Ok',
14 | },
15 | {
16 | id: 'cancel',
17 | title: 'Cancel',
18 | style: 'cancel',
19 | },
20 | {
21 | id: 'remove',
22 | title: 'Remove',
23 | style: 'destructive',
24 | },
25 | ],
26 | onActionButtonPressed(e) {
27 | navigation.navigate('Menu');
28 | },
29 | });
30 | CarPlay.presentTemplate(template);
31 | return () => {};
32 | }, []);
33 |
34 | return (
35 |
36 | Action Sheet
37 |
38 | );
39 | }
40 |
41 | ActionSheet.navigationOptions = {
42 | headerTitle: 'Action Sheet Template',
43 | };
44 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/java/org/birkir/carplay/parser/RCTMessageTemplate.kt:
--------------------------------------------------------------------------------
1 | package org.birkir.carplay.parser
2 |
3 | import androidx.car.app.CarContext
4 | import androidx.car.app.model.MessageTemplate
5 | import com.facebook.react.bridge.ReadableMap
6 | import org.birkir.carplay.screens.CarScreenContext
7 |
8 | class RCTMessageTemplate(
9 | context: CarContext,
10 | carScreenContext: CarScreenContext
11 | ) : RCTTemplate(context, carScreenContext) {
12 | override fun parse(props: ReadableMap): MessageTemplate {
13 | val message = props.getString("message") ?: "No message"
14 | val messageText = parseCarText(message, props)
15 | return MessageTemplate.Builder(messageText).apply {
16 | props.getArray("actions")?.let { setActionStrip(parseActionStrip(it)) }
17 | props.getMap("headerAction")?.let { setHeaderAction(parseAction(it)) }
18 | props.getMap("icon")?.let { setIcon(parseCarIcon(it)) }
19 | props.getString("title")?.let { setTitle(it) }
20 | props.getString("debugMessage")?.let { setDebugMessage(it) }
21 | setLoading(props.isLoading())
22 | }.build()
23 | }
24 |
25 | companion object {
26 | const val TAG = "RCTMessageTemplate"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/example/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | ios/.xcode.env.local
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 | *.hprof
33 | .cxx/
34 | *.keystore
35 | !debug.keystore
36 |
37 | # node.js
38 | #
39 | node_modules/
40 | npm-debug.log
41 | yarn-error.log
42 |
43 | # fastlane
44 | #
45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
46 | # screenshots whenever they are needed.
47 | # For more information about the recommended setup visit:
48 | # https://docs.fastlane.tools/best-practices/source-control/
49 |
50 | **/fastlane/report.xml
51 | **/fastlane/Preview.html
52 | **/fastlane/screenshots
53 | **/fastlane/test_output
54 |
55 | # Bundle artifact
56 | *.jsbundle
57 |
58 | # Ruby / CocoaPods
59 | /ios/Pods/
60 | /vendor/bundle/
61 |
62 | # Temporary files created by Metro to check the health of the file watcher
63 | .metro-health-check*
--------------------------------------------------------------------------------
/apps/example/src/screens/List.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Text, View } from 'react-native';
3 | import { CarPlay, ListTemplate } from 'react-native-carplay';
4 |
5 | const sections = Array.from({ length: 26 }).map((_, i) => ({
6 | header: `Header ${String.fromCharCode(97 + i).toLocaleUpperCase()}`,
7 | items: Array.from({ length: 3 }).map((_, j) => ({
8 | text: `Item ${j + 1}`,
9 | })),
10 | sectionIndexTitle: String.fromCharCode(97 + i).toLocaleUpperCase(),
11 | }));
12 |
13 | export function List() {
14 | const [selected, setSelected] = useState();
15 |
16 | useEffect(() => {
17 | const listTemplate = new ListTemplate({
18 | sections,
19 | title: 'List Template',
20 | async onItemSelect(e) {
21 | const { index } = e;
22 | setSelected(index);
23 | },
24 | });
25 |
26 | CarPlay.pushTemplate(listTemplate, true);
27 |
28 | return () => CarPlay.popToRootTemplate(true);
29 | }, []);
30 |
31 | return (
32 |
33 | SELECTED: {selected}
34 |
35 | );
36 | }
37 |
38 | List.navigationOptions = {
39 | headerTitle: 'List Template',
40 | };
41 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/ContactTemplate.ts:
--------------------------------------------------------------------------------
1 | import { ImageSourcePropType } from 'react-native';
2 | import { Template, TemplateConfig } from './Template';
3 |
4 | export interface ContactButtonEvent {
5 | id: string;
6 | templateId: string;
7 | }
8 |
9 | export interface ContactActionBase {
10 | id: string;
11 | type: 'call' | 'directions' | 'message';
12 | disabled?: boolean;
13 | title?: string;
14 | }
15 |
16 | export interface ContactActionMessage extends ContactActionBase {
17 | type: 'message';
18 | phoneOrEmail: string;
19 | }
20 |
21 | export type ContactAction = ContactActionBase | ContactActionMessage;
22 |
23 | export interface ContactTemplateConfig extends TemplateConfig {
24 | name: string;
25 | image: ImageSourcePropType;
26 | subtitle?: string;
27 | informativeText?: string;
28 | actions?: ContactAction[];
29 | /**
30 | * Fired when bar button is pressed
31 | * @param e Event
32 | */
33 | onButtonPressed?(e: ContactButtonEvent): void;
34 | }
35 |
36 | export class ContactTemplate extends Template {
37 | public get type(): string {
38 | return 'contact';
39 | }
40 | get eventMap() {
41 | return {
42 | buttonPressed: 'onButtonPressed',
43 | };
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/interfaces/NavigationAlert.ts:
--------------------------------------------------------------------------------
1 | import { ImageSourcePropType } from 'react-native';
2 |
3 | export enum NavigationAlertActionStyle {
4 | Default = 0,
5 | Cancel = 1,
6 | Destructive = 2,
7 | }
8 |
9 | export interface NavigationAlertAction {
10 | /**
11 | * The action button's title.
12 | */
13 | title: string;
14 | /**
15 | * The display style for the action button.
16 | */
17 | style?: NavigationAlertActionStyle;
18 | }
19 |
20 | /**
21 | * An alert panel that displays map or navigation related information to the user.
22 | */
23 | export interface NavigationAlert {
24 | lightImage?: ImageSourcePropType;
25 | darkImage?: ImageSourcePropType;
26 | /**
27 | * An array of title strings.
28 | */
29 | titleVariants: string[];
30 | /**
31 | * An array of subtitle strings.
32 | */
33 | subtitleVariants?: string[];
34 | /**
35 | * The primary action, and button, for the navigation alert.
36 | */
37 | primaryAction: NavigationAlertAction;
38 | /**
39 | * An optional, secondary action (and button) for the navigation alert.
40 | */
41 | secondaryAction?: NavigationAlertAction;
42 | /**
43 | * The amount of time, in seconds, that the alert is visible.
44 | */
45 | duration: number;
46 | }
47 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/interfaces/Maneuver.ts:
--------------------------------------------------------------------------------
1 | import { TravelEstimates } from './TravelEstimates';
2 | import { ColorValue, ImageSourcePropType, ProcessedColorValue } from 'react-native';
3 |
4 | /**
5 | * Navigation instructions and distance from the previous maneuver.
6 | */
7 | export interface Maneuver {
8 | junctionImage?: ImageSourcePropType;
9 | initialTravelEstimates?: TravelEstimates;
10 | symbolImage?: ImageSourcePropType;
11 | /**
12 | * The size of the image in points. Please read the CarPlay App Programming Guide
13 | * to get the recommended size.
14 | */
15 | symbolImageSize?: { width: number; height: number };
16 | /**
17 | * Allows the supplied symbol image to be tinted
18 | * via a color, ie. 'red'. This functionality would usually
19 | * be available via the `` tag but carplay requires
20 | * an image asset to this tinting is done on the native side.
21 | * If a string is supplied, it will be passed to `processColor`.
22 | * You may also use `processColor` yourself.
23 | */
24 | tintSymbolImage?: null | number | ColorValue | ProcessedColorValue;
25 | instructionVariants: string[];
26 |
27 | // not yet implemented
28 | dashboardInstructionVariants?: string[];
29 | notificationInstructionVariants?: string[];
30 | }
31 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/AndroidManifestNew.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/java/org/birkir/carplay/CarPlayService.kt:
--------------------------------------------------------------------------------
1 | package org.birkir.carplay
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.IntentFilter
7 | import android.util.Log
8 | import androidx.car.app.CarAppService
9 | import androidx.car.app.Session
10 | import androidx.car.app.SessionInfo
11 | import androidx.car.app.validation.HostValidator
12 | import com.facebook.react.ReactApplication
13 | import com.facebook.react.ReactInstanceManager
14 |
15 | class CarPlayService : CarAppService() {
16 | private lateinit var reactInstanceManager: ReactInstanceManager
17 | override fun onCreate() {
18 | super.onCreate()
19 | reactInstanceManager =
20 | (application as ReactApplication).reactNativeHost.reactInstanceManager
21 | }
22 |
23 | override fun createHostValidator(): HostValidator {
24 | return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
25 | }
26 |
27 | override fun onCreateSession(sessionInfo: SessionInfo): Session {
28 | Log.d(TAG, "onCreateSession: sessionId = ${sessionInfo.sessionId}, display = ${sessionInfo.displayType}")
29 | return CarPlaySession(reactInstanceManager)
30 | }
31 |
32 | companion object {
33 | var TAG = "CarPlayService"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/apps/example/src/screens/TabBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Text, View } from 'react-native';
3 | import { CarPlay, ListTemplate, TabBarTemplate } from 'react-native-carplay';
4 |
5 | export function TabBar() {
6 | useEffect(() => {
7 | const template1 = new ListTemplate({
8 | sections: [
9 | {
10 | header: 'Test 1',
11 | items: [{ text: 'Hello world 1' }, { text: 'Hello world 2' }],
12 | },
13 | ],
14 | title: 'AA',
15 | });
16 | const template2 = new ListTemplate({
17 | sections: [
18 | {
19 | header: 'Test 2',
20 | items: [{ text: 'Hello world 3' }, { text: 'Hello world 4' }],
21 | },
22 | ],
23 | title: 'BB',
24 | });
25 |
26 | const tabBarTemplate = new TabBarTemplate({
27 | templates: [template1, template2],
28 | onTemplateSelect(e: any) {
29 | console.log('selected', e);
30 | },
31 | });
32 |
33 | CarPlay.setRootTemplate(tabBarTemplate);
34 |
35 | return () => {};
36 | }, []);
37 |
38 | return (
39 |
40 | TabBar
41 |
42 | );
43 | }
44 |
45 | TabBar.navigationOptions = {
46 | headerTitle: 'TabBar Template',
47 | };
48 |
--------------------------------------------------------------------------------
/apps/example/src/screens/NowPlaying.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from 'react';
2 | import {Text, View} from 'react-native';
3 | import {CarPlay, NowPlayingTemplate} from 'react-native-carplay';
4 |
5 | export function NowPlaying() {
6 | useEffect(() => {
7 | const template = new NowPlayingTemplate({
8 | buttons: [
9 | {
10 | id: 'foo',
11 | type: 'more',
12 | },
13 | {
14 | id: 'demo',
15 | type: 'playback',
16 | },
17 | {
18 | id: 'baz',
19 | type: 'image',
20 | image: require('../images/star.png'),
21 | },
22 | ],
23 | albumArtistButtonEnabled: true,
24 | upNextButtonTitle:"Tester",
25 | upNextButtonEnabled: false,
26 | onUpNextButtonPressed() {
27 | console.log('up next was pressed');
28 | },
29 | onButtonPressed(e) {
30 | console.log(e);
31 | },
32 | });
33 | CarPlay.enableNowPlaying(true);
34 | CarPlay.pushTemplate(template);
35 | return () => {};
36 | }, []);
37 |
38 | return (
39 |
40 | Now Playing
41 |
42 | );
43 | }
44 |
45 | NowPlaying.navigationOptions = {
46 | headerTitle: 'Now Playing Template',
47 | };
48 |
--------------------------------------------------------------------------------
/monorepo.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "name": "🌳 root",
5 | "path": "."
6 | },
7 | {
8 | "name": "🍂 example",
9 | "path": "apps/example"
10 | },
11 | {
12 | "name": "📄 docs",
13 | "path": "apps/docs"
14 | },
15 | {
16 | "name": "📦 react-native-carplay",
17 | "path": "packages/react-native-carplay"
18 | },
19 | ],
20 | "extensions": {
21 | "recommendations": [
22 | "dbaeumer.vscode-eslint",
23 | "esbenp.prettier-vscode"
24 | ]
25 | },
26 | "settings": {
27 | "typescript.tsdk": "node_modules/typescript/lib",
28 | "typescript.enablePromptUseWorkspaceTsdk": true,
29 | "editor.formatOnSave": true,
30 | "editor.defaultFormatter": "esbenp.prettier-vscode",
31 | "editor.codeActionsOnSave": {
32 | "source.fixAll.eslint": true
33 | },
34 | // Disable vscode formatting for js,jsx,ts,tsx files
35 | // to allow dbaeumer.vscode-eslint to format them
36 | "[javascript]": {
37 | "editor.formatOnSave": false
38 | },
39 | "eslint.alwaysShowStatus": true,
40 | // https://github.com/Microsoft/vscode-eslint#mono-repository-setup
41 | "eslint.workingDirectories": [
42 | "./apps/example",
43 | "./apps/docs",
44 | "./packages/react-native-carplay"
45 | ]
46 | }
47 | }
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/InformationTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Template, TemplateConfig } from './Template';
2 | import { CarPlay } from '../CarPlay';
3 |
4 | export interface InformationItem {
5 | title: string;
6 | detail: string;
7 | }
8 |
9 | export interface InformationAction {
10 | id: string;
11 | title: string;
12 | }
13 |
14 | export interface InformationTemplateConfig extends TemplateConfig {
15 | title: string;
16 | leading?: boolean;
17 | items: InformationItem[];
18 | actions: InformationAction[];
19 | onActionButtonPressed(e: { id: string; templateId: string }): void;
20 | }
21 |
22 | export class InformationTemplate extends Template {
23 | public get type(): string {
24 | return 'information';
25 | }
26 |
27 | get eventMap() {
28 | return {
29 | actionButtonPressed: 'onActionButtonPressed',
30 | };
31 | }
32 |
33 | public updateInformationTemplateItems = (items: InformationItem[]) => {
34 | this.config.items = items;
35 | return CarPlay.bridge.updateInformationTemplateItems(this.id, this.parseConfig(items));
36 | };
37 |
38 | public updateInformationTemplateActions = (actions: InformationAction[]) => {
39 | this.config.actions = actions;
40 | return CarPlay.bridge.updateInformationTemplateActions(this.id, this.parseConfig(actions));
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/apps/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@carplay/example",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "lint": "eslint .",
9 | "start": "react-native start",
10 | "test": "jest"
11 | },
12 | "dependencies": {
13 | "@react-navigation/native": "^6.1.6",
14 | "@react-navigation/stack": "^6.3.16",
15 | "react": "18.2.0",
16 | "react-native": "0.71.13",
17 | "react-native-carplay": "workspace:*",
18 | "react-native-gesture-handler": "^2.10.1",
19 | "react-native-safe-area-context": "^4.5.3",
20 | "react-native-screens": "^3.20.0"
21 | },
22 | "devDependencies": {
23 | "@babel/core": "^7.20.0",
24 | "@babel/preset-env": "^7.20.0",
25 | "@babel/runtime": "^7.20.0",
26 | "@react-native-community/eslint-config": "^3.2.0",
27 | "@tsconfig/react-native": "^2.0.2",
28 | "@types/jest": "^29.2.1",
29 | "@types/react": "^18.0.24",
30 | "@types/react-test-renderer": "^18.0.0",
31 | "babel-jest": "^29.2.1",
32 | "eslint": "^8.19.0",
33 | "jest": "^29.2.1",
34 | "metro-react-native-babel-preset": "0.73.10",
35 | "prettier": "2.6.2",
36 | "react-test-renderer": "18.2.0",
37 | "typescript": "4.8.4"
38 | },
39 | "jest": {
40 | "preset": "react-native"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/apps/example/src/screens/Alert.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Text, View } from 'react-native';
3 | import { CarPlay, AlertTemplate } from 'react-native-carplay';
4 |
5 | export function Alert() {
6 | const [buttonClicked, setButtonClicked] = useState();
7 |
8 | useEffect(() => {
9 | const template = new AlertTemplate({
10 | titleVariants: ['Hello world', 'Mega stuff'],
11 | actions: [
12 | {
13 | id: 'ok',
14 | title: 'Ok',
15 | },
16 | {
17 | id: 'cancel',
18 | title: 'Cancel',
19 | style: 'cancel',
20 | },
21 | {
22 | id: 'remove',
23 | title: 'Remove',
24 | style: 'destructive',
25 | },
26 | ],
27 | onActionButtonPressed({ id }) {
28 | setButtonClicked(id);
29 | if (id === 'remove') {
30 | CarPlay.dismissTemplate();
31 | }
32 | },
33 | });
34 | CarPlay.presentTemplate(template);
35 | return () => {
36 | CarPlay.dismissTemplate();
37 | };
38 | }, []);
39 |
40 | return (
41 |
42 | Alert
43 | {`Clicked button: ${buttonClicked}`}
44 |
45 | );
46 | }
47 |
48 | Alert.navigationOptions = {
49 | headerTitle: 'Alert Template',
50 | };
51 |
--------------------------------------------------------------------------------
/apps/example/src/screens/Search.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Text, View } from 'react-native';
3 | import { CarPlay, SearchTemplate } from 'react-native-carplay';
4 |
5 | export function Search({ navigation }) {
6 | const [query, setQuery] = useState(null);
7 | const [selected, setSelected] = useState(null);
8 |
9 | useEffect(() => {
10 | const searchTemplate = new SearchTemplate({
11 | async onSearch(q: string) {
12 | setQuery(q);
13 | return (
14 | q && [
15 | {
16 | text: q,
17 | image: require('../images/item.png'),
18 | },
19 | ]
20 | );
21 | },
22 | async onItemSelect(e) {
23 | setSelected(e.index);
24 | },
25 | onSearchButtonPressed() {
26 | // on search button pressed, should display
27 | // list template with results
28 | navigation.navigate('List');
29 | },
30 | });
31 |
32 | CarPlay.pushTemplate(searchTemplate, true);
33 |
34 | return () => CarPlay.popToRootTemplate(true);
35 | }, []);
36 |
37 | return (
38 |
39 | Query: {query}
40 | Selected Index: {selected}
41 |
42 | );
43 | }
44 |
45 | Search.navigationOptions = {
46 | headerTitle: 'Search Template',
47 | };
48 |
--------------------------------------------------------------------------------
/apps/example/src/screens/Information.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Text, View } from 'react-native';
3 | import { CarPlay, InformationTemplate } from 'react-native-carplay';
4 |
5 | export function Information() {
6 | useEffect(() => {
7 | const template = new InformationTemplate({
8 | title: 'Information',
9 | items: Array.from({ length: 30 }).fill({ title: 'foo', detail: 'bar' }),
10 | actions: [{ id: 'u', title: 'Update List' }, { id: 'r', title: 'Random #:' }],
11 | onActionButtonPressed(action) {
12 | console.log('pressed', action);
13 | if (action.id == 'u') {
14 | const numOfItems = Math.floor(Math.random() * 6);
15 | template.updateInformationTemplateItems(Array.from({ length: numOfItems }).fill({ title: 'foo', detail: 'bar' }));
16 | }
17 | else if (action.id == 'r') {
18 | template.updateInformationTemplateActions([{ id: 'u', title: 'Update List' }, { id: 'r', title: 'Random #:' + Math.floor(Math.random() * 100) }]);
19 | }
20 | },
21 | });
22 |
23 | CarPlay.pushTemplate(template);
24 | return () => {};
25 | }, []);
26 |
27 | return (
28 |
29 | Information
30 |
31 | );
32 | }
33 |
34 | Information.navigationOptions = {
35 | headerTitle: 'Information Template',
36 | };
37 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/ios/RNCarPlay.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import
4 | #import
5 | #import "RCTConvert+RNCarPlay.h"
6 | #import "RNCPStore.h"
7 |
8 | typedef void(^SearchResultUpdateBlock)(NSArray * _Nonnull);
9 | typedef void(^SelectedResultBlock)(void);
10 |
11 | @interface RNCarPlay : RCTEventEmitter {
12 | CPInterfaceController *interfaceController;
13 | CPWindow *window;
14 | SearchResultUpdateBlock searchResultBlock;
15 | SelectedResultBlock selectedResultBlock;
16 | BOOL isNowPlayingActive;
17 | }
18 |
19 | @property (nonatomic, retain) CPInterfaceController *interfaceController;
20 | @property (nonatomic, retain) CPWindow *window;
21 | @property (nonatomic, copy) SearchResultUpdateBlock searchResultBlock;
22 | @property (nonatomic, copy) SelectedResultBlock selectedResultBlock;
23 | @property (nonatomic) BOOL isNowPlayingActive;
24 |
25 | + (void) connectWithInterfaceController:(CPInterfaceController*)interfaceController window:(CPWindow*)window;
26 | + (void) disconnect;
27 | - (NSArray*) parseSections:(NSArray*)sections templateId:(NSString *)templateId;
28 |
29 | @end
30 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/interfaces/Pane.ts:
--------------------------------------------------------------------------------
1 | import { ImageSourcePropType } from 'react-native';
2 | import { Action } from './Action';
3 | import { ListItem } from './ListItem';
4 |
5 | /**
6 | * Represents a list of rows used for displaying informational content and a set of Actions that users can perform based on such content.
7 | * @namespace Android
8 | */
9 | export interface Pane {
10 | /**
11 | * Sets whether the Pane is in a loading state.
12 | * If set to true, the UI will display a loading indicator where the list content would be otherwise. The caller is expected to call invalidate and send the new template content to the host once the data is ready. If set to false, the UI shows the actual row contents.
13 | */
14 | loading?: boolean;
15 | /**
16 | * Sets an CarIcon to display alongside the rows in the pane.
17 | * Image Sizing Guidance To minimize scaling artifacts across a wide range of car screens, apps should provide images targeting a 480 x 480 dp bounding box. If the image exceeds this maximum size in either one of the dimensions, it will be scaled down to be centered inside the bounding box while preserving its aspect ratio.
18 | */
19 | image?: ImageSourcePropType;
20 | /**
21 | * Actions to display alongside the rows in the pane.
22 | * By default, no actions are displayed.
23 | */
24 | actions?: Action[];
25 | /**
26 | * Rows to display in the list.
27 | */
28 | items?: ListItem[];
29 | }
30 |
--------------------------------------------------------------------------------
/apps/example/src/screens/Grid.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Text, View } from 'react-native';
3 | import { CarPlay, GridTemplate } from 'react-native-carplay';
4 |
5 | export function Grid() {
6 | const [selected, setSelected] = useState(null);
7 |
8 | useEffect(() => {
9 | const gridTemplate = new GridTemplate({
10 | trailingNavigationBarButtons: [
11 | {
12 | id: 'LEAD_2',
13 | type: 'image',
14 | image: require('../images/star.png'),
15 | },
16 | ],
17 | buttons: Array.from({ length: 8 }).map((_, i) => ({
18 | id: `BUTTON_${i}`,
19 | image: require('../images/click.png'),
20 | titleVariants: [`Item ${i}`],
21 | })),
22 | title: 'Grid Template',
23 | onButtonPressed(e) {
24 | setSelected(e.id);
25 | },
26 | onBarButtonPressed(e) {
27 | setSelected(e.id);
28 | },
29 | });
30 |
31 | CarPlay.pushTemplate(gridTemplate, true);
32 | // CarPlay.setRootTemplate(gridTemplate);
33 |
34 | // return () => { console.log('running') };
35 |
36 | return () => {
37 | CarPlay.popToRootTemplate(true);
38 | };
39 | }, []);
40 |
41 | return (
42 |
43 | SELECTED: {selected}
44 |
45 | );
46 | }
47 |
48 | Grid.navigationOptions = {
49 | headerTitle: 'Grid Template',
50 | };
51 |
--------------------------------------------------------------------------------
/apps/example/android/app/src/main/java/com/rncarplayscene/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.rncarplayscene;
2 |
3 | import com.facebook.react.ReactActivity;
4 | import com.facebook.react.ReactActivityDelegate;
5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
6 | import com.facebook.react.defaults.DefaultReactActivityDelegate;
7 |
8 | public class MainActivity extends ReactActivity {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | @Override
15 | protected String getMainComponentName() {
16 | return "RNCarPlayScene";
17 | }
18 |
19 | /**
20 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
21 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
22 | * (aka React 18) with two boolean flags.
23 | */
24 | @Override
25 | protected ReactActivityDelegate createReactActivityDelegate() {
26 | return new DefaultReactActivityDelegate(
27 | this,
28 | getMainComponentName(),
29 | // If you opted-in for the New Architecture, we enable the Fabric Renderer.
30 | DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
31 | // If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
32 | DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/android/AndroidNavigationBaseTemplate.ts:
--------------------------------------------------------------------------------
1 | import { AppRegistry, Platform } from 'react-native';
2 | import { Template, TemplateConfig } from '../Template';
3 | import { CarPlay } from '../../CarPlay';
4 |
5 | export interface AndroidNavigationBaseTemplateConfig extends TemplateConfig {
6 | /**
7 | * Your component to render inside Android Auto Map view
8 | * Example `component: MyComponent`
9 | */
10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 | component: React.ComponentType;
12 |
13 | onDidShowPanningInterface?(): void;
14 | onDidDismissPanningInterface?(): void;
15 | }
16 |
17 | export class AndroidNavigationBaseTemplate<
18 | T extends AndroidNavigationBaseTemplateConfig,
19 | > extends Template {
20 | get eventMap() {
21 | return {
22 | didShowPanningInterface: 'onDidShowPanningInterface',
23 | didDismissPanningInterface: 'onDidDismissPanningInterface',
24 | };
25 | }
26 |
27 | constructor(public config: T) {
28 | super(config);
29 |
30 | if (config.component) {
31 | AppRegistry.registerComponent(this.id, () => config.component);
32 | }
33 |
34 | const callbackFn = Platform.select({
35 | android: ({ error }: { error?: string } = {}) => {
36 | error && console.error(error);
37 | },
38 | });
39 |
40 | CarPlay.bridge.createTemplate(
41 | this.id,
42 | this.parseConfig({ type: this.type, ...config, render: true }),
43 | callbackFn,
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/java/org/birkir/carplay/parser/TemplateParser.kt:
--------------------------------------------------------------------------------
1 | package org.birkir.carplay.parser
2 |
3 | import androidx.car.app.CarContext
4 | import androidx.car.app.model.Pane
5 | import androidx.car.app.model.PaneTemplate
6 | import androidx.car.app.model.Template
7 | import com.facebook.react.bridge.ReadableMap
8 | import org.birkir.carplay.screens.CarScreenContext
9 |
10 | class TemplateParser internal constructor(
11 | private val context: CarContext,
12 | private val carScreenContext: CarScreenContext) {
13 |
14 | fun parse(props: ReadableMap): Template {
15 | val template = when (props.getString("type")) {
16 | "list" -> RCTListTemplate(context, carScreenContext)
17 | "grid" -> RCTGridTemplate(context, carScreenContext)
18 | "map" -> RCTMapTemplate(context, carScreenContext)
19 | "navigation" -> RCTMapTemplate(context, carScreenContext)
20 | "place-list-map" -> RCTMapTemplate(context, carScreenContext)
21 | "place-list-navigation" -> RCTMapTemplate(context, carScreenContext)
22 | "route-preview" -> RCTMapTemplate(context, carScreenContext)
23 | "pane" -> RCTPaneTemplate(context, carScreenContext)
24 | "search" -> RCTSearchTemplate(context, carScreenContext)
25 | "tabbar" -> RCTTabTemplate(context, carScreenContext)
26 | "message" -> RCTMessageTemplate(context, carScreenContext)
27 | else -> null
28 | }
29 |
30 | return template?.parse(props) ?: PaneTemplate
31 | .Builder(
32 | Pane.Builder().setLoading(true).build()
33 | ).setTitle("Template missing").build()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/ios/RNCarPlayViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // RNCarPlayViewController.m
3 | // react-native-carplay
4 | //
5 | // Created by Susan Thapa on 27/02/2024.
6 | //
7 |
8 | #import
9 | #import "RNCarPlayViewController.h"
10 | #import
11 |
12 | @interface RNCarPlayViewController ()
13 |
14 | @property (nonatomic, strong) RCTRootView *rootView;
15 |
16 | @end
17 |
18 | @implementation RNCarPlayViewController
19 |
20 | - (instancetype)initWithRootView:(RCTRootView *)rootView {
21 | self = [super init];
22 | if (self) {
23 | _rootView = rootView;
24 | }
25 | return self;
26 | }
27 |
28 | - (void)viewDidLoad {
29 | [super viewDidLoad];
30 | self.view.translatesAutoresizingMaskIntoConstraints = false;
31 | if (self.rootView) {
32 | self.rootView.translatesAutoresizingMaskIntoConstraints = false;
33 | self.rootView.frame = self.view.bounds;
34 | [self.view addSubview:self.rootView];
35 |
36 | [NSLayoutConstraint activateConstraints:@[
37 | [self.rootView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
38 | [self.rootView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
39 | [self.rootView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
40 | [self.rootView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
41 | ]];
42 | }
43 | }
44 |
45 | - (void)viewWillLayoutSubviews {
46 | [super viewWillLayoutSubviews];
47 | self.rootView.frame = self.view.bounds;
48 | }
49 |
50 | @end
51 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/java/org/birkir/carplay/parser/RCTSearchTemplate.kt:
--------------------------------------------------------------------------------
1 | package org.birkir.carplay.parser
2 |
3 | import androidx.car.app.CarContext
4 | import androidx.car.app.model.SearchTemplate
5 | import androidx.car.app.model.SearchTemplate.SearchCallback
6 | import com.facebook.react.bridge.ReadableMap
7 | import org.birkir.carplay.screens.CarScreenContext
8 |
9 |
10 | class RCTSearchTemplate(
11 | context: CarContext,
12 | carScreenContext: CarScreenContext
13 | ) : RCTTemplate(context, carScreenContext) {
14 | override fun parse(props: ReadableMap): SearchTemplate {
15 | return SearchTemplate.Builder(object : SearchCallback {
16 | override fun onSearchTextChanged(searchText: String) {
17 | eventEmitter.updatedSearchText(searchText)
18 | }
19 |
20 | override fun onSearchSubmitted(searchText: String) {
21 | eventEmitter.searchButtonPressed(searchText)
22 | }
23 | }).apply {
24 | props.getArray("actions")?.let { setActionStrip(parseActionStrip(it)) }
25 | props.getMap("headerAction")?.let { setHeaderAction(parseAction(it)) }
26 | props.getString("initialSearchText")?.let {
27 | setInitialSearchText(it)
28 | }
29 | props.getArray("items")?.let { setItemList(parseItemList(it, "row")) }
30 | setLoading(props.isLoading())
31 | props.getString("searchHint")?.let { setSearchHint(it) }
32 | if (props.hasKey("showKeyboardByDefault")) {
33 | setShowKeyboardByDefault(props.getBoolean("showKeyboardByDefault"))
34 | }
35 | }.build()
36 | }
37 |
38 | companion object {
39 | const val TAG = "RCTSearchTemplate"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/apps/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
19 |
22 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.github/workflows/ci-monorepo-integrity.yml:
--------------------------------------------------------------------------------
1 | name: ci/monorepo-integrity
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev
7 | - main
8 | paths:
9 | - 'yarn.lock'
10 | - '.yarnrc.yml'
11 | - '.github/workflows/ci-monorepo-integrity.yml'
12 |
13 | pull_request:
14 | types:
15 | - opened
16 | - synchronize
17 | - reopened
18 | paths:
19 | - 'yarn.lock'
20 | - '.yarnrc.yml'
21 | - '.github/workflows/ci-monorepo-integrity.yml'
22 |
23 | jobs:
24 | test:
25 | runs-on: ubuntu-latest
26 | strategy:
27 | matrix:
28 | node-version: [16.x]
29 | steps:
30 | - uses: actions/checkout@v3
31 |
32 | - name: Use Node.js ${{ matrix.node-version }}
33 | uses: actions/setup-node@v3
34 | with:
35 | node-version: ${{ matrix.node-version }}
36 |
37 | - name: Get yarn cache directory path
38 | id: yarn-cache-dir-path
39 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
40 |
41 | - name: Restore yarn cache
42 | uses: actions/cache@v3
43 | id: yarn-cache
44 | with:
45 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
46 | key: yarn-cache-folder-${{ hashFiles('**/yarn.lock', '.yarnrc.yml') }}
47 | restore-keys: |
48 | yarn-cache-folder-
49 | - name: Install dependencies
50 | run: |
51 | yarn install --immutable
52 | - name: Check for duplicate dependencies in lock file
53 | run: |
54 | yarn dedupe --check
55 | - name: Check for yarn constraints.pro
56 | run: |
57 | yarn constraints
58 | - name: Check monorepo dependency graph
59 | run: |
60 | yarn check:install
61 |
--------------------------------------------------------------------------------
/apps/example/src/templates/menu.template.ts:
--------------------------------------------------------------------------------
1 | import { CarPlay, GridTemplate } from 'react-native-carplay';
2 | import { listTemplate } from './list.template';
3 | import { gridTemplate } from './grid.template';
4 | import { searchTemplate } from './search.template';
5 | import { messageTemplate } from './message.template';
6 | import { paneTemplate } from './pane.template';
7 | import { mapTemplate } from './map.template';
8 | const gridItemImage = require('../images/go.png');
9 |
10 | export const menuTemplate = new GridTemplate({
11 | buttons: [
12 | {
13 | id: 'List',
14 | titleVariants: ['List'],
15 | image: gridItemImage,
16 | },
17 | {
18 | id: 'Grid',
19 | titleVariants: ['Grid'],
20 | image: gridItemImage,
21 | },
22 | {
23 | id: 'Map',
24 | titleVariants: ['Map'],
25 | image: gridItemImage,
26 | },
27 | {
28 | id: 'Search',
29 | titleVariants: ['Search'],
30 | image: gridItemImage,
31 | },
32 | {
33 | id: 'Message',
34 | titleVariants: ['Message'],
35 | image: gridItemImage,
36 | },
37 | {
38 | id: 'Pane',
39 | titleVariants: ['Pane'],
40 | image: gridItemImage,
41 | },
42 | ],
43 | title: 'Hello from react-native-carplay',
44 | onButtonPressed: e => {
45 | if (e.id === 'List') {
46 | CarPlay.pushTemplate(listTemplate);
47 | } else if (e.id === 'Grid') {
48 | CarPlay.pushTemplate(gridTemplate);
49 | } else if (e.id === 'Search') {
50 | CarPlay.pushTemplate(searchTemplate);
51 | } else if (e.id === 'Message') {
52 | CarPlay.pushTemplate(messageTemplate);
53 | } else if (e.id === 'Pane') {
54 | CarPlay.pushTemplate(paneTemplate);
55 | } else if (e.id === 'Map') {
56 | CarPlay.pushTemplate(mapTemplate);
57 | }
58 | },
59 | });
60 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/TabBarTemplate.ts:
--------------------------------------------------------------------------------
1 | import { CarPlay } from '../CarPlay';
2 | import { GridTemplate } from './GridTemplate';
3 | import { InformationTemplate } from './InformationTemplate';
4 | import { ListTemplate } from './ListTemplate';
5 | import { PointOfInterestTemplate } from './PointOfInterestTemplate';
6 | import { Template, TemplateConfig } from './Template';
7 |
8 | export type TabBarTemplates =
9 | | ListTemplate
10 | | GridTemplate
11 | | InformationTemplate
12 | | PointOfInterestTemplate;
13 |
14 | export interface TabBarTemplateConfig extends TemplateConfig {
15 | /**
16 | * The title displayed in the navigation bar while the tab bar template is visible.
17 | */
18 | title?: string;
19 | /**
20 | * The templates to show as tabs.
21 | */
22 | templates: TabBarTemplates[];
23 |
24 | onTemplateSelect(
25 | template: TabBarTemplates | undefined,
26 | e: { templateId: string; selectedTemplateId: string },
27 | ): void;
28 | }
29 |
30 | /**/
31 | export class TabBarTemplate extends Template {
32 | public get type(): string {
33 | return 'tabbar';
34 | }
35 |
36 | constructor(public config: TabBarTemplateConfig) {
37 | super(config);
38 |
39 | CarPlay.emitter.addListener(
40 | 'didSelectTemplate',
41 | (e: { templateId: string; selectedTemplateId: string }) => {
42 | if (config.onTemplateSelect && e.templateId === this.id) {
43 | config.onTemplateSelect(
44 | config.templates.find(tpl => tpl.id === e.selectedTemplateId),
45 | e,
46 | );
47 | }
48 | },
49 | );
50 | }
51 |
52 | public updateTemplates = (config: TabBarTemplateConfig) => {
53 | this.config = config;
54 | return CarPlay.bridge.updateTabBarTemplates(this.id, this.parseConfig(config));
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/java/org/birkir/carplay/parser/RCTListTemplate.kt:
--------------------------------------------------------------------------------
1 | package org.birkir.carplay.parser
2 |
3 | import androidx.car.app.CarContext
4 | import androidx.car.app.model.ListTemplate
5 | import androidx.car.app.model.SectionedItemList
6 | import com.facebook.react.bridge.ReadableMap
7 | import org.birkir.carplay.screens.CarScreenContext
8 |
9 | class RCTListTemplate(
10 | context: CarContext,
11 | screenContext: CarScreenContext
12 | ) : RCTTemplate(context, screenContext) {
13 |
14 | override fun parse(props: ReadableMap): ListTemplate {
15 | return ListTemplate.Builder().apply {
16 | props.getString("title")?.let { setTitle(it) }
17 |
18 | // Actions
19 | props.getArray("actions")?.let {
20 | setActionStrip(
21 | parseActionStrip(it)
22 | )
23 | }
24 |
25 | // Header Action
26 | props.getMap("headerAction")?.let {
27 | setHeaderAction(
28 | parseAction(it)
29 | )
30 | }
31 |
32 | // Loading
33 | setLoading(props.isLoading())
34 |
35 | // Sections
36 | props.getArray("sections")?.let {
37 | for (i in 0 until it.size()) {
38 | val section = it.getMap(i)
39 | val header = section.getString("header")
40 | addSectionedList(
41 | SectionedItemList.create(
42 | parseItemList(section.getArray("items")),
43 | header ?: "Missing title"
44 | )
45 | )
46 | }
47 | }
48 |
49 | // Single List
50 | // @todo handle when sections and items are defined at once.
51 | props.getArray("items")?.let {
52 | setSingleList(
53 | parseItemList(it)
54 | )
55 | }
56 |
57 | }.build()
58 | }
59 |
60 |
61 | companion object {
62 | const val TAG = "RCTListTemplate"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/apps/example/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/apps/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Version of flipper SDK to use with React Native
28 | FLIPPER_VERSION=0.125.0
29 |
30 | # Use this property to specify which architecture you want to build.
31 | # You can also override it from the CLI using
32 | # ./gradlew -PreactNativeArchitectures=x86_64
33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
34 |
35 | # Use this property to enable support to the new architecture.
36 | # This will allow you to use TurboModules and the Fabric render in
37 | # your application. You should enable this flag either if you want
38 | # to write custom TurboModules/Fabric components OR use libraries that
39 | # are providing them.
40 | newArchEnabled=false
41 |
42 | # Use this property to enable or disable the Hermes JS engine.
43 | # If set to false, you will be using JSC instead.
44 | hermesEnabled=true
45 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-carplay",
3 | "version": "2.4.1-beta.0",
4 | "description": "CarPlay for React Native",
5 | "main": "lib/index.js",
6 | "react-native": "src/index.ts",
7 | "source": "src/index.ts",
8 | "files": [
9 | "src",
10 | "lib",
11 | "ios",
12 | "android",
13 | "react-native-carplay.podspec"
14 | ],
15 | "podspecPath": "./react-native-carplay.podspec",
16 | "scripts": {
17 | "prepack": "yarn run build",
18 | "lint": "eslint . --ext .ts,.tsx,.js,.jsx",
19 | "typecheck": "tsc --noEmit",
20 | "build": "tsc -p tsconfig.build.json",
21 | "fix-all-files": "eslint . --ext .ts,.tsx,.js,.jsx --fix"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/birkir/react-native-carplay.git"
26 | },
27 | "keywords": [
28 | "react",
29 | "native",
30 | "carplay",
31 | "navigation",
32 | "car",
33 | "auto"
34 | ],
35 | "author": "Birkir Gudjonsson ",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/birkir/react-native-carplay/issues"
39 | },
40 | "homepage": "https://github.com/birkir/react-native-carplay#readme",
41 | "peerDependencies": {
42 | "react": "^17.0.2 || ^18.0.0",
43 | "react-native": "^0.60.0"
44 | },
45 | "peerDependenciesMeta": {
46 | "react": {
47 | "optional": true
48 | },
49 | "react-native": {
50 | "optional": true
51 | }
52 | },
53 | "devDependencies": {
54 | "@react-native-community/eslint-config": "3.2.0",
55 | "@tsconfig/react-native": "3.0.2",
56 | "@types/node": "17.0.25",
57 | "@types/react": "18.0.6",
58 | "@types/react-native": "0.67.5",
59 | "cross-env": "7.0.3",
60 | "docusaurus-plugin-typedoc": "0.19.2",
61 | "eslint": "^8.19.0",
62 | "jest": "29.5.0",
63 | "microbundle": "0.14.2",
64 | "prettier": "2.6.2",
65 | "rimraf": "3.0.2",
66 | "typedoc": "0.24.7",
67 | "typedoc-github-wiki-theme": "1.1.0",
68 | "typedoc-plugin-markdown": "3.15.3",
69 | "typescript": "5.0.4"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/ios/RNCPStore.m:
--------------------------------------------------------------------------------
1 | #import "RNCPStore.h"
2 |
3 | @implementation RNCPStore {
4 | NSMutableDictionary* _templatesStore;
5 | NSMutableDictionary* _navigationSessionsStore;
6 | NSMutableDictionary* _tripsStore;
7 | Boolean _connected;
8 | }
9 |
10 | @synthesize window;
11 | @synthesize interfaceController;
12 |
13 | -(instancetype)init {
14 | if (self = [super init]) {
15 | _templatesStore = [[NSMutableDictionary alloc] init];
16 | _navigationSessionsStore = [[NSMutableDictionary alloc] init];
17 | _tripsStore = [[NSMutableDictionary alloc] init];
18 | _connected = false;
19 | }
20 |
21 | return self;
22 | }
23 |
24 | + (RNCPStore*) sharedManager {
25 | static RNCPStore *shared = nil;
26 | static dispatch_once_t onceToken;
27 | dispatch_once(&onceToken, ^{
28 | shared = [[self alloc] init];
29 | });
30 | return shared;
31 | }
32 |
33 | - (void) setConnected:(Boolean) isConnected {
34 | _connected = isConnected;
35 | }
36 |
37 | - (Boolean) isConnected {
38 | return _connected;
39 | }
40 |
41 | - (CPTemplate*) findTemplateById:(NSString*)templateId {
42 | return [_templatesStore objectForKey:templateId];
43 | }
44 |
45 | - (NSString*) setTemplate:(NSString*)templateId template:(CPTemplate*)carPlayTemplate {
46 | [_templatesStore setObject:carPlayTemplate forKey:templateId];
47 | return templateId;
48 | }
49 |
50 | - (CPTrip*) findTripById:(NSString*)tripId {
51 | return [_tripsStore objectForKey:tripId];
52 | }
53 |
54 | - (NSString*) setTrip:(NSString*)tripId trip:(CPTrip*)trip {
55 | [_tripsStore setObject:trip forKey:tripId];
56 | return tripId;
57 | }
58 |
59 | - (CPNavigationSession*) findNavigationSessionById:(NSString*)navigationSessionId {
60 | return [_navigationSessionsStore objectForKey:navigationSessionId];
61 | }
62 |
63 | - (NSString*) setNavigationSession:(NSString*)navigationSessionId navigationSession:(CPNavigationSession*)navigationSession {
64 | [_navigationSessionsStore setObject:navigationSession forKey:navigationSessionId];
65 | return navigationSessionId;
66 | }
67 |
68 | @end
69 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/index.ts:
--------------------------------------------------------------------------------
1 | // Templates
2 | export * from './templates/Template';
3 | export * from './templates/ActionSheetTemplate';
4 | export * from './templates/AlertTemplate';
5 | export * from './templates/ContactTemplate';
6 | export * from './templates/GridTemplate';
7 | export * from './templates/InformationTemplate';
8 | export * from './templates/ListTemplate';
9 | export * from './templates/MapTemplate';
10 | export * from './templates/NowPlayingTemplate';
11 | export * from './templates/PointOfInterestTemplate';
12 | export * from './templates/SearchTemplate';
13 | export * from './templates/TabBarTemplate';
14 | export * from './templates/VoiceControlTemplate';
15 |
16 | // Android-only Templates
17 | export * from './templates/android/MessageTemplate';
18 | export * from './templates/android/NavigationTemplate';
19 | export * from './templates/android/PaneTemplate';
20 | export * from './templates/android/PlaceListMapTemplate';
21 | export * from './templates/android/PlaceListNavigationTemplate';
22 | export * from './templates/android/RoutePreviewNavigationTemplate';
23 |
24 | // Others
25 | export * from './CarPlay';
26 | export * from './navigation/Trip';
27 | export * from './navigation/NavigationSession';
28 |
29 | // Types
30 | export * from './interfaces/Action';
31 | export * from './interfaces/AlertAction';
32 | export * from './interfaces/BarButton';
33 | export * from './interfaces/CarColor';
34 | export * from './interfaces/GridButton';
35 | export * from './interfaces/Header';
36 | export * from './interfaces/ListItem';
37 | export * from './interfaces/ListItemUpdate';
38 | export * from './interfaces/ListSection';
39 | export * from './interfaces/Maneuver';
40 | export * from './interfaces/MapButton';
41 | export * from './interfaces/NavigationAlert';
42 | export * from './interfaces/NavigationInfo';
43 | export * from './interfaces/NavigationStep';
44 | export * from './interfaces/Pane';
45 | export * from './interfaces/PauseReason';
46 | export * from './interfaces/Place';
47 | export * from './interfaces/TextConfiguration';
48 | export * from './interfaces/TimeRemainingColor';
49 | export * from './interfaces/TravelEstimates';
50 | export * from './interfaces/VoiceControlState';
51 |
--------------------------------------------------------------------------------
/apps/example/ios/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import CarPlay
3 | import React
4 |
5 | #if DEBUG
6 | #if FB_SONARKIT_ENABLED
7 | import FlipperKit
8 | #endif
9 | #endif
10 |
11 | @main
12 | class AppDelegate: RCTAppDelegate {
13 |
14 | var rootView: UIView?;
15 | var concurrentRootEnabled = true;
16 |
17 | static var shared: AppDelegate { return UIApplication.shared.delegate as! AppDelegate }
18 |
19 | override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
20 | moduleName = "RNCarPlayScene"
21 | let app = super.application(application, didFinishLaunchingWithOptions: launchOptions);
22 | self.rootView = self.createRootView(
23 | with: self.bridge,
24 | moduleName: self.moduleName,
25 | initProps: self.prepareInitialProps()
26 | );
27 | return app;
28 | }
29 |
30 | override func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
31 | if (connectingSceneSession.role == UISceneSession.Role.carTemplateApplication) {
32 | let scene = UISceneConfiguration(name: "CarPlay", sessionRole: connectingSceneSession.role)
33 | scene.delegateClass = CarSceneDelegate.self
34 | return scene
35 | } else {
36 | let scene = UISceneConfiguration(name: "Phone", sessionRole: connectingSceneSession.role)
37 | scene.delegateClass = PhoneSceneDelegate.self
38 | return scene
39 | }
40 | }
41 |
42 | override func sourceURL(for bridge: RCTBridge!) -> URL! {
43 | #if DEBUG
44 | return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index");
45 | #else
46 | return Bundle.main.url(forResource:"main", withExtension:"jsbundle")
47 | #endif
48 | }
49 |
50 | // not exposed from RCTAppDelegate, recreating.
51 | func prepareInitialProps() -> [String: Any] {
52 | var initProps = self.initialProps as? [String: Any] ?? [String: Any]()
53 | #if RCT_NEW_ARCH_ENABLED
54 | initProps["kRNConcurrentRoot"] = concurrentRootEnabled()
55 | #endif
56 | return initProps
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.github/workflows/ci-gh-pages.yml:
--------------------------------------------------------------------------------
1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages
2 | name: ci/gh-pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["master"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Build job
26 | build:
27 | runs-on: ubuntu-latest
28 | steps:
29 | - name: Checkout
30 | uses: actions/checkout@v3
31 | with:
32 | fetch-depth: 0
33 | - name: Use Node.js 16
34 | uses: actions/setup-node@v3
35 | with:
36 | node-version: 16.x
37 | - name: Get yarn cache directory path
38 | id: yarn-cache-dir-path
39 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
40 | - name: Restore yarn cache
41 | uses: actions/cache@v3
42 | id: yarn-cache
43 | with:
44 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
45 | key: yarn-cache-folder-${{ hashFiles('**/yarn.lock', '.yarnrc.yml') }}
46 | restore-keys: |
47 | yarn-cache-folder-
48 | - name: Install dependencies
49 | run: |
50 | yarn install --immutable
51 | - name: Build
52 | run: yarn workspace @carplay/docs build
53 |
54 | - name: Upload artifact
55 | uses: actions/upload-pages-artifact@v2
56 | with:
57 | path: ./apps/docs/out
58 |
59 | # Deployment job
60 | deploy:
61 | environment:
62 | name: github-pages
63 | url: ${{ steps.deployment.outputs.page_url }}
64 | runs-on: ubuntu-latest
65 | needs: build
66 | steps:
67 | - name: Deploy to GitHub Pages
68 | id: deployment
69 | uses: actions/deploy-pages@v2
70 |
--------------------------------------------------------------------------------
/apps/example/android/app/src/main/java/com/rncarplayscene/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.rncarplayscene;
2 | import android.app.Application;
3 | import com.facebook.react.PackageList;
4 | import com.facebook.react.ReactApplication;
5 | import com.facebook.react.ReactNativeHost;
6 | import com.facebook.react.ReactPackage;
7 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
8 | import com.facebook.react.defaults.DefaultReactNativeHost;
9 | import com.facebook.soloader.SoLoader;
10 | import java.util.List;
11 |
12 | public class MainApplication extends Application implements ReactApplication {
13 |
14 | private final ReactNativeHost mReactNativeHost =
15 | new DefaultReactNativeHost(this) {
16 | @Override
17 | public boolean getUseDeveloperSupport() {
18 | return BuildConfig.DEBUG;
19 | }
20 |
21 | @Override
22 | protected List getPackages() {
23 | @SuppressWarnings("UnnecessaryLocalVariable")
24 | List packages = new PackageList(this).getPackages();
25 | // Packages that cannot be autolinked yet can be added manually here, for example:
26 | // packages.add(new MyReactNativePackage());
27 | return packages;
28 | }
29 |
30 | @Override
31 | protected String getJSMainModuleName() {
32 | return "index";
33 | }
34 |
35 | @Override
36 | protected boolean isNewArchEnabled() {
37 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
38 | }
39 |
40 | @Override
41 | protected Boolean isHermesEnabled() {
42 | return BuildConfig.IS_HERMES_ENABLED;
43 | }
44 | };
45 |
46 | @Override
47 | public ReactNativeHost getReactNativeHost() {
48 | return mReactNativeHost;
49 | }
50 |
51 | @Override
52 | public void onCreate() {
53 | super.onCreate();
54 | SoLoader.init(this, /* native exopackage */ false);
55 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
56 | // If you opted-in for the New Architecture, we load the native entry point for this app.
57 | DefaultNewArchitectureEntryPoint.load();
58 | }
59 | ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/apps/example/ios/RNCarPlaySceneTests/RNCarPlaySceneTests.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #import
5 | #import
6 |
7 | #define TIMEOUT_SECONDS 600
8 | #define TEXT_TO_LOOK_FOR @"Welcome to React"
9 |
10 | @interface RNCarPlaySceneTests : XCTestCase
11 |
12 | @end
13 |
14 | @implementation RNCarPlaySceneTests
15 |
16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test
17 | {
18 | if (test(view)) {
19 | return YES;
20 | }
21 | for (UIView *subview in [view subviews]) {
22 | if ([self findSubviewInView:subview matching:test]) {
23 | return YES;
24 | }
25 | }
26 | return NO;
27 | }
28 |
29 | - (void)testRendersWelcomeScreen
30 | {
31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
33 | BOOL foundElement = NO;
34 |
35 | __block NSString *redboxError = nil;
36 | #ifdef DEBUG
37 | RCTSetLogFunction(
38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
39 | if (level >= RCTLogLevelError) {
40 | redboxError = message;
41 | }
42 | });
43 | #endif
44 |
45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
48 |
49 | foundElement = [self findSubviewInView:vc.view
50 | matching:^BOOL(UIView *view) {
51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
52 | return YES;
53 | }
54 | return NO;
55 | }];
56 | }
57 |
58 | #ifdef DEBUG
59 | RCTSetLogFunction(RCTDefaultLogFunction);
60 | #endif
61 |
62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
64 | }
65 |
66 | @end
67 |
--------------------------------------------------------------------------------
/apps/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | require_relative '../../../node_modules/react-native/scripts/react_native_pods'
2 | require_relative '../../../node_modules/@react-native-community/cli-platform-ios/native_modules'
3 |
4 | platform :ios, '13.0'
5 | prepare_react_native_project!
6 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set.
7 | # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded
8 | #
9 | # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js`
10 | # ```js
11 | # module.exports = {
12 | # dependencies: {
13 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}),
14 | # ```
15 | flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled
16 | linkage = ENV['USE_FRAMEWORKS']
17 | if linkage != nil
18 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
19 | use_frameworks! :linkage => linkage.to_sym
20 | end
21 |
22 | target 'RNCarPlayScene' do
23 | config = use_native_modules!
24 |
25 | # Flags change depending on the env values.
26 | flags = get_default_flags()
27 |
28 | use_react_native!(
29 | :path => config[:reactNativePath],
30 | # Hermes is now enabled by default. Disable by setting this flag to false.
31 | # Upcoming versions of React Native may rely on get_default_flags(), but
32 | # we make it explicit here to aid in the React Native upgrade process.
33 | :hermes_enabled => flags[:hermes_enabled],
34 | :fabric_enabled => flags[:fabric_enabled],
35 | # Enables Flipper.
36 | #
37 | # Note that if you have use_frameworks! enabled, Flipper will not work and
38 | # you should disable the next line.
39 | :flipper_configuration => flipper_config,
40 | # An absolute path to your application root.
41 | :app_path => "#{Pod::Config.instance.installation_root}/.."
42 | )
43 |
44 | target 'RNCarPlaySceneTests' do
45 | inherit! :complete
46 | # Pods for testing
47 | end
48 |
49 | post_install do |installer|
50 | react_native_post_install(
51 | installer,
52 | react_native_path = "../../../node_modules/react-native",
53 | # Set `mac_catalyst_enabled` to `true` in order to apply patches
54 | # necessary for Mac Catalyst builds
55 | :mac_catalyst_enabled => false
56 | )
57 | __apply_Xcode_12_5_M1_post_install_workaround(installer)
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/navigation/NavigationSession.ts:
--------------------------------------------------------------------------------
1 | import { CarPlay } from '../CarPlay';
2 | import { Maneuver } from '../interfaces/Maneuver';
3 | import { PauseReason } from '../interfaces/PauseReason';
4 | import { TravelEstimates } from '../interfaces/TravelEstimates';
5 | import { MapTemplate } from '../templates/MapTemplate';
6 | import { Trip } from './Trip';
7 | import { Image, processColor } from 'react-native';
8 |
9 | export class NavigationSession {
10 | public maneuvers: Maneuver[] = [];
11 |
12 | constructor(public id: string, public trip: Trip, public mapTemplate: MapTemplate) {}
13 |
14 | public updateManeuvers(maneuvers: Maneuver[]) {
15 | this.maneuvers = maneuvers;
16 | const windowScale = CarPlay.window?.scale ?? 1;
17 | CarPlay.bridge.updateManeuversNavigationSession(
18 | this.id,
19 | maneuvers.map(maneuver => {
20 | if (maneuver.symbolImage) {
21 | const image = Image.resolveAssetSource(maneuver.symbolImage);
22 | maneuver.symbolImage = image;
23 | maneuver.symbolImageSize = maneuver.symbolImageSize ?? { width: 50, height: 50 };
24 | const width = Math.floor((maneuver.symbolImageSize.width * windowScale) / image.scale);
25 | const height = Math.floor((maneuver.symbolImageSize.height * windowScale) / image.scale);
26 | maneuver.symbolImageSize = { width, height };
27 | }
28 | if (maneuver.junctionImage) {
29 | maneuver.junctionImage = Image.resolveAssetSource(maneuver.junctionImage);
30 | }
31 | if (maneuver.tintSymbolImage && typeof maneuver.tintSymbolImage === 'string') {
32 | maneuver.tintSymbolImage = processColor(maneuver.tintSymbolImage);
33 | }
34 | return maneuver;
35 | }),
36 | );
37 | }
38 |
39 | public updateTravelEstimates(maneuverIndex: number, travelEstimates: TravelEstimates) {
40 | if (!travelEstimates.distanceUnits) {
41 | travelEstimates.distanceUnits = 'kilometers';
42 | }
43 | CarPlay.bridge.updateTravelEstimatesNavigationSession(this.id, maneuverIndex, travelEstimates);
44 | }
45 |
46 | public cancel() {
47 | CarPlay.bridge.cancelNavigationSession(this.id);
48 | }
49 |
50 | public finish() {
51 | CarPlay.bridge.finishNavigationSession(this.id);
52 | }
53 |
54 | public pause(reason: PauseReason, description?: string) {
55 | CarPlay.bridge.pauseNavigationSession(this.id, reason, description);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/SearchTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Image, Platform } from 'react-native';
2 | import { CarPlay } from '../CarPlay';
3 | import { ListItem } from '../interfaces/ListItem';
4 | import { BaseEvent, Template, TemplateConfig } from './Template';
5 |
6 | export interface SearchTemplateConfig extends TemplateConfig {
7 | /**
8 | * Fired when search input is changed.
9 | * Must return list of items to show.
10 | * @param query Search query
11 | */
12 | onSearch?(query: string): Promise;
13 | /**
14 | * Fired when result item is selected.
15 | * Spinner shows by default.
16 | * When the returned promise is resolved the spinner will hide.
17 | * @param item Object with the selected index
18 | */
19 | onItemSelect?(item: { index: number }): Promise;
20 | /**
21 | * Fired when search button is pressed
22 | */
23 | onSearchButtonPressed?(e: BaseEvent): void;
24 | }
25 |
26 | export class SearchTemplate extends Template {
27 | public get type(): string {
28 | return 'search';
29 | }
30 |
31 | get eventMap() {
32 | return {
33 | searchButtonPressed: 'onSearchButtonPressed',
34 | };
35 | }
36 |
37 | constructor(public config: SearchTemplateConfig) {
38 | // parse out any images in the results
39 |
40 | super(config);
41 |
42 | CarPlay.emitter.addListener(
43 | 'updatedSearchText',
44 | (e: { searchText: string; templateId: string }) => {
45 | if (config.onSearch && e.templateId === this.id) {
46 | void Promise.resolve(config.onSearch(e.searchText)).then((result = []) => {
47 | if (Platform.OS === 'ios') {
48 | const parsedResults = result.map(item => ({
49 | ...item,
50 | image: item.image ? Image.resolveAssetSource(item.image) : undefined,
51 | }));
52 | CarPlay.bridge.reactToUpdatedSearchText(e.templateId, parsedResults);
53 | }
54 | });
55 | }
56 | },
57 | );
58 |
59 | CarPlay.emitter.addListener(
60 | 'selectedResult',
61 | (e: { templateId: string; index: number; id?: string }) => {
62 | if (config.onItemSelect && e.templateId === this.id) {
63 | void Promise.resolve(config.onItemSelect(e)).then(
64 | () => Platform.OS === 'ios' && CarPlay.bridge.reactToSelectedResult(true),
65 | );
66 | }
67 | },
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/interfaces/ListItem.ts:
--------------------------------------------------------------------------------
1 | import { ImageSourcePropType } from 'react-native';
2 | import { Action } from './Action';
3 |
4 | /**
5 | * A list item that appears in a list template.
6 | */
7 | export interface ListItem {
8 | /**
9 | * References the item by id
10 | */
11 | id?: string;
12 | /**
13 | * The primary text displayed in the list item cell.
14 | */
15 | text: string;
16 | /**
17 | * Extra text displayed below the primary text in the list item cell.
18 | */
19 | detailText?: string;
20 | /**
21 | * Image from file system displayed on the leading edge of the list item cell.
22 | */
23 | image?: ImageSourcePropType;
24 | /**
25 | * A list of images shown in the ListRowImageItem
26 | * @namespace iOS
27 | */
28 | images?: ImageSourcePropType[];
29 | /**
30 | * Url for image displayed on the leading edge of the list item cell.
31 | */
32 | imgUrl?: null;
33 | /**
34 | * Url for image displayed on the leading edge of the list item cell.
35 | * @namespace iOS
36 | */
37 | imgUrls?: string[];
38 | /**
39 | * A Boolean value indicating whether the list item cell shows a disclosure indicator on the trailing edge of the list item cell.
40 | * @namespace iOS
41 | */
42 | showsDisclosureIndicator?: boolean;
43 | /**
44 | * Is Playing flag.
45 | * @namespace iOS
46 | */
47 | isPlaying?: boolean;
48 | /**
49 | ** Value between 0.0 and 1.0 for progress bar of the list item cell.
50 | * @namespace iOS
51 | */
52 | playbackProgress?: number;
53 | /**
54 | * The image from file system displayed on the trailing edge of the list item cell.
55 | * @namespace iOS
56 | */
57 | accessoryImage?: ImageSourcePropType;
58 | /**
59 | * Sets the initial enabled state for Row.
60 | * @default true
61 | * @namespace Android
62 | */
63 | enabled?: boolean;
64 | /**
65 | * Shows an icon at the end of the row that indicates that the row is browsable.
66 | * Browsable rows can be used, for example, to represent the parent row in a hierarchy of lists with child lists.
67 | * If a row is browsable, then no Action or Toggle can be added to it.
68 | * @namespace Android
69 | */
70 | browsable?: boolean;
71 | /**
72 | * If a row has a toggle set, then no Action or numeric decoration can be set.
73 | * @namespace Android
74 | */
75 | toggle?: number;
76 | /**
77 | * Adds an additional action to the end of the row.
78 | * @namespace Android
79 | */
80 | action?: Action<'custom'>;
81 | }
82 |
--------------------------------------------------------------------------------
/.github/workflows/ci-packages.yml:
--------------------------------------------------------------------------------
1 | name: ci/packages
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev
7 | - main
8 | paths:
9 | - 'packages/**'
10 | - '.yarnrc.yml'
11 | - 'yarn.lock'
12 | - '.prettier*'
13 | - 'tsconfig.base.json'
14 | - '.prettier*'
15 | - '.github/workflows/ci-packages.yml'
16 |
17 | pull_request:
18 | types:
19 | - opened
20 | - synchronize
21 | - reopened
22 | paths:
23 | - 'packages/**'
24 | - '.yarnrc.yml'
25 | - 'yarn.lock'
26 | - '.prettier*'
27 | - 'tsconfig.base.json'
28 | - '.prettier*'
29 | - '.github/workflows/ci-packages.yml'
30 |
31 | jobs:
32 | test:
33 | runs-on: ubuntu-latest
34 | strategy:
35 | matrix:
36 | node-version: [14.x]
37 | steps:
38 | - uses: actions/checkout@v3
39 | with:
40 | fetch-depth: 0
41 |
42 | - name: Use Node.js ${{ matrix.node-version }}
43 | uses: actions/setup-node@v3
44 | with:
45 | node-version: ${{ matrix.node-version }}
46 |
47 | - name: Get yarn cache directory path
48 | id: yarn-cache-dir-path
49 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
50 |
51 | - name: Restore yarn cache
52 | uses: actions/cache@v3
53 | id: yarn-cache
54 | with:
55 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
56 | key: yarn-cache-folder-${{ hashFiles('**/yarn.lock', '.yarnrc.yml') }}
57 | restore-keys: |
58 | yarn-cache-folder-
59 | - name: Restore packages cache
60 | uses: actions/cache@v3
61 | with:
62 | path: |
63 | ${{ github.workspace }}/.cache
64 | ${{ github.workspace }}/**/tsconfig.tsbuildinfo
65 | ${{ github.workspace }}/**/.eslintcache
66 | key: ${{ runner.os }}-packages-cache-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/**.[jt]sx?', 'packages/**.json') }}
67 | restore-keys: |
68 | ${{ runner.os }}-packages-cache-${{ hashFiles('**/yarn.lock') }}-
69 | - name: Install dependencies
70 | run: |
71 | yarn install --immutable
72 | - name: Typecheck
73 | run: |
74 | yarn workspaces foreach -tv --from 'react-native-carplay' --since=origin/master --recursive run typecheck
75 | - name: Linter
76 | run: |
77 | yarn workspaces foreach -tv --include 'react-native-carplay' --since=origin/master --recursive run lint --cache
78 | - name: Run build for changed packages
79 | run: |
80 | yarn workspaces foreach -tv --include 'react-native-carplay' --since=origin/master run build
81 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/java/org/birkir/carplay/utils/VirtualRenderer.kt:
--------------------------------------------------------------------------------
1 | package org.birkir.carplay.utils
2 |
3 | import android.app.Presentation
4 | import android.content.Context
5 | import android.hardware.display.DisplayManager
6 | import android.os.Bundle
7 | import android.util.Log
8 | import android.view.Display
9 | import android.view.ViewGroup
10 | import androidx.car.app.AppManager
11 | import androidx.car.app.CarContext
12 | import androidx.car.app.SurfaceCallback
13 | import androidx.car.app.SurfaceContainer
14 | import com.facebook.react.ReactApplication
15 | import com.facebook.react.ReactRootView
16 |
17 | /**
18 | * Renders the view tree into a surface using VirtualDisplay. It runs the ReactNative component registered
19 | */
20 | class VirtualRenderer(private val context: CarContext, private val moduleName: String) {
21 |
22 | private var rootView: ReactRootView? = null
23 |
24 | init {
25 | context.getCarService(AppManager::class.java).setSurfaceCallback(object : SurfaceCallback {
26 | override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
27 | val surface = surfaceContainer.surface
28 | if (surface == null) {
29 | Log.w(TAG, "surface is null")
30 | } else {
31 | renderPresentation(surfaceContainer)
32 | }
33 | }
34 | })
35 | }
36 |
37 | private fun renderPresentation(container: SurfaceContainer) {
38 | val manager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
39 | val display = manager.createVirtualDisplay(
40 | "AndroidAutoMapTemplate",
41 | container.width,
42 | container.height,
43 | container.dpi,
44 | container.surface,
45 | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION,
46 | )
47 | val presentation = MapPresentation(context, display.display, moduleName)
48 | presentation.show()
49 | }
50 |
51 | inner class MapPresentation(context: Context, display: Display, private val moduleName: String) :
52 | Presentation(context, display) {
53 | override fun onCreate(savedInstanceState: Bundle?) {
54 | super.onCreate(savedInstanceState)
55 | val instanceManager =
56 | (context.applicationContext as ReactApplication).reactNativeHost.reactInstanceManager
57 | if (rootView == null) {
58 | Log.d(TAG, "onCreate: rootView is null, initializing rootView")
59 | rootView = ReactRootView(context).apply {
60 | startReactApplication(instanceManager, moduleName)
61 | runApplication()
62 | }
63 | } else {
64 | (rootView?.parent as? ViewGroup)?.removeView(rootView)
65 | }
66 | rootView?.let {
67 | setContentView(it)
68 | }
69 | }
70 | }
71 |
72 | companion object {
73 | const val TAG = "VirtualRenderer"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/apps/example/ios/RNCarPlayScene/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | RNCarPlayScene
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSExceptionDomains
30 |
31 | localhost
32 |
33 | NSExceptionAllowsInsecureHTTPLoads
34 |
35 |
36 |
37 |
38 | NSLocationWhenInUseUsageDescription
39 |
40 | UIApplicationSceneManifest
41 |
42 | UIApplicationSupportsMultipleScenes
43 |
44 | UISceneConfigurations
45 |
46 | CPTemplateApplicationSceneSessionRoleApplication
47 |
48 |
49 | UISceneClassName
50 | CPTemplateApplicationScene
51 | UISceneConfigurationName
52 | CarPlay
53 | UISceneDelegateClassName
54 | $(PRODUCT_MODULE_NAME).CarSceneDelegate
55 |
56 |
57 | UIWindowSceneSessionRoleApplication
58 |
59 |
60 | UISceneClassName
61 | UIWindowScene
62 | UISceneConfigurationName
63 | Phone
64 | UISceneDelegateClassName
65 | $(PRODUCT_MODULE_NAME).PhoneSceneDelegate
66 |
67 |
68 |
69 |
70 | UILaunchStoryboardName
71 | LaunchScreen
72 | UIRequiredDeviceCapabilities
73 |
74 | armv7
75 |
76 | UISupportedInterfaceOrientations
77 |
78 | UIInterfaceOrientationPortrait
79 | UIInterfaceOrientationLandscapeLeft
80 | UIInterfaceOrientationLandscapeRight
81 |
82 | UIViewControllerBasedStatusBarAppearance
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/apps/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Button, Text, View } from 'react-native';
3 | // import 'react-native-gesture-handler';
4 | import { NavigationContainer } from '@react-navigation/native';
5 | import { createStackNavigator } from '@react-navigation/stack';
6 | import { Grid } from './screens/Grid';
7 | import { List } from './screens/List';
8 | import { Map } from './screens/Map';
9 | import { Menu } from './screens/Menu';
10 | import { Search } from './screens/Search';
11 | import { VoiceControl } from './screens/VoiceControl';
12 | import { TabBar } from './screens/TabBar';
13 | import { Contact } from './screens/Contact';
14 | import { ActionSheet } from './screens/ActionSheet';
15 | import { Alert } from './screens/Alert';
16 | import { Information } from './screens/Information';
17 | import { NowPlaying } from './screens/NowPlaying';
18 | import { POI } from './screens/POI';
19 | import { CarPlay } from 'react-native-carplay';
20 |
21 | const Stack = createStackNavigator();
22 |
23 | export const App = () => {
24 | const [carPlayConnected, setCarPlayConnected] = useState(CarPlay.connected);
25 |
26 | useEffect(() => {
27 | function onConnect() {
28 | setCarPlayConnected(true);
29 | }
30 |
31 | function onDisconnect() {
32 | setCarPlayConnected(false);
33 | }
34 |
35 | CarPlay.registerOnConnect(onConnect);
36 | CarPlay.registerOnDisconnect(onDisconnect);
37 |
38 | return () => {
39 | CarPlay.unregisterOnConnect(onConnect);
40 | CarPlay.unregisterOnDisconnect(onDisconnect);
41 | };
42 | });
43 |
44 | return carPlayConnected ? (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | ) : (
63 |
64 | Please connect Car Play and open the test app
65 | setCarPlayConnected(true)} />
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/android/NavigationTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '../../interfaces/Action';
2 | import { CarColor } from '../../interfaces/CarColor';
3 | import { NavigationInfo } from '../../interfaces/NavigationInfo';
4 | import { TravelEstimates } from '../../interfaces/TravelEstimates';
5 | import {
6 | AndroidNavigationBaseTemplate,
7 | AndroidNavigationBaseTemplateConfig,
8 | } from './AndroidNavigationBaseTemplate';
9 |
10 | export interface NavigationTemplateConfig extends AndroidNavigationBaseTemplateConfig {
11 | /**
12 | * Sets an ActionStrip with a list of template-scoped actions for this template.
13 | * The Action buttons in Map Based Template are automatically adjusted based on the screen size. On narrow width screen, icon Actions show by default. If no icon specify, showing title Actions instead. On wider width screen, title Actions show by default. If no title specify, showing icon Actions instead.
14 | * Requirements This template allows up to 4 Actions in its ActionStrip. Of the 4 allowed Actions, it can either be a title Action as set via setTitle, or a icon Action as set via setIcon.
15 | */
16 | actions: Action[];
17 | /**
18 | * Sets the background color to use for the navigation information.
19 | * Depending on contrast requirements, capabilities of the vehicle screens, or other factors, the color may be ignored by the host or overridden by the vehicle system.
20 | */
21 | backgroundColor?: CarColor;
22 | /**
23 | * Sets the TravelEstimate to the final destination.
24 | */
25 | travelEstimate?: TravelEstimates;
26 | /**
27 | * Sets an ActionStrip with a list of map-control related actions for this template, such as pan or zoom.
28 | * The host will draw the buttons in an area that is associated with map controls.
29 | * If the app does not include the PAN button in this ActionStrip, the app will not receive the user input for panning gestures from SurfaceCallback methods, and the host will exit any previously activated pan mode.
30 | * Requirements This template allows up to 4 Actions in its map ActionStrip. Only Actions with icons set via setIcon are allowed.
31 | */
32 | mapButtons?: Action[];
33 | /**
34 | * Sets the navigation information to display on the template.
35 | * Unless set with this method, navigation info won't be displayed on the template.
36 | */
37 | navigationInfo?: NavigationInfo;
38 | }
39 |
40 | /**
41 | * A template for showing navigation information.
42 | * This template has two independent sections which can be updated:
43 | * - Navigation information such as routing instructions or navigation-related messages.
44 | * - Travel estimates to the destination.
45 | */
46 | export class NavigationTemplate extends AndroidNavigationBaseTemplate {
47 | public get type(): string {
48 | return 'navigation';
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/src/main/java/org/birkir/carplay/screens/CarScreen.kt:
--------------------------------------------------------------------------------
1 | package org.birkir.carplay.screens
2 |
3 | import android.util.Log
4 | import androidx.car.app.CarContext
5 | import androidx.car.app.Screen
6 | import androidx.car.app.model.Pane
7 | import androidx.car.app.model.PaneTemplate
8 | import androidx.car.app.model.PlaceListMapTemplate
9 | import androidx.car.app.model.TabTemplate
10 | import androidx.car.app.model.Template
11 | import androidx.car.app.navigation.model.MapTemplate
12 | import androidx.car.app.navigation.model.NavigationTemplate
13 | import androidx.car.app.navigation.model.PlaceListNavigationTemplate
14 | import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate
15 | import androidx.lifecycle.Lifecycle
16 | import androidx.lifecycle.LifecycleEventObserver
17 | import androidx.lifecycle.LifecycleOwner
18 | import com.facebook.react.bridge.ReadableMap
19 | import org.birkir.carplay.utils.VirtualRenderer
20 |
21 | class CarScreen(carContext: CarContext) : Screen(carContext) {
22 |
23 | var template: Template? = null
24 | set(value) {
25 | field = value
26 | Log.d(TAG, "Template set: ${value?.javaClass?.simpleName}")
27 | invalidate()
28 | }
29 | private var virtualRenderer: VirtualRenderer? = null
30 |
31 | init {
32 | lifecycle.addObserver(object : LifecycleEventObserver {
33 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
34 | if (event == Lifecycle.Event.ON_DESTROY && virtualRenderer != null) {
35 | Log.d(TAG, "onStateChanged: got $event, removing virtual renderer")
36 | virtualRenderer = null
37 | }
38 | }
39 | })
40 | }
41 |
42 | fun setTemplate(template: Template?, templateId: String, props: ReadableMap?) {
43 | // allow MapTemplate, NavigationTemplate and PlaceListMapTemplate
44 | val isSurfaceTemplate = template is MapTemplate
45 | || template is NavigationTemplate
46 | || template is PlaceListMapTemplate
47 | || template is PlaceListNavigationTemplate
48 | || template is RoutePreviewNavigationTemplate
49 |
50 | if (isSurfaceTemplate && virtualRenderer == null) {
51 | Log.d(TAG, "setTemplate: received navigation template with args: $templateId")
52 | virtualRenderer = VirtualRenderer(carContext, templateId)
53 | }
54 | this.template = template
55 | if (template is TabTemplate) {
56 | invalidate()
57 | }
58 | }
59 |
60 | override fun onGetTemplate(): Template {
61 | Log.d(TAG, "onGetTemplate for $marker")
62 | val currentTemplate = template
63 | if (currentTemplate == null) {
64 | Log.d(TAG, "Template is null, returning loading template")
65 | return PaneTemplate.Builder(Pane.Builder().setLoading(true).build())
66 | .setTitle("Loading...")
67 | .build()
68 | }
69 | Log.d(TAG, "Returning template: ${currentTemplate::class.simpleName}")
70 | return currentTemplate
71 | }
72 |
73 | companion object {
74 | const val TAG = "CarScreen"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/apps/example/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.7)
5 | base64
6 | nkf
7 | rexml
8 | activesupport (6.1.7.7)
9 | concurrent-ruby (~> 1.0, >= 1.0.2)
10 | i18n (>= 1.6, < 2)
11 | minitest (>= 5.1)
12 | tzinfo (~> 2.0)
13 | zeitwerk (~> 2.3)
14 | addressable (2.8.6)
15 | public_suffix (>= 2.0.2, < 6.0)
16 | algoliasearch (1.27.5)
17 | httpclient (~> 2.8, >= 2.8.3)
18 | json (>= 1.5.1)
19 | atomos (0.1.3)
20 | base64 (0.2.0)
21 | claide (1.1.0)
22 | cocoapods (1.15.2)
23 | addressable (~> 2.8)
24 | claide (>= 1.0.2, < 2.0)
25 | cocoapods-core (= 1.15.2)
26 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
27 | cocoapods-downloader (>= 2.1, < 3.0)
28 | cocoapods-plugins (>= 1.0.0, < 2.0)
29 | cocoapods-search (>= 1.0.0, < 2.0)
30 | cocoapods-trunk (>= 1.6.0, < 2.0)
31 | cocoapods-try (>= 1.1.0, < 2.0)
32 | colored2 (~> 3.1)
33 | escape (~> 0.0.4)
34 | fourflusher (>= 2.3.0, < 3.0)
35 | gh_inspector (~> 1.0)
36 | molinillo (~> 0.8.0)
37 | nap (~> 1.0)
38 | ruby-macho (>= 2.3.0, < 3.0)
39 | xcodeproj (>= 1.23.0, < 2.0)
40 | cocoapods-core (1.15.2)
41 | activesupport (>= 5.0, < 8)
42 | addressable (~> 2.8)
43 | algoliasearch (~> 1.0)
44 | concurrent-ruby (~> 1.1)
45 | fuzzy_match (~> 2.0.4)
46 | nap (~> 1.0)
47 | netrc (~> 0.11)
48 | public_suffix (~> 4.0)
49 | typhoeus (~> 1.0)
50 | cocoapods-deintegrate (1.0.5)
51 | cocoapods-downloader (2.1)
52 | cocoapods-plugins (1.0.0)
53 | nap
54 | cocoapods-search (1.0.1)
55 | cocoapods-trunk (1.6.0)
56 | nap (>= 0.8, < 2.0)
57 | netrc (~> 0.11)
58 | cocoapods-try (1.2.0)
59 | colored2 (3.1.2)
60 | concurrent-ruby (1.2.3)
61 | escape (0.0.4)
62 | ethon (0.16.0)
63 | ffi (>= 1.15.0)
64 | ffi (1.16.3)
65 | fourflusher (2.3.1)
66 | fuzzy_match (2.0.4)
67 | gh_inspector (1.1.3)
68 | httpclient (2.8.3)
69 | i18n (1.14.1)
70 | concurrent-ruby (~> 1.0)
71 | json (2.7.1)
72 | minitest (5.22.2)
73 | molinillo (0.8.0)
74 | nanaimo (0.3.0)
75 | nap (1.1.0)
76 | netrc (0.11.0)
77 | nkf (0.2.0)
78 | public_suffix (4.0.7)
79 | rexml (3.3.6)
80 | strscan
81 | ruby-macho (2.5.1)
82 | strscan (3.1.0)
83 | typhoeus (1.4.1)
84 | ethon (>= 0.9.0)
85 | tzinfo (2.0.6)
86 | concurrent-ruby (~> 1.0)
87 | xcodeproj (1.25.0)
88 | CFPropertyList (>= 2.3.3, < 4.0)
89 | atomos (~> 0.1.3)
90 | claide (>= 1.0.2, < 2.0)
91 | colored2 (~> 3.1)
92 | nanaimo (~> 0.3.0)
93 | rexml (>= 3.3.2, < 4.0)
94 | zeitwerk (2.6.13)
95 |
96 | PLATFORMS
97 | ruby
98 |
99 | DEPENDENCIES
100 | cocoapods (>= 1.11.3)
101 |
102 | RUBY VERSION
103 | ruby 2.6.10p210
104 |
105 | BUNDLED WITH
106 | 2.3.3
107 |
--------------------------------------------------------------------------------
/apps/example/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 |
3 | def kotlin_version = rootProject.ext.has('kotlinVersion')
4 | ? rootProject.ext.get('kotlinVersion')
5 | : project.properties['RnAndroidAuto_kotlinVersion']
6 |
7 | repositories {
8 | mavenLocal()
9 | mavenCentral()
10 | google()
11 | jcenter()
12 | }
13 |
14 | dependencies {
15 | classpath "com.android.tools.build:gradle:7.2.1"
16 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
17 | }
18 | }
19 |
20 | def isNewArchitectureEnabled() {
21 | return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
22 | }
23 |
24 | apply plugin: "com.android.library"
25 | apply plugin: "kotlin-android"
26 |
27 | def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
28 |
29 | if (isNewArchitectureEnabled()) {
30 | apply plugin: "com.facebook.react"
31 | }
32 |
33 | def getExtOrDefault(name) {
34 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["Carplay_" + name]
35 | }
36 |
37 | def getExtOrIntegerDefault(name) {
38 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Carplay_" + name]).toInteger()
39 | }
40 |
41 | def supportsNamespace() {
42 | def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
43 | def major = parsed[0].toInteger()
44 | def minor = parsed[1].toInteger()
45 |
46 | // Namespace support was added in 7.3.0
47 | if (major == 7 && minor >= 3) {
48 | return true
49 | }
50 |
51 | return major >= 8
52 | }
53 |
54 | android {
55 | if (supportsNamespace()) {
56 | namespace "org.birkir.carplay"
57 |
58 | sourceSets {
59 | main {
60 | manifest.srcFile "src/main/AndroidManifestNew.xml"
61 | java.srcDirs = ["src/main/java"]
62 | res.srcDirs = ["src/main/res"]
63 | }
64 | }
65 | }
66 |
67 | compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
68 |
69 | defaultConfig {
70 | minSdkVersion getExtOrIntegerDefault("minSdkVersion")
71 | targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
72 |
73 | }
74 |
75 | buildTypes {
76 | release {
77 | minifyEnabled false
78 | }
79 | }
80 |
81 | lintOptions {
82 | disable "GradleCompatible"
83 | }
84 |
85 | compileOptions {
86 | sourceCompatibility JavaVersion.VERSION_1_8
87 | targetCompatibility JavaVersion.VERSION_1_8
88 | }
89 | }
90 |
91 | repositories {
92 | mavenLocal()
93 | mavenCentral()
94 | google()
95 | jcenter()
96 | }
97 |
98 | def kotlin_version = getExtOrDefault('kotlinVersion')
99 |
100 | dependencies {
101 | // For < 0.71, this will be from the local maven repo
102 | // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
103 | //noinspection GradleDynamicVersion
104 | implementation "com.facebook.react:react-native:+"
105 | implementation "androidx.car.app:app-projected:1.4.0-beta02"
106 | implementation "androidx.car.app:app:1.4.0-beta02"
107 | implementation "org.greenrobot:eventbus:3.2.0"
108 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
109 | }
110 |
111 |
--------------------------------------------------------------------------------
/.github/workflows/ci-codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: 'ci/codeql'
13 |
14 | on:
15 | push:
16 | branches: ['master']
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: ['master']
20 | schedule:
21 | - cron: '33 2 * * 3'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: ['typescript']
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Use only 'java' to analyze code written in Java, Kotlin or both
38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
40 |
41 | steps:
42 | - name: Checkout repository
43 | uses: actions/checkout@v3
44 |
45 | # Initializes the CodeQL tools for scanning.
46 | - name: Initialize CodeQL
47 | uses: github/codeql-action/init@v2
48 | with:
49 | languages: ${{ matrix.language }}
50 | # If you wish to specify custom queries, you can do so here or in a config file.
51 | # By default, queries listed here will override any specified in a config file.
52 | # Prefix the list here with "+" to use these queries and those in the config file.
53 |
54 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
55 | # queries: security-extended,security-and-quality
56 |
57 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
58 | # If this step fails, then you should remove it and run the build manually (see below)
59 | - name: Autobuild
60 | uses: github/codeql-action/autobuild@v2
61 |
62 | # ℹ️ Command-line programs to run using the OS shell.
63 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
64 |
65 | # If the Autobuild fails above, remove it and uncomment the following three lines.
66 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
67 |
68 | # - run: |
69 | # echo "Run, Build Application using script"
70 | # ./location_of_script_within_repo/buildscript.sh
71 |
72 | - name: Perform CodeQL Analysis
73 | uses: github/codeql-action/analyze@v2
74 | with:
75 | category: '/language:${{matrix.language}}'
76 |
--------------------------------------------------------------------------------
/apps/example/android/app/src/debug/java/com/rncarplayscene/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.rncarplayscene;
8 |
9 | import android.content.Context;
10 | import com.facebook.flipper.android.AndroidFlipperClient;
11 | import com.facebook.flipper.android.utils.FlipperUtils;
12 | import com.facebook.flipper.core.FlipperClient;
13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping;
17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
20 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
21 | import com.facebook.react.ReactInstanceEventListener;
22 | import com.facebook.react.ReactInstanceManager;
23 | import com.facebook.react.bridge.ReactContext;
24 | import com.facebook.react.modules.network.NetworkingModule;
25 | import okhttp3.OkHttpClient;
26 |
27 | public class ReactNativeFlipper {
28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
29 | if (FlipperUtils.shouldEnableFlipper(context)) {
30 | final FlipperClient client = AndroidFlipperClient.getInstance(context);
31 |
32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
33 | client.addPlugin(new DatabasesFlipperPlugin(context));
34 | client.addPlugin(new SharedPreferencesFlipperPlugin(context));
35 | client.addPlugin(CrashReporterPlugin.getInstance());
36 |
37 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
38 | NetworkingModule.setCustomClientBuilder(
39 | new NetworkingModule.CustomClientBuilder() {
40 | @Override
41 | public void apply(OkHttpClient.Builder builder) {
42 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
43 | }
44 | });
45 | client.addPlugin(networkFlipperPlugin);
46 | client.start();
47 |
48 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
49 | // Hence we run if after all native modules have been initialized
50 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
51 | if (reactContext == null) {
52 | reactInstanceManager.addReactInstanceEventListener(
53 | new ReactInstanceEventListener() {
54 | @Override
55 | public void onReactContextInitialized(ReactContext reactContext) {
56 | reactInstanceManager.removeReactInstanceEventListener(this);
57 | reactContext.runOnNativeModulesQueueThread(
58 | new Runnable() {
59 | @Override
60 | public void run() {
61 | client.addPlugin(new FrescoFlipperPlugin());
62 | }
63 | });
64 | }
65 | });
66 | } else {
67 | client.addPlugin(new FrescoFlipperPlugin());
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/apps/example/src/screens/Menu.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { View, Button } from 'react-native';
3 | import { CarPlay, GridTemplate } from 'react-native-carplay';
4 |
5 | const gridItemImage = require('../images/go.png');
6 |
7 | export function Menu({ navigation }: any) {
8 | useEffect(() => {
9 | const gridTemplate = new GridTemplate({
10 | buttons: [
11 | {
12 | id: 'List',
13 | titleVariants: ['List'],
14 | image: gridItemImage,
15 | },
16 | {
17 | id: 'Grid',
18 | titleVariants: ['Grid'],
19 | image: gridItemImage,
20 | },
21 | {
22 | id: 'Map',
23 | titleVariants: ['Map'],
24 | image: gridItemImage,
25 | },
26 | {
27 | id: 'Search',
28 | titleVariants: ['Search'],
29 | image: gridItemImage,
30 | },
31 | {
32 | id: 'Information',
33 | titleVariants: ['Information'],
34 | image: gridItemImage,
35 | },
36 | {
37 | id: 'VoiceControl',
38 | titleVariants: ['Voice'],
39 | image: gridItemImage,
40 | },
41 | {
42 | id: 'Alert',
43 | titleVariants: ['Alert'],
44 | image: gridItemImage,
45 | },
46 | {
47 | id: 'ActionSheet',
48 | titleVariants: ['ActionSheet'],
49 | image: gridItemImage,
50 | },
51 | ],
52 | onButtonPressed: ({ id }) => {
53 | navigation.navigate(id);
54 | },
55 | onWillAppear: () => {
56 | navigation.navigate('Menu');
57 | },
58 | title: 'Hello, world',
59 | });
60 |
61 | CarPlay.setRootTemplate(gridTemplate);
62 | }, []);
63 |
64 | const onTabBarPress = () => navigation.navigate('TabBar');
65 | const onListPress = () => navigation.navigate('List');
66 | const onGridPress = () => navigation.navigate('Grid');
67 | const onMapPress = () => navigation.navigate('Map');
68 | const onSearchPress = () => navigation.navigate('Search');
69 | const onVoiceControlPress = () => navigation.navigate('VoiceControl');
70 | const onContactPress = () => navigation.navigate('Contact');
71 | const onActionSheetPress = () => navigation.navigate('ActionSheet');
72 | const onAlertPress = () => navigation.navigate('Alert');
73 | const onInformationPress = () => navigation.navigate('Information');
74 | const onNowPlayingPress = () => navigation.navigate('NowPlaying');
75 | const onPOIPress = () => navigation.navigate('POI');
76 |
77 | return (
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
91 |
92 |
93 |
94 |
95 | );
96 | }
97 |
98 | Menu.navigationOptions = {
99 | headerTitle: 'Car Play Example',
100 | };
101 |
--------------------------------------------------------------------------------
/apps/example/ios/RNCarPlayScene.xcodeproj/xcshareddata/xcschemes/RNCarPlayScene.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/android/PlaceListNavigationTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '../../interfaces/Action';
2 | import { Header } from '../../interfaces/Header';
3 | import { ListItem } from '../../interfaces/ListItem';
4 | import {
5 | AndroidNavigationBaseTemplate,
6 | AndroidNavigationBaseTemplateConfig,
7 | } from './AndroidNavigationBaseTemplate';
8 |
9 | export interface PlaceListNavigationTemplateConfig extends AndroidNavigationBaseTemplateConfig {
10 | /**
11 | * Sets an ActionStrip with a list of template-scoped actions for this template.
12 | * The Action buttons in Map Based Template are automatically adjusted based on the screen size. On narrow width screen, icon Actions show by default. If no icon specify, showing title Actions instead. On wider width screen, title Actions show by default. If no title specify, showing icon Actions instead.
13 | * @limit This template allows up to 4 Actions in its ActionStrip. Of the 4 allowed Actions, it can either be a title Action as set via setTitle, or a icon Action as set via setIcon.
14 | */
15 | actions?: Action[];
16 | /**
17 | * Sets the Header for this template.
18 | */
19 | header?: Header;
20 | /**
21 | * Sets an ItemList to show in a list view along with the map.
22 | * Unless set with this method, the template will not show an item list.
23 | * To show a marker corresponding to a point of interest represented by a row, set the Place instance via setMetadata. The host will display the PlaceMarker in both the map and the list view as the row becomes visible.
24 | * @limit The number of items in the ItemList should be smaller or equal than the limit provided by CONTENT_LIMIT_TYPE_PLACE_LIST. The host will ignore any items over that limit. The list itself cannot be selectable as set via setOnSelectedListener. Each Row can add up to 2 lines of texts via addText and cannot contain a Toggle.
25 | */
26 | itemList?: ListItem[];
27 | /**
28 | * Sets whether the template is in a loading state.
29 | */
30 | loading?: boolean;
31 | /**
32 | * Sets an ActionStrip with a list of map-control related actions for this template, such as pan or zoom.
33 | * The host will draw the buttons in an area that is associated with map controls.
34 | * If the app does not include the PAN button in this ActionStrip, the app will not receive the user input for panning gestures from SurfaceCallback methods, and the host will exit any previously activated pan mode.
35 | * @limit This template allows up to 4 Actions in its map ActionStrip. Only Actions with icons set via setIcon are allowed.
36 | */
37 | mapButtons?: Action[];
38 | /**
39 | * Title for the map
40 | */
41 | title?: string;
42 | }
43 |
44 | /**
45 | * A template that supports showing a list of places alongside a custom drawn map.
46 | * The template itself does not expose a drawing surface. In order to draw on the canvas, use setSurfaceCallback.
47 | * Template Restrictions In regards to template refreshes, as described in onGetTemplate, this template is considered a refresh of a previous one if:
48 | * - The previous template is in a loading state (see setLoading, or
49 | * - The template title has not changed, and the number of rows and the title (not counting spans) of each row between the previous and new ItemLists have not changed.
50 | * - The template is sent in response to a user-initiated content refresh request. (see setOnContentRefreshListener.
51 | * In order to use this template your car app MUST declare that it uses the **androidx.car.app.NAVIGATION_TEMPLATES** permission in the manifest.
52 | */
53 | export class PlaceListNavigationTemplate extends AndroidNavigationBaseTemplate {
54 | public get type(): string {
55 | return 'place-navigation-map';
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/android/PlaceListMapTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Action, HeaderAction } from '../../interfaces/Action';
2 | import {
3 | AndroidNavigationBaseTemplate,
4 | AndroidNavigationBaseTemplateConfig,
5 | } from './AndroidNavigationBaseTemplate';
6 | import { Place } from '../../interfaces/Place';
7 | import { ListItem } from '../../interfaces/ListItem';
8 |
9 | export interface PlaceListMapTemplateConfig extends AndroidNavigationBaseTemplateConfig {
10 | /**
11 | * Sets an ActionStrip with a list of template-scoped actions for this template.
12 | * The Action buttons in Map Based Template are automatically adjusted based on the screen size. On narrow width screen, icon Actions show by default. If no icon specify, showing title Actions instead. On wider width screen, title Actions show by default. If no title specify, showing icon Actions instead.
13 | * @limit This template allows up to 4 Actions in its ActionStrip. Of the 4 allowed Actions, it can either be a title Action as set via setTitle, or a icon Action as set via setIcon.
14 | */
15 | actions?: Action[];
16 | /**
17 | * Sets the anchor maker on the map.
18 | * An anchor marker will not be displayed unless set with this method.
19 | * The anchor marker is displayed differently from other markers by the host.
20 | * If not null, an anchor marker will be shown at the specified CarLocation on the map. The camera will adapt to always have the anchor marker visible within its viewport, along with other places' markers from Row that are currently visible in the Pane. This can be used to provide a reference point on the map (e.g. the center of a search region) as the user pages through the Pane's markers, for example.
21 | */
22 | anchor?: Place;
23 | /**
24 | * Sets whether to show the current location in the map.
25 | * The map template will show the user's current location on the map.
26 | * This functionality requires the app to have either the ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION permission. When isEnabled is true, the host may receive location updates from the app in order to show the user's current location.
27 | */
28 | currentLocationEnabled?: boolean;
29 | /**
30 | * Sets the Action that will be displayed in the header of the template.
31 | * Unless set with this method, the template will not have a header action.
32 | * @limit This template only supports either one of APP_ICON and BACK as a header Action.
33 | */
34 | headerAction?: HeaderAction;
35 | /**
36 | * Sets an ItemList to show in a list view along with the map.
37 | * Unless set with this method, the template will not show an item list.
38 | * To show a marker corresponding to a point of interest represented by a row, set the Place instance via setMetadata. The host will display the PlaceMarker in both the map and the list view as the row becomes visible.
39 | * @limit The number of items in the ItemList should be smaller or equal than the limit provided by CONTENT_LIMIT_TYPE_PLACE_LIST. The host will ignore any items over that limit. The list itself cannot be selectable as set via setOnSelectedListener. Each Row can add up to 2 lines of texts via addText and cannot contain a Toggle.
40 | */
41 | itemList?: ListItem[];
42 | /**
43 | * Sets whether the template is in a loading state.
44 | */
45 | loading?: boolean;
46 | /**
47 | * Title for the map
48 | */
49 | title?: string;
50 | }
51 |
52 | /**
53 | * A template that displays a map along with a list of places.
54 | * The map can display markers corresponding to the places in the list. See setItemList for details.
55 | * Template Restrictions In regards to template refreshes, as described in onGetTemplate, this template is considered a refresh of a previous one if:
56 | * - The previous template is in a loading state (see setLoading, or
57 | * - The template title has not changed, and the number of rows and the title (not counting spans) of each row between the previous and new ItemLists have not changed.
58 | * - The template is sent in response to a user-initiated content refresh request. (see setOnContentRefreshListener.
59 | */
60 | export class PlaceListMapTemplate extends AndroidNavigationBaseTemplate {
61 | public get type(): string {
62 | return 'place-list-map';
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/apps/example/ios/RNCarPlayScene/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/src/templates/android/RoutePreviewNavigationTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '../../interfaces/Action';
2 | import { Header } from '../../interfaces/Header';
3 | import { ListItem } from '../../interfaces/ListItem';
4 | import {
5 | AndroidNavigationBaseTemplate,
6 | AndroidNavigationBaseTemplateConfig,
7 | } from './AndroidNavigationBaseTemplate';
8 |
9 | export interface RoutePreviewNavigationTemplateConfig extends AndroidNavigationBaseTemplateConfig {
10 | /**
11 | * Sets an ActionStrip with a list of template-scoped actions for this template.
12 | * The Action buttons in Map Based Template are automatically adjusted based on the screen size. On narrow width screen, icon Actions show by default. If no icon specify, showing title Actions instead. On wider width screen, title Actions show by default. If no title specify, showing icon Actions instead.
13 | * @limit This template allows up to 4 Actions in its ActionStrip. Of the 4 allowed Actions, it can either be a title Action as set via setTitle, or a icon Action as set via setIcon.
14 | */
15 | actions?: Action[];
16 | /**
17 | * Sets the Header for this template.
18 | */
19 | header?: Header;
20 | /**
21 | * Sets an ItemList to show in a list view along with the map.
22 | * Unless set with this method, the template will not show an item list.
23 | * To show a marker corresponding to a point of interest represented by a row, set the Place instance via setMetadata. The host will display the PlaceMarker in both the map and the list view as the row becomes visible.
24 | * @limit The number of items in the ItemList should be smaller or equal than the limit provided by CONTENT_LIMIT_TYPE_PLACE_LIST. The host will ignore any items over that limit. The list itself cannot be selectable as set via setOnSelectedListener. Each Row can add up to 2 lines of texts via addText and cannot contain a Toggle.
25 | */
26 | itemList?: ListItem[];
27 | /**
28 | * Sets whether the template is in a loading state.
29 | */
30 | loading?: boolean;
31 | /**
32 | * Sets an ActionStrip with a list of map-control related actions for this template, such as pan or zoom.
33 | * The host will draw the buttons in an area that is associated with map controls.
34 | * If the app does not include the PAN button in this ActionStrip, the app will not receive the user input for panning gestures from SurfaceCallback methods, and the host will exit any previously activated pan mode.
35 | * @limit This template allows up to 4 Actions in its map ActionStrip. Only Actions with icons set via setIcon are allowed.
36 | */
37 | mapButtons?: Action[];
38 | /**
39 | * Sets the Action to allow users to request navigation using the currently selected route.
40 | * This should not be null if the template is not in a loading state (see #setIsLoading}), and the Action's title must be set.
41 | * Spans are not supported in the navigate action and will be ignored.
42 | */
43 | navigateAction?: Action;
44 | /**
45 | * Title for the map
46 | */
47 | title?: string;
48 | }
49 |
50 | /**
51 | * A template that supports showing a list of routes alongside a custom drawn map.
52 | * The list must have its ItemList.OnSelectedListener set, and the template must have its navigate action set (see setNavigateAction). These are used in conjunction to inform the app that:
53 | * - A route has been selected. The app should also highlight the route on the map surface.
54 | * - A navigate action has been triggered. The app should begin navigation using the selected route.
55 | * The template itself does not expose a drawing surface. In order to draw on the canvas, use setSurfaceCallback.
56 | * Template Restrictions In regards to template refreshes, as described in onGetTemplate, this template is considered a refresh of a previous one if:
57 | * - The previous template is in a loading state (see setLoading, or
58 | * - The template title has not changed, and the number of rows and the title (not counting spans) of each row between the previous and new ItemLists have not changed.
59 | * Note that specifically, this means the app should not use this template to continuously refresh the routes as the car moves.
60 | * In order to use this template your car app MUST declare that it uses the **androidx.car.app.NAVIGATION_TEMPLATES** permission in the manifest.
61 | */
62 | export class RoutePreviewNavigationTemplate extends AndroidNavigationBaseTemplate {
63 | public get type(): string {
64 | return 'route-preview-navigation';
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/packages/react-native-carplay/ios/RCTConvert+RNCarPlay.m:
--------------------------------------------------------------------------------
1 | #import "RCTConvert+RNCarPlay.h"
2 | #import
3 | #import
4 |
5 | @implementation RCTConvert (RNCarPlay)
6 |
7 | RCT_ENUM_CONVERTER(CPTripEstimateStyle, (@{
8 | @"light": @(CPTripEstimateStyleLight),
9 | @"dark": @(CPTripEstimateStyleDark)
10 | }),
11 | CPTripEstimateStyleDark,
12 | integerValue)
13 |
14 | RCT_ENUM_CONVERTER(CPPanDirection, (@{
15 | @"up": @(CPPanDirectionUp),
16 | @"right": @(CPPanDirectionRight),
17 | @"bottom": @(CPPanDirectionDown),
18 | @"left": @(CPPanDirectionLeft),
19 | @"none": @(CPPanDirectionNone)
20 | }), CPPanDirectionNone, integerValue)
21 |
22 | RCT_ENUM_CONVERTER(CPAssistantCellPosition, (@{
23 | @"top": @(CPAssistantCellPositionTop),
24 | @"bottom": @(CPAssistantCellPositionBottom)
25 | }), CPAssistantCellPositionTop, integerValue)
26 |
27 | RCT_ENUM_CONVERTER(CPAssistantCellVisibility, (@{
28 | @"off": @(CPAssistantCellVisibilityOff),
29 | @"always": @(CPAssistantCellVisibilityAlways),
30 | @"limited": @(CPAssistantCellVisibilityWhileLimitedUIActive)
31 | }), CPAssistantCellVisibilityOff, integerValue)
32 |
33 | RCT_ENUM_CONVERTER(CPAssistantCellActionType, (@{
34 | @"playMedia": @(CPAssistantCellActionTypePlayMedia),
35 | @"startCall": @(CPAssistantCellActionTypeStartCall)
36 | }), CPAssistantCellActionTypeStartCall, integerValue)
37 |
38 |
39 | + (CPMapButton*)CPMapButton:(id)json withHandler:(void (^)(CPMapButton * _Nonnull mapButton))handler {
40 | CPMapButton *mapButton = [[CPMapButton alloc] initWithHandler:handler];
41 |
42 | if ([json objectForKey:@"image"]) {
43 | [mapButton setImage:[RCTConvert UIImage:json[@"image"]]];
44 | }
45 |
46 | if ([json objectForKey:@"focusedImage"]) {
47 | [mapButton setFocusedImage:[RCTConvert UIImage:json[@"focusedImage"]]];
48 | }
49 |
50 | if ([json objectForKey:@"disabled"]) {
51 | [mapButton setEnabled:![RCTConvert BOOL:json[@"disabled"]]];
52 | }
53 |
54 | if ([json objectForKey:@"hidden"]) {
55 | [mapButton setHidden:[RCTConvert BOOL:json[@"hidden"]]];
56 | }
57 |
58 | return mapButton;
59 | }
60 |
61 | + (MKMapItem*)MKMapItem:(id)json {
62 | CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake([RCTConvert double:json[@"latitude"]], [RCTConvert double:json[@"longitude"]]);
63 | NSString *name = [RCTConvert NSString:json[@"name"]];
64 | MKPlacemark *placemark = [[MKPlacemark alloc] initWithCoordinate:coordinate];
65 | MKMapItem *mapItem = [[MKMapItem alloc] initWithPlacemark:placemark];
66 | mapItem.name = name;
67 | return mapItem;
68 | }
69 |
70 | + (CPRouteChoice*)CPRouteChoice:(id)json {
71 | return [[CPRouteChoice alloc] initWithSummaryVariants:[RCTConvert NSStringArray:json[@"additionalInformationVariants"]] additionalInformationVariants:[RCTConvert NSStringArray:json[@"selectionSummaryVariants"]] selectionSummaryVariants:[RCTConvert NSStringArray:json[@"summaryVariants"]]];
72 | }
73 |
74 | + (CPPointOfInterest*)CPPointOfInterest:(id)json {
75 | MKMapItem *location = [RCTConvert MKMapItem:json[@"location"]];
76 | NSString *title = [RCTConvert NSString:json[@"title"]];
77 | NSString *subtitle = [RCTConvert NSString:json[@"subtitle"]];
78 | NSString *summary = [RCTConvert NSString:json[@"summary"]];
79 | NSString *detailTitle = [RCTConvert NSString:json[@"detailTitle"]];
80 | NSString *detailSubtitle = [RCTConvert NSString:json[@"detailSubtitle"]];
81 | NSString *detailSummary = [RCTConvert NSString:json[@"detailSummary"]];
82 |
83 | CPPointOfInterest *poi = [[CPPointOfInterest alloc] initWithLocation:location title:title subtitle:subtitle summary:summary detailTitle:detailTitle detailSubtitle:detailSubtitle detailSummary:detailSummary pinImage:nil];
84 | return poi;
85 | }
86 |
87 | + (CPAlertActionStyle)CPAlertActionStyle:(NSString*) json {
88 | if ([json isEqualToString:@"cancel"]) {
89 | return CPAlertActionStyleCancel;
90 | } else if ([json isEqualToString:@"destructive"]) {
91 | return CPAlertActionStyleDestructive;
92 | }
93 | return CPAlertActionStyleDefault;
94 | }
95 |
96 | @end
97 |
--------------------------------------------------------------------------------