Merge pull request #254 from anultravioletaurora/5-implement-carplay-android-auto-new

5 implement carplay android auto new
This commit is contained in:
Violet Caulfield
2025-04-14 19:01:14 -05:00
committed by GitHub
22 changed files with 6498 additions and 1121 deletions
+6 -5
View File
@@ -165,8 +165,8 @@ This is undoubtedly a passion project of [mine](https://github.com/anultraviolet
##### Setup
- Clone this repository
- Run `npm run init` to initialize the project
- This will install `npm` packages, install `bundler` and required gems, and installs CocoaPods
- Run `npm run init:ios` to initialize the project
- This will install `npm` packages, install `bundler` and required gems, and install required CocoaPods
- In the `ios` directory, run `fastlane match development --readonly` to fetch the development signing certificates
- *You will need access to the *Jellify Signing* private repository*
@@ -179,7 +179,7 @@ This is undoubtedly a passion project of [mine](https://github.com/anultraviolet
##### Building
- To create a build, run `npm run fastlane:ios:build` to use fastlane to compile an `.ipa` for you
- To create a build, run `npm run fastlane:ios:build` to use fastlane to compile an `.ipa`
### 🤖 Android
@@ -204,12 +204,13 @@ This is undoubtedly a passion project of [mine](https://github.com/anultraviolet
##### Building
- To create a build, run `npm run fastlane:android:build` to use fastlane to compile an `.apk` for you
- To create a build, run `npm run fastlane:android:build` to use fastlane to compile an `.apk` for all architectures
#### References
- [Setting up Android SDK](https://developer.android.com/about/versions/14/setup-sdk)
- [ANDROID_HOME not being set](https://stackoverflow.com/questions/26356359/error-android-home-is-not-set-and-android-command-not-in-your-path-you-must/54888107#54888107)
- [Android Auto app not showing up](https://www.reddit.com/r/AndroidAuto/s/LGYHoSPdXm)
## 🙏 Special Thanks To
@@ -221,7 +222,7 @@ This is undoubtedly a passion project of [mine](https://github.com/anultraviolet
- [Nicolas Charpentier](https://github.com/charpeni) for his [React Native URL Polyfill](https://github.com/charpeni/react-native-url-polyfill) module and for his assistance with getting Jest working
- My fellow [contributors](https://github.com/anultravioletaurora/Jellify/graphs/contributors) who have poured so much heart and a lot of sweat into making _Jellify_ a great experience
- Extra thanks to [John](https://github.com/johngrantdev) and [Vali-98](https://github.com/Vali-98) for shaping and designing the user experience in many places
- Huge thank you to [Ritesh](https://github.com/riteshshukla04) for your project automation and backend knowledge (and for the memes)
- Huge thank you to [Ritesh](https://github.com/riteshshukla04) for your project automation and backend expertise (and for the memes)
- The friends I made along the way that have been critical in fostering an amazing community around _Jellify_
- [Thalia](https://github.com/PercyGabriel1129)
- [BotBlake](https://github.com/BotBlake)
+3
View File
@@ -30,6 +30,9 @@
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</activity>
</application>
</manifest>
+5 -2
View File
@@ -1,4 +1,5 @@
## fastlane documentation
fastlane documentation
----
# Installation
@@ -28,6 +29,8 @@ Runs all the tests
[bundle exec] fastlane android build
```
### android deploy
```sh
@@ -36,7 +39,7 @@ Runs all the tests
Deploy a new version to the Google Play
---
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
+3
View File
@@ -1,7 +1,10 @@
import { ListTemplate } from 'react-native-carplay'
import uuid from 'react-native-uuid'
const CarPlayDiscover = new ListTemplate({
id: uuid.v4(),
tabTitle: 'Discover',
tabSystemImageName: 'globe',
})
export default CarPlayDiscover
+12 -12
View File
@@ -1,15 +1,17 @@
import { QueryKeys } from '../../enums/query-keys'
import Client from '../../api/client'
import { fetchRecentlyPlayed } from '../../api/queries/functions/recents'
import { CarPlay, ListTemplate } from 'react-native-carplay'
import { queryClient } from '../../constants/query-client'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import ListItemTemplate from './ListTemplate'
import RecentTracksTemplate from './RecentTracks'
import RecentArtistsTemplate from './RecentArtists'
import uuid from 'react-native-uuid'
const CarPlayHome: ListTemplate = new ListTemplate({
id: 'Home',
const CarPlayHome = new ListTemplate({
id: uuid.v4(),
title: 'Home',
tabTitle: 'Home',
tabSystemImageName: 'music.house.fill',
sections: [
{
header: `Hi ${Client.user?.name ?? 'there'}`,
@@ -25,20 +27,18 @@ const CarPlayHome: ListTemplate = new ListTemplate({
switch (index) {
case 0: {
const artists = queryClient.getQueryData<BaseItemDto[]>([
QueryKeys.RecentlyPlayedArtists,
])
CarPlay.pushTemplate(ListItemTemplate(artists))
const artists =
queryClient.getQueryData<BaseItemDto[]>([QueryKeys.RecentlyPlayedArtists]) ?? []
CarPlay.pushTemplate(RecentArtistsTemplate(artists))
break
}
case 1: {
const tracks = await fetchRecentlyPlayed()
CarPlay.pushTemplate(ListItemTemplate(tracks))
const items =
queryClient.getQueryData<BaseItemDto[]>([QueryKeys.RecentlyPlayed]) ?? []
CarPlay.pushTemplate(RecentTracksTemplate(items))
break
}
case 2: {
const playlists = queryClient.getQueryData<BaseItemDto[]>([QueryKeys.UserPlaylists])
CarPlay.pushTemplate(ListItemTemplate(playlists))
break
}
}
-26
View File
@@ -1,26 +0,0 @@
import { queryClient } from '../../constants/query-client'
import { QueryKeys } from '../../enums/query-keys'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { ListTemplate } from 'react-native-carplay'
export default function ListItemTemplate(items: BaseItemDto[] | undefined): ListTemplate {
return new ListTemplate({
sections: [
{
items:
items?.map((item) => {
return {
id: item.Id!,
text: item.Name ?? 'Untitled',
image: {
uri: queryClient.getQueryData<string | undefined>([
QueryKeys.ItemImage,
item.Id!,
]),
},
}
}) ?? [],
},
],
})
}
+5 -6
View File
@@ -1,14 +1,13 @@
import { CarPlay, TabBarTemplate } from 'react-native-carplay'
import { TabBarTemplate } from 'react-native-carplay'
import CarPlayHome from './Home'
import CarPlayDiscover from './Discover'
import uuid from 'react-native-uuid'
const CarPlayNavigation: TabBarTemplate = new TabBarTemplate({
id: 'Tabs',
const CarPlayNavigation = new TabBarTemplate({
id: uuid.v4(),
title: 'Tabs',
templates: [CarPlayHome, CarPlayDiscover],
onTemplateSelect(template, e) {
if (template) CarPlay.pushTemplate(template, true)
},
onTemplateSelect(template, e) {},
})
export default CarPlayNavigation
+1 -1
View File
@@ -1,5 +1,5 @@
import { NowPlayingTemplate } from 'react-native-carplay'
const CarPlayNowPlaying: NowPlayingTemplate = new NowPlayingTemplate({})
const CarPlayNowPlaying = new NowPlayingTemplate({})
export default CarPlayNowPlaying
+22
View File
@@ -0,0 +1,22 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { ListTemplate } from 'react-native-carplay'
import uuid from 'react-native-uuid'
const RecentArtistsTemplate = (items: BaseItemDto[]) =>
new ListTemplate({
id: uuid.v4(),
sections: [
{
items:
items?.map((item) => {
return {
id: item.Id!,
text: item.Name ?? 'Untitled',
}
}) ?? [],
},
],
onItemSelect: async (item) => {},
})
export default RecentArtistsTemplate
+33
View File
@@ -0,0 +1,33 @@
import { mapDtoToTrack } from '../..//helpers/mappings'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { CarPlay, ListTemplate } from 'react-native-carplay'
import TrackPlayer from 'react-native-track-player'
import uuid from 'react-native-uuid'
import CarPlayNowPlaying from './NowPlaying'
const RecentTracksTemplate = (items: BaseItemDto[]) =>
new ListTemplate({
id: uuid.v4(),
sections: [
{
items:
items?.map((item) => {
return {
id: item.Id!,
text: item.Name ?? 'Untitled',
}
}) ?? [],
},
],
onItemSelect: async (item) => {
await TrackPlayer.setQueue(items.map((item) => mapDtoToTrack(item)))
await TrackPlayer.skip(item.index)
await TrackPlayer.play()
CarPlay.pushTemplate(CarPlayNowPlaying())
},
})
export default RecentTracksTemplate
@@ -169,7 +169,7 @@ export default function TrackOptions({
</Text>
<YGroup separator={<Separator />}>
{playlists.map((playlist) => {
{playlists?.map((playlist) => {
return (
<YGroup.Item key={playlist.Id!}>
<ListItem
-1
View File
@@ -1 +0,0 @@
export const CarPlay = null
-2
View File
@@ -1,2 +0,0 @@
import { CarPlay as CarPlayInterface } from 'react-native-carplay'
export const CarPlay = CarPlayInterface
+10 -13
View File
@@ -1,13 +1,10 @@
import { isUndefined } from 'lodash'
import { createContext, ReactNode, SetStateAction, useContext, useEffect, useState } from 'react'
import { CarPlayInterface } from 'react-native-carplay'
import { CarPlay } from 'react-native-carplay'
import Client from '../api/client'
import { CarPlay as NativeCarPlay } from './NativeCarPlay'
// 'react-native-carplay' has also been disabled for android builds in react-native.config.js
const CarPlay = NativeCarPlay as CarPlayInterface | null
const CarPlayNavigation = CarPlay ? require('./CarPlay/Navigation') : null
const CarPlayNowPlaying = CarPlay ? require('./CarPlay/NowPlaying') : null
import CarPlayNavigation from './CarPlay/Navigation'
import CarPlayNowPlaying from './CarPlay/NowPlaying'
import { Platform } from 'react-native'
interface JellifyContext {
loggedIn: boolean
@@ -30,13 +27,13 @@ const JellifyContextInitializer = () => {
function onConnect() {
setCarPlayConnected(true)
if (loggedIn && CarPlay) {
console.debug(CarPlayNavigation)
console.debug(CarPlayNowPlaying)
if (loggedIn) {
CarPlay.setRootTemplate(CarPlayNavigation)
CarPlay.pushTemplate(CarPlayNowPlaying)
CarPlay.setRootTemplate(CarPlayNavigation, true)
CarPlay.pushTemplate(CarPlayNowPlaying, true)
CarPlay.enableNowPlaying(true) // https://github.com/birkir/react-native-carplay/issues/185
if (Platform.OS === 'ios') {
CarPlay.enableNowPlaying(true) // https://github.com/birkir/react-native-carplay/issues/185
}
}
}
+5
View File
@@ -5,6 +5,11 @@ export enum QueryKeys {
ArtistAlbums = 'ARTIST_ALBUMS',
ArtistById = 'ARTIST_BY_ID',
Credentials = 'CREDENTIALS',
/**
* @deprecated Expo Image is being used instead of
* querying for the images with Tanstack
*/
ItemImage = 'IMAGE_BY_ITEM_ID',
Libraries = 'LIBRARIES',
Pause = 'PAUSE',
+15 -2
View File
@@ -1,7 +1,20 @@
jest.mock('react-native-carplay', () => {
return {
default: {
checkForConnection: jest.fn(),
ListTemplate: class {
constructor(config) {
this.config = config
}
},
NowPlayingTemplate: class {
constructor(config) {
this.config = config
}
},
TabBarTemplate: class {
constructor(config) {
this.config = config
}
},
checkForConnection: jest.fn(), // if needed as a named export too
}
})
+2
View File
@@ -1,6 +1,8 @@
// https://github.com/react-native-device-info/react-native-device-info/issues/1360
import mockRNDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock'
jest.mock('../api/client')
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter')
jest.mock('react-native-device-info', () => mockRNDeviceInfo)
+971 -929
View File
File diff suppressed because it is too large Load Diff
+118 -114
View File
@@ -1,115 +1,119 @@
{
"name": "jellify",
"version": "0.10.95",
"private": true,
"scripts": {
"init": "npm i",
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
"clean:ios": "cd ios && pod deintegrate",
"clean:android": "cd android && rm -rf app/ build/",
"pod:install": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=0 bundle exec pod install",
"pod:install-new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
"fastlane:ios:beta": "cd ios && bundle exec fastlane beta",
"fastlane:android:build": "cd android && bundle install && bundle exec fastlane build",
"androidBuild": "cd android && ./gradlew clean && ./gradlew assembleRelease && cd .. && echo 'find apk in android/app/build/outputs/apk/release'",
"prepare": "husky",
"format:check": "prettier --check .",
"format": "prettier --write ."
},
"dependencies": {
"@jellyfin/sdk": "^0.11.0",
"@react-native-community/blur": "^4.4.1",
"@react-native-community/cli": "^15.1.3",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.1.1",
"@react-navigation/stack": "^7.1.0",
"@tamagui/config": "^1.124.17",
"@tamagui/toast": "^1.124.17",
"@tanstack/query-sync-storage-persister": "^5.66.0",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-persist-client": "^5.66.0",
"axios": "^1.7.9",
"bundle": "^2.1.0",
"burnt": "^0.12.2",
"expo": "^52.0.0",
"expo-image": "^2.0.7",
"gem": "^2.4.3",
"invert-color": "^2.0.0",
"jest-expo": "^52.0.6",
"lodash": "^4.17.21",
"npm-bundle": "^3.0.3",
"react": "18.3.1",
"react-freeze": "^1.0.4",
"react-native": "0.77.0",
"react-native-background-actions": "^4.0.1",
"react-native-blurhash": "^2.1.1",
"react-native-boost": "^0.5.5",
"react-native-carplay": "^2.4.1-beta.0",
"react-native-device-info": "^14.0.4",
"react-native-draggable-flatlist": "^4.0.1",
"react-native-file-access": "^3.1.1",
"react-native-gesture-handler": "^2.23.0",
"react-native-haptic-feedback": "^2.3.3",
"react-native-mmkv": "^2.12.2",
"react-native-reanimated": "^3.17.2",
"react-native-safe-area-context": "^5.2.0",
"react-native-screens": "^4.6.0",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.14.0",
"react-native-track-player": "^4.1.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.3",
"react-native-vector-icons": "^10.2.0",
"ruby": "^0.6.1",
"tamagui": "^1.124.17"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli-platform-android": "15.1.3",
"@react-native-community/cli-platform-ios": "15.1.3",
"@react-native/babel-preset": "0.77.0",
"@react-native/eslint-config": "0.77.0",
"@react-native/metro-config": "0.77.0",
"@react-native/typescript-config": "0.77.0",
"@types/jest": "^29.5.13",
"@types/lodash": "^4.17.10",
"@types/react": "^18.2.6",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"babel-plugin-module-resolver": "^5.0.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-native": "^5.0.0",
"husky": "^9.1.7",
"jest": "^29.6.3",
"jscodeshift": "^0.15.2",
"lint-staged": "^15.5.0",
"prettier": "^2.8.8",
"react-native-cli-bump-version": "^1.5.1",
"react-test-renderer": "18.3.1",
"typescript": "5.7.3"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix"
]
},
"engines": {
"node": ">=18"
}
}
"name": "jellify",
"version": "0.10.95",
"private": true,
"scripts": {
"init": "npm i",
"init:ios": "npm i && npm run pod:install",
"reinstall": "rm -rf ./node_modules && npm i",
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
"clean:ios": "cd ios && pod deintegrate",
"clean:android": "cd android && rm -rf app/ build/",
"pod:install": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=0 bundle exec pod install",
"pod:install-new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
"fastlane:ios:beta": "cd ios && bundle exec fastlane beta",
"fastlane:android:build": "cd android && bundle install && bundle exec fastlane build",
"androidBuild": "cd android && ./gradlew clean && ./gradlew assembleRelease && cd .. && echo 'find apk in android/app/build/outputs/apk/release'",
"prepare": "husky",
"format:check": "prettier --check .",
"format": "prettier --write .",
"postinstall": "patch-package"
},
"dependencies": {
"@jellyfin/sdk": "^0.11.0",
"@react-native-community/blur": "^4.4.1",
"@react-native-community/cli": "^15.1.3",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.1.1",
"@react-navigation/stack": "^7.1.0",
"@tamagui/config": "^1.125.34",
"@tamagui/toast": "^1.125.34",
"@tanstack/query-sync-storage-persister": "^5.73.3",
"@tanstack/react-query": "^5.73.3",
"@tanstack/react-query-persist-client": "^5.73.3",
"axios": "^1.7.9",
"bundle": "^2.1.0",
"burnt": "^0.12.2",
"expo": "^52.0.0",
"expo-image": "^2.0.7",
"gem": "^2.4.3",
"invert-color": "^2.0.0",
"jest-expo": "^52.0.6",
"lodash": "^4.17.21",
"npm-bundle": "^3.0.3",
"patch-package": "^8.0.0",
"react": "18.3.1",
"react-freeze": "^1.0.4",
"react-native": "0.77.0",
"react-native-background-actions": "^4.0.1",
"react-native-blurhash": "^2.1.1",
"react-native-boost": "^0.5.5",
"react-native-carplay": "^2.4.1-beta.0",
"react-native-device-info": "^14.0.4",
"react-native-draggable-flatlist": "^4.0.1",
"react-native-file-access": "^3.1.1",
"react-native-gesture-handler": "^2.23.0",
"react-native-haptic-feedback": "^2.3.3",
"react-native-mmkv": "^2.12.2",
"react-native-reanimated": "^3.17.2",
"react-native-safe-area-context": "^5.2.0",
"react-native-screens": "^4.6.0",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.14.0",
"react-native-track-player": "^4.1.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.3",
"react-native-vector-icons": "^10.2.0",
"ruby": "^0.6.1",
"tamagui": "^1.125.34"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli-platform-android": "15.1.3",
"@react-native-community/cli-platform-ios": "15.1.3",
"@react-native/babel-preset": "0.77.0",
"@react-native/eslint-config": "0.77.0",
"@react-native/metro-config": "0.77.0",
"@react-native/typescript-config": "0.77.0",
"@types/jest": "^29.5.13",
"@types/lodash": "^4.17.10",
"@types/react": "^18.2.6",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"babel-plugin-module-resolver": "^5.0.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-native": "^5.0.0",
"husky": "^9.1.7",
"jest": "^29.6.3",
"jscodeshift": "^0.15.2",
"lint-staged": "^15.5.0",
"prettier": "^2.8.8",
"react-native-cli-bump-version": "^1.5.1",
"react-test-renderer": "18.3.1",
"typescript": "5.7.3"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix"
]
},
"engines": {
"node": ">=18"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
diff --git a/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt b/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
index 9d6d869..cffff66 100644
--- a/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
+++ b/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
@@ -120,9 +120,14 @@ class MusicService : HeadlessJsTaskService() {
notificationBuilder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
}
val notification = notificationBuilder.build()
- startForeground(EMPTY_NOTIFICATION_ID, notification)
- @Suppress("DEPRECATION")
- stopForeground(true)
+ try{
+
+ startForeground(EMPTY_NOTIFICATION_ID, notification)
+ @Suppress("DEPRECATION")
+ stopForeground(true)
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to start foreground service")
+ }
}
@MainThread
-7
View File
@@ -4,11 +4,4 @@ module.exports = {
android: {},
},
assets: ['./assets/fonts/'],
dependencies: {
'react-native-carplay': {
platforms: {
android: null, // Disable autolinking for Android
},
},
},
}