Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Android Auto Support #2094

Open
wants to merge 84 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
d99812b
feat: frankenstein HeadlessJsTaskService + MediaBrowserServiceCompat
lovegaoshi Jun 15, 2023
e90b886
feat: android auto
Jun 15, 2023
04c27d0
feat: b4 implementing setCommand
lovegaoshi Jun 18, 2023
2cfa59a
feat: android auto
lovegaoshi Jun 20, 2023
8734a43
Create automotive_app_desc.xml
lovegaoshi Jun 20, 2023
ba3afe4
Update AndroidManifest.xml
lovegaoshi Jun 20, 2023
0a3f2a1
Revert "Update AndroidManifest.xml"
lovegaoshi Jun 20, 2023
e0a9246
Update build.gradle
lovegaoshi Jun 21, 2023
6df90ac
Update build.gradle
lovegaoshi Jun 26, 2023
b28f50d
Merge branch 'dev-android-auto' of https://github.com/lovegaoshi/reac…
lovegaoshi Jun 27, 2023
1977e7d
Update build.gradle
lovegaoshi Jun 27, 2023
19086af
fix: revert changes in targetSdkVersion
lovegaoshi Jun 27, 2023
2a6e765
chore: resolve
Jul 10, 2023
ed2f430
Merge branch 'doublesymmetry-main' into dev-android-auto
Jul 10, 2023
77655a8
Update build.gradle
lovegaoshi Jul 10, 2023
9d0589e
feat: AA MediaSession.Callback test
lovegaoshi Jul 12, 2023
e256990
feat: emit MediaSession.Callback
Jul 12, 2023
e7d2a5a
feat: loadMediaItems
Jul 12, 2023
0cbfe06
fix: KA mediaSession.Callback
lovegaoshi Jul 13, 2023
d99c436
feat: loadBrowseTree
Jul 13, 2023
329562a
feat: loadBrowseTree
Jul 13, 2023
da4a181
feat: AA content hierarchy
lovegaoshi Jul 14, 2023
ff4c15b
Merge pull request #5 from lovegaoshi/dev-android-auto
lovegaoshi Jul 14, 2023
29a43fb
feat: RemoteSkip
lovegaoshi Jul 17, 2023
66156cd
Merge pull request #6 from lovegaoshi/dev-android-auto
lovegaoshi Jul 17, 2023
325a23d
Update build.gradle
lovegaoshi Jul 19, 2023
d882a0a
Update build.gradle
lovegaoshi Jul 19, 2023
932f299
fix: android only remote services
lovegaoshi Jul 20, 2023
9a30009
fix: refresh browseTree content
lovegaoshi Jul 24, 2023
333d7a4
Merge pull request #7 from doublesymmetry/main
lovegaoshi Jul 24, 2023
b739a13
feat: AA content style
lovegaoshi Jul 25, 2023
95e0927
Merge branch 'dev-android-auto-PR' into dev-android-auto
lovegaoshi Jul 25, 2023
55adabb
Merge pull request #8 from lovegaoshi/dev-android-auto
lovegaoshi Jul 25, 2023
de77707
fix: cycling dependency
lovegaoshi Jul 25, 2023
7215788
Merge branch 'dev-android-auto' of https://github.com/lovegaoshi/reac…
lovegaoshi Jul 25, 2023
7cfdf0b
Merge branch 'dev-android-auto' of https://github.com/lovegaoshi/reac…
lovegaoshi Jul 25, 2023
5643a83
FIX: MERGE CONFLICT
lovegaoshi Jul 25, 2023
5971065
feat: open UI when create service
lovegaoshi Jul 28, 2023
aee71eb
Merge branch 'main' of https://github.com/doublesymmetry/react-native…
lovegaoshi Aug 13, 2023
e2f95d0
fix: sync up KotlinAudio
lovegaoshi Aug 13, 2023
63f6e33
Merge branch 'main' of https://github.com/doublesymmetry/react-native…
lovegaoshi Aug 19, 2023
9cd6ebb
chore: upgrade KA
lovegaoshi Aug 19, 2023
62ea3ec
feat(musicmodule): adds support for grouping lists with subheading
schoetty Aug 23, 2023
1a96713
Merge pull request #11 from schoetty/dev-android-auto
lovegaoshi Aug 24, 2023
58f64ca
feat: startActivity permission
lovegaoshi Aug 24, 2023
f8778a9
chore: grouping list example
lovegaoshi Aug 24, 2023
1a5ed5c
fix(example): startActivity permission
lovegaoshi Aug 24, 2023
ebd4aca
Merge pull request #12 from doublesymmetry/main
lovegaoshi Aug 26, 2023
f3c9c78
fix(example): AA declarations
lovegaoshi Aug 26, 2023
430bc66
fix: linting
lovegaoshi Aug 26, 2023
014141c
fix: linting
lovegaoshi Aug 29, 2023
4e2730d
feat: remoteBrowse
lovegaoshi Aug 29, 2023
bf0062b
fix: lint
lovegaoshi Aug 29, 2023
11b5ff8
Merge pull request #17 from doublesymmetry/main
lovegaoshi Sep 13, 2023
7411ef5
Merge pull request #19 from lovegaoshi/dev-android-auto
lovegaoshi Sep 19, 2023
670548e
Merge branch 'main' of https://github.com/doublesymmetry/react-native…
lovegaoshi Sep 19, 2023
8186ddd
Merge pull request #22 from lovegaoshi/dev-android-auto-PR
lovegaoshi Sep 19, 2023
bf538ba
feat(musicmodule.kt): adds per-item content styles
schoetty Sep 28, 2023
888d6a9
chore: per-item content styles example
lovegaoshi Oct 2, 2023
01cfcd4
Merge branch 'main' of https://github.com/doublesymmetry/react-native…
lovegaoshi Oct 13, 2023
34e5d25
fix: per-item content styles example
lovegaoshi Oct 13, 2023
31170ae
Merge branch 'main' of https://github.com/doublesymmetry/react-native…
lovegaoshi Oct 17, 2023
9415a9d
Merge branch 'main' of https://github.com/doublesymmetry/react-native…
lovegaoshi Oct 23, 2023
046e14e
fix: onCreate wake activity condition
lovegaoshi Oct 23, 2023
a07cd8b
fix(android): wake activity logic
lovegaoshi Oct 23, 2023
db09dc4
fix: wake activity logic
lovegaoshi Oct 24, 2023
9eea5fa
docs(android): android auto docs
lovegaoshi Oct 25, 2023
4d352eb
Merge branch 'doublesymmetry:main' into dev-android-auto
lovegaoshi Oct 31, 2023
d4378aa
doc(android): android auto
lovegaoshi Nov 1, 2023
bf7d400
Merge branch 'main' of https://github.com/doublesymmetry/react-native…
lovegaoshi Feb 3, 2024
f9eb2f3
chore: sync up main
lovegaoshi Feb 3, 2024
de53eb7
feat: enables playback progress on Android Auto list items
schoetty Jan 30, 2024
d6c9d2b
chore: aA playback progress demo
lovegaoshi Feb 9, 2024
0e447bb
Merge branch 'schoetty-dev-android-auto' into dev-android-auto
lovegaoshi Feb 9, 2024
0695653
Merge branch 'main' into dev-android-auto
lovegaoshi Mar 7, 2024
9757c89
Merge branch 'main' of https://github.com/lovegaoshi/react-native-tra…
lovegaoshi Mar 14, 2024
420dd50
feat: sync up main
lovegaoshi Mar 25, 2024
a5451f7
Merge branch 'main' of https://github.com/lovegaoshi/react-native-tra…
lovegaoshi Mar 27, 2024
44d6a47
fix: remove redundant file
lovegaoshi Apr 19, 2024
7715f12
Merge branch 'main' of https://github.com/lovegaoshi/react-native-tra…
lovegaoshi Apr 19, 2024
df0efa2
Merge branch 'doublesymmetry:main' into dev-android-auto
lovegaoshi May 3, 2024
eee3e45
docs(android): android auto docs update
lovegaoshi May 7, 2024
fddbc54
fix: map artist to subtitle
lovegaoshi May 16, 2024
6f6af76
Update android-auto.md
lovegaoshi May 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ android {
compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
namespace 'com.doublesymmetry.trackplayer'

def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
defaultConfig {
minSdkVersion getExtOrIntegerDefault('minSdkVersion') // RN's minimum version
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
Expand Down Expand Up @@ -50,7 +51,7 @@ repositories {
}

dependencies {
implementation 'com.github.doublesymmetry:kotlinaudio:v2.1.0'
implementation 'com.github.lovegaoshi:KotlinAudio:v2.0.0-aa23'
// used when building against local maven
// implementation "com.github.doublesymmetry:kotlin-audio:2.1.0"

Expand All @@ -62,4 +63,5 @@ dependencies {
implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0"
implementation "androidx.lifecycle:lifecycle-process:2.5.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3"
implementation 'androidx.media:media:1.6.0'
}
9 changes: 7 additions & 2 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@

<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<application>

<!-- The main service, handles playback, playlists and media buttons -->
<service android:name="com.doublesymmetry.trackplayer.service.MusicService" android:enabled="true" android:exported="true" android:foregroundServiceType="mediaPlayback">
<service
android:name="com.doublesymmetry.trackplayer.service.MusicService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.doublesymmetry.trackplayer;

import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.PowerManager;

import androidx.annotation.Nullable;
import androidx.media.MediaBrowserServiceCompat;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import com.facebook.react.jstasks.HeadlessJsTaskContext;
import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactInstanceEventListener;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactApplication;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import timber.log.Timber;

/**
* Base class for running JS without a UI. Generally, you only need to override {@link
* #getTaskConfig}, which is called for every {@link #onStartCommand}. The result, if not {@code
* null}, is used to run a JS task.
*
* <p>If you need more fine-grained control over how tasks are run, you can override {@link
* #onStartCommand} and call {@link #startTask} depending on your custom logic.
*
* <p>If you're starting a {@code HeadlessJsTaskService} from a {@code BroadcastReceiver} (e.g.
* handling push notifications), make sure to call {@link #acquireWakeLockNow} before returning from
* {@link BroadcastReceiver#onReceive}, to make sure the device doesn't go to sleep before the
* service is started.
*/
public abstract class HeadlessJsMediaService extends MediaBrowserServiceCompat implements HeadlessJsTaskEventListener {

private final Set<Integer> mActiveTasks = new CopyOnWriteArraySet<>();
private static @Nullable PowerManager.WakeLock sWakeLock;



@Override
public int onStartCommand(Intent intent, int flags, int startId) {
HeadlessJsTaskConfig taskConfig = getTaskConfig(intent);
if (taskConfig != null) {
startTask(taskConfig);
return START_REDELIVER_INTENT;
}
return START_NOT_STICKY;
}

/**
* Called from {@link #onStartCommand} to create a {@link HeadlessJsTaskConfig} for this intent.
*
* @param intent the {@link Intent} received in {@link #onStartCommand}.
* @return a {@link HeadlessJsTaskConfig} to be used with {@link #startTask}, or {@code null} to
* ignore this command.
*/
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
return null;
}

/**
* Acquire a wake lock to ensure the device doesn't go to sleep while processing background tasks.
*/
@SuppressLint("WakelockTimeout")
public static void acquireWakeLockNow(Context context) {
if (sWakeLock == null || !sWakeLock.isHeld()) {
PowerManager powerManager =
Assertions.assertNotNull((PowerManager) context.getSystemService(POWER_SERVICE));
sWakeLock =
powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, HeadlessJsMediaService.class.getCanonicalName());
sWakeLock.setReferenceCounted(false);
sWakeLock.acquire();
}
}

@Override
public @Nullable IBinder onBind(Intent intent) {
return super.onBind(intent);
}

@Override
public void onCreate() {
super.onCreate();
}

/**
* Start a task. This method handles starting a new React instance if required.
*
* <p>Has to be called on the UI thread.
*
* @param taskConfig describes what task to start and the parameters to pass to it
*/
protected void startTask(final HeadlessJsTaskConfig taskConfig) {
UiThreadUtil.assertOnUiThread();
acquireWakeLockNow(this);
final ReactInstanceManager reactInstanceManager =
getReactNativeHost().getReactInstanceManager();
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
if (reactContext == null) {
reactInstanceManager.addReactInstanceEventListener(
new ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext reactContext) {
invokeStartTask(reactContext, taskConfig);
reactInstanceManager.removeReactInstanceEventListener(this);
}
});
reactInstanceManager.createReactContextInBackground();
} else {
invokeStartTask(reactContext, taskConfig);
}
}

private void invokeStartTask(ReactContext reactContext, final HeadlessJsTaskConfig taskConfig) {
final HeadlessJsTaskContext headlessJsTaskContext =
HeadlessJsTaskContext.getInstance(reactContext);
headlessJsTaskContext.addTaskEventListener(this);

UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
int taskId = headlessJsTaskContext.startTask(taskConfig);
mActiveTasks.add(taskId);
}
});
}

@Override
public void onDestroy() {
super.onDestroy();

if (getReactNativeHost().hasInstance()) {
ReactInstanceManager reactInstanceManager = getReactNativeHost().getReactInstanceManager();
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
if (reactContext != null) {
HeadlessJsTaskContext headlessJsTaskContext =
HeadlessJsTaskContext.getInstance(reactContext);
headlessJsTaskContext.removeTaskEventListener(this);
}
}
if (sWakeLock != null) {
sWakeLock.release();
}
}

@Override
public void onHeadlessJsTaskStart(int taskId) {}

@Override
public void onHeadlessJsTaskFinish(int taskId) {
mActiveTasks.remove(taskId);
if (mActiveTasks.size() == 0) {
stopSelf();
}
}

/**
* Get the {@link ReactNativeHost} used by this app. By default, assumes {@link #getApplication()}
* is an instance of {@link ReactApplication} and calls {@link
* ReactApplication#getReactNativeHost()}. Override this method if your application class does not
* implement {@code ReactApplication} or you simply have a different mechanism for storing a
* {@code ReactNativeHost}, e.g. as a static field somewhere.
*/
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getApplication()).getReactNativeHost();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class MusicEvents(private val reactContext: ReactContext) : BroadcastReceiver()
const val BUTTON_JUMP_FORWARD = "remote-jump-forward"
const val BUTTON_JUMP_BACKWARD = "remote-jump-backward"
const val BUTTON_DUCK = "remote-duck"
const val BUTTON_BROWSE = "remote-browse"

// Playback Events
const val PLAYBACK_PLAY_WHEN_READY_CHANGED = "playback-play-when-ready-changed"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.doublesymmetry.trackplayer.module

import android.content.*
import android.media.MediaDescription
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.net.Uri
import android.support.v4.media.RatingCompat
import android.support.v4.media.MediaBrowserCompat.MediaItem
import android.support.v4.media.MediaDescriptionCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.media.utils.MediaConstants
import com.doublesymmetry.kotlinaudio.models.Capability
import com.doublesymmetry.kotlinaudio.models.RepeatMode
import com.doublesymmetry.trackplayer.extensions.NumberExt.Companion.toMilliseconds
Expand Down Expand Up @@ -90,6 +95,68 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
return Track(context, bundle, musicService.ratingType)
}

private fun hashmapToMediaItem(hashmap: HashMap<String, String>): MediaItem {

val mediaId = hashmap["mediaId"]
val title = hashmap["title"]
val subtitle = hashmap["subtitle"]
val mediaUri = hashmap["mediaUri"]
val iconUri = hashmap["iconUri"]
val playableFlag = if (hashmap["playable"]?.toInt() == 1) MediaItem.FLAG_BROWSABLE else MediaItem.FLAG_PLAYABLE

val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
mediaDescriptionBuilder.setMediaId(mediaId)
mediaDescriptionBuilder.setTitle(title)
mediaDescriptionBuilder.setSubtitle(subtitle)
mediaDescriptionBuilder.setMediaUri(if (mediaUri != null) Uri.parse(mediaUri) else null)
mediaDescriptionBuilder.setIconUri(if (iconUri != null) Uri.parse(iconUri) else null)
val extras = Bundle()
hashmap["groupTitle"]?.let {
extras.putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, it)
}
hashmap["contentStyle"]?.toInt()?.let {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM, it)
}
hashmap["childrenPlayableContentStyle"]?.toInt()?.let {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, it)
}
hashmap["childrenBrowsableContentStyle"]?.toInt()?.let {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, it)
}

// playbackProgress should contain a string representation of a number between 0 and 1 if present
hashmap["playbackProgress"]?.toDouble()?.let {
if (it > 0.98) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED)
} else if (it == 0.0) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, it)
}
}

mediaDescriptionBuilder.setExtras(extras)
return MediaItem(mediaDescriptionBuilder.build(), playableFlag)
}

private fun readableArrayToMediaItems(data: ArrayList<HashMap<String, String>>): MutableList<MediaItem> {
return data.map {
hashmapToMediaItem(it)
}.toMutableList()
}

private fun rejectWithException(callback: Promise, exception: Exception) {
when (exception) {
is RejectionException -> {
Expand Down Expand Up @@ -626,4 +693,36 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(Arguments.fromBundle(musicService.getPlayerStateBundle(musicService.state)))
}

@ReactMethod
fun setBrowseTree(mediaItems: ReadableMap, callback: Promise) = scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
val mediaItemsMap = mediaItems.toHashMap()
musicService.mediaTree = mediaItemsMap.mapValues { readableArrayToMediaItems(it.value as ArrayList<HashMap<String, String>>) }
Timber.d("refreshing browseTree")
mediaItemsMap.keys.forEach {
musicService.notifyChildrenChanged(it)
}
callback.resolve(musicService.mediaTree.toString())
}

@ReactMethod
// this method doesn't seem to affect style after onGetRoot is called, and won't change if notifyChildrenChanged is emitted.
fun setBrowseTreeStyle(browsableStyle: Int, playableStyle: Int, callback: Promise) = scope.launch {
fun getStyle(check: Int): Int {
return when (check) {
2 -> MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
3 -> MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM
4 -> MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM
else -> MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
}
}
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.mediaTreeStyle = listOf(
getStyle(browsableStyle),
getStyle(playableStyle)
)
callback.resolve(null)
}

}
Loading
Loading