diff options
Diffstat (limited to 'TestMediaApp')
13 files changed, 279 insertions, 26 deletions
diff --git a/TestMediaApp/Android.bp b/TestMediaApp/Android.bp index f2f6d59..a2e80e2 100644 --- a/TestMediaApp/Android.bp +++ b/TestMediaApp/Android.bp @@ -23,15 +23,14 @@ android_app { resource_dirs: ["res"], - platform_apis: true, + sdk_version: "system_current", certificate: "platform", - // car_car is ok here because this is meant to simulate a third party media app // Do NOT add dependencies preventing the app from being unbundled (compiled with gradle in Studio). static_libs: [ - "androidx.car_car", "androidx.appcompat_appcompat", + "androidx-constraintlayout_constraintlayout", "androidx.preference_preference", "androidx.legacy_legacy-support-v4", ], diff --git a/TestMediaApp/AndroidManifest.xml b/TestMediaApp/AndroidManifest.xml index 59069c4..4f806c6 100644 --- a/TestMediaApp/AndroidManifest.xml +++ b/TestMediaApp/AndroidManifest.xml @@ -20,7 +20,8 @@ <uses-feature android:name="android.hardware.type.automotive" android:required="true"/> - + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <application android:allowBackup="true" android:label="@string/app_name" @@ -42,6 +43,13 @@ </intent-filter> </service> + <service android:name=".TmaForegroundService" + android:icon="@drawable/ic_app_icon" + android:exported="false" + android:foregroundServiceType="location" + android:label="@string/app_name"> + </service> + <service android:name=".TmaBrowser2" android:icon="@mipmap/ic_launcher" android:exported="true" diff --git a/TestMediaApp/assets/media_items/advanced.json b/TestMediaApp/assets/media_items/advanced.json index 45a18ef..9266a1c 100644 --- a/TestMediaApp/assets/media_items/advanced.json +++ b/TestMediaApp/assets/media_items/advanced.json @@ -54,6 +54,16 @@ "DISPLAY_TITLE": "Exceptions" }, "INCLUDE":"media_items/exceptions.json" + }, + { + "FLAGS": "playable", + "METADATA": { + "MEDIA_ID": "location", + "DISPLAY_TITLE": "Location", + "DURATION": 10000, + "ART_URI": "drawable/ic_location" + }, + "CUSTOM_ACTIONS": ["REQUEST_LOCATION"] } ] }
\ No newline at end of file diff --git a/TestMediaApp/assets/media_items/album_art/art_nodes.json b/TestMediaApp/assets/media_items/album_art/art_nodes.json index 692809f..abfc869 100644 --- a/TestMediaApp/assets/media_items/album_art/art_nodes.json +++ b/TestMediaApp/assets/media_items/album_art/art_nodes.json @@ -61,6 +61,16 @@ "DISPLAY_TITLE": "Nature files" }, "INCLUDE":"media_items/album_art/nature/art_nature_files.json" + }, + { + "FLAGS": "browsable", + "PLAYABLE_HINT": "GRID", + "METADATA": { + "MEDIA_ID": "album_art/art_nodes nature self updating", + "DISPLAY_TITLE": "Nature self updating" + }, + "SELF_UPDATE_MS": "2000", + "INCLUDE":"media_items/album_art/nature/art_nature_512.json" } ] }
\ No newline at end of file diff --git a/TestMediaApp/res/drawable/ic_location.xml b/TestMediaApp/res/drawable/ic_location.xml new file mode 100644 index 0000000..1017cbc --- /dev/null +++ b/TestMediaApp/res/drawable/ic_location.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/> +</vector> diff --git a/TestMediaApp/res/values/strings.xml b/TestMediaApp/res/values/strings.xml index ac621be..1ffd39c 100644 --- a/TestMediaApp/res/values/strings.xml +++ b/TestMediaApp/res/values/strings.xml @@ -31,4 +31,5 @@ <string name="heart_less_less" translatable="false">Heart--</string> + <string name="location" translatable="false">Location</string> </resources> diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java index 7a62137..3c3eeb6 100644 --- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java +++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaBrowser.java @@ -204,11 +204,24 @@ public class TmaBrowser extends MediaBrowserServiceCompat { addSearchResults(node, pat.matcher(""), hits, MAX_SEARCH_DEPTH); result.sendResult(hits); } else { - List<MediaItem> items = new ArrayList<>(node.mChildren.size()); - for (TmaMediaItem child : node.mChildren) { - items.add(child.toMediaItem()); + List<TmaMediaItem> children = node.getChildren(); + int childrenCount = children.size(); + List<MediaItem> items = new ArrayList<>(childrenCount); + if (childrenCount <= 0) { + result.sendResult(items); + } else { + int selfUpdateDelay = node.getSelfUpdateDelay(); + int toShow = (selfUpdateDelay > 0) ? 1 + node.mRevealCounter : childrenCount; + for (int childIndex = 0 ; childIndex < toShow; childIndex++) { + items.add(children.get(childIndex).toMediaItem()); + } + result.sendResult(items); + + if (selfUpdateDelay > 0) { + mHandler.postDelayed(new UpdateNodeTask(parentId), selfUpdateDelay); + node.mRevealCounter = (node.mRevealCounter + 1) % (childrenCount); + } } - result.sendResult(items); } }; if (delay == TmaReplyDelay.NONE) { @@ -225,7 +238,7 @@ public class TmaBrowser extends MediaBrowserServiceCompat { return; } - for (TmaMediaItem child : node.mChildren) { + for (TmaMediaItem child : node.getChildren()) { MediaItem item = child.toMediaItem(); CharSequence title = item.getDescription().getTitle(); if (title != null) { @@ -240,4 +253,18 @@ public class TmaBrowser extends MediaBrowserServiceCompat { addSearchResults(child, matcher, hits, currentDepth - 1); } } + + private class UpdateNodeTask implements Runnable { + + private final String mNodeId; + + UpdateNodeTask(@NonNull String nodeId) { + mNodeId = nodeId; + } + + @Override + public void run() { + notifyChildrenChanged(mNodeId); + } + } } diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaForegroundService.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaForegroundService.java new file mode 100644 index 0000000..6241455 --- /dev/null +++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaForegroundService.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.media.testmediaapp; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.os.IBinder; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import com.android.car.media.testmediaapp.prefs.TmaPrefsActivity; + +/** + * Service used to test and demonstrate the access to "foreground" permissions. In particular, this + * implementation deals with location access from a headless service. This service is initiated + * using {@link Service#startService(Intent)} from the browse service as a respond to a custom + * playback command. Subsequent start commands make the service toggle between running and stopping. + * + * In real applications, this service would be handling background playback, maybe using location + * and other sensors to automatically select songs. + */ +public class TmaForegroundService extends Service { + public static final String CHANNEL_ID = "ForegroundServiceChannel"; + private LocationManager mLocationManager; + + @Override + public void onCreate() { + super.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (doWork()) { + createNotificationChannel(); + Intent notificationIntent = new Intent(this, TmaPrefsActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(this, + 0, notificationIntent, 0); + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Foreground Service") + .setSmallIcon(R.drawable.ic_app_icon) + .setContentIntent(pendingIntent) + .build(); + startForeground(1, notification); + } else { + getMainExecutor().execute(this::stopSelf); + } + + return START_NOT_STICKY; + } + + @Override + public void onDestroy() { + if (mLocationManager != null) { + mLocationManager.removeUpdates(mLocationListener); + toast("Location is off"); + } + super.onDestroy(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void createNotificationChannel() { + NotificationChannel serviceChannel = new NotificationChannel( + CHANNEL_ID, + "Foreground Service Channel", + NotificationManager.IMPORTANCE_DEFAULT + ); + NotificationManager manager = getSystemService(NotificationManager.class); + manager.createNotificationChannel(serviceChannel); + } + + private boolean doWork() { + if (mLocationManager != null) { + return false; + } + mLocationManager = (LocationManager) getSystemService(LOCATION_SERVICE); + try { + mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, + 0, mLocationListener); + toast("Location is on"); + } catch (Throwable e) { + toast("Unable to get location: " + e.getMessage()); + } + return true; + } + + /** + * We use toasts here as it is the only way for a headless service to show something on the + * screen. Real application shouldn't be using toasts from service. + */ + private void toast(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + + private final LocationListener mLocationListener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + toast("Location provider: " + location.getLatitude() + ":" + location.getLongitude()); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + toast("Location provider: " + provider + " status changed to: " + status); + } + + @Override + public void onProviderEnabled(String provider) { + toast("Location provider enabled: " + provider); + } + + @Override + public void onProviderDisabled(String provider) { + toast("Location provider disabled: " + provider); + } + }; +} diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java index 327a2b6..5bd74ad 100644 --- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java +++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaLibrary.java @@ -66,8 +66,9 @@ class TmaLibrary { TmaMediaItem getMediaItemById(String mediaId) { TmaMediaItem result = mMediaItemsByMediaId.get(mediaId); // Processing includes only on request allows recursive structures :-) - if (result != null && !TextUtils.isEmpty(result.mInclude)) { - result = result.append(loadAssetFile(result.mInclude).mChildren); + if (result != null && !TextUtils.isEmpty(result.mInclude) + && result.getChildren().isEmpty()) { + result.setChildren(loadAssetFile(result.mInclude).getChildren()); } return result; } @@ -89,7 +90,7 @@ class TmaLibrary { private void cacheMediaItem(TmaMediaItem item) { String key = item.getMediaId(); if (mMediaItemsByMediaId.putIfAbsent(key, item) == null) { - for (TmaMediaItem child : item.mChildren) { + for (TmaMediaItem child : item.getChildren()) { cacheMediaItem(child); } } else { diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaItem.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaItem.java index af1b2e3..e2cb533 100644 --- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaItem.java +++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaMediaItem.java @@ -55,7 +55,9 @@ public class TmaMediaItem { HEART_PLUS_PLUS(CUSTOM_ACTION_PREFIX + "heart_plus_plus", R.string.heart_plus_plus, R.drawable.ic_heart_plus_plus), HEART_LESS_LESS(CUSTOM_ACTION_PREFIX + "heart_less_less", R.string.heart_less_less, - R.drawable.ic_heart_less_less); + R.drawable.ic_heart_less_less), + REQUEST_LOCATION(CUSTOM_ACTION_PREFIX + "location", R.string.location, + R.drawable.ic_location); final String mId; final int mNameId; @@ -66,16 +68,17 @@ public class TmaMediaItem { mNameId = name; mIcon = icon; } - } private final int mFlags; private final MediaMetadataCompat mMediaMetadata; private final ContentStyle mPlayableStyle; private final ContentStyle mBrowsableStyle; + private final int mSelfUpdateMs; - /** Read only list. */ - final List<TmaMediaItem> mChildren; + + /** Internally modifiable list (for includes). */ + private final List<TmaMediaItem> mChildren; /** Read only list. */ private final List<TmaMediaItem> mPlayableChildren; /** Read only list. */ @@ -87,18 +90,20 @@ public class TmaMediaItem { private @Nullable TmaMediaItem mParent; int mHearts; + int mRevealCounter; public TmaMediaItem(int flags, ContentStyle playableStyle, ContentStyle browsableStyle, - MediaMetadataCompat metadata, List<TmaCustomAction> customActions, - List<TmaMediaEvent> mediaEvents, + MediaMetadataCompat metadata, int selfUpdateMs, + List<TmaCustomAction> customActions, List<TmaMediaEvent> mediaEvents, List<TmaMediaItem> children, String include) { mFlags = flags; mPlayableStyle = playableStyle; mBrowsableStyle = browsableStyle; mMediaMetadata = metadata; + mSelfUpdateMs = selfUpdateMs; mCustomActions = Collections.unmodifiableList(customActions); - mChildren = Collections.unmodifiableList(children); + mChildren = children; mMediaEvents = Collections.unmodifiableList(mediaEvents); mInclude = include; List<TmaMediaItem> playableChildren = new ArrayList<>(children.size()); @@ -115,6 +120,14 @@ public class TmaMediaItem { mParent = parent; } + int getSelfUpdateDelay() { + return mSelfUpdateMs; + } + + List<TmaMediaItem> getChildren() { + return Collections.unmodifiableList(mChildren); + } + @Nullable TmaMediaItem getParent() { return mParent; @@ -155,12 +168,9 @@ public class TmaMediaItem { return result; } - TmaMediaItem append(List<TmaMediaItem> children) { - List<TmaMediaItem> allChildren = new ArrayList<>(mChildren.size() + children.size()); - allChildren.addAll(mChildren); - allChildren.addAll(children); - return new TmaMediaItem(mFlags, mPlayableStyle, mBrowsableStyle, mMediaMetadata, - mCustomActions, mMediaEvents, allChildren, null); + void setChildren(List<TmaMediaItem> children) { + mChildren.clear(); + mChildren.addAll(children); } void updateSessionMetadata(MediaSessionCompat session) { diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java index d8fab6c..65cc787 100644 --- a/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java +++ b/TestMediaApp/src/com/android/car/media/testmediaapp/TmaPlayer.java @@ -30,6 +30,7 @@ import static android.support.v4.media.session.PlaybackStateCompat.ERROR_CODE_AP import static android.support.v4.media.session.PlaybackStateCompat.STATE_ERROR; import androidx.annotation.Nullable; + import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -77,7 +78,6 @@ public class TmaPlayer extends MediaSessionCompat.Callback { private TmaMediaItem mActiveItem; private int mNextEventIndex = -1; - TmaPlayer(Context context, TmaLibrary library, AudioManager audioManager, Handler handler, MediaSessionCompat session) { mContext = context; @@ -248,6 +248,8 @@ public class TmaPlayer extends MediaSessionCompat.Callback { } else if (TmaCustomAction.HEART_LESS_LESS.mId.equals(action)) { mActiveItem.mHearts--; toast("" + mActiveItem.mHearts); + } else if (TmaCustomAction.REQUEST_LOCATION.mId.equals(action)) { + mContext.startService(new Intent(mContext, TmaForegroundService.class)); } } } diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaItemReader.java b/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaItemReader.java index 2d4b845..e62055d 100644 --- a/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaItemReader.java +++ b/TestMediaApp/src/com/android/car/media/testmediaapp/loader/TmaMediaItemReader.java @@ -24,6 +24,7 @@ import static com.android.car.media.testmediaapp.loader.TmaLoaderUtils.enumNames import static com.android.car.media.testmediaapp.loader.TmaLoaderUtils.getArray; import static com.android.car.media.testmediaapp.loader.TmaLoaderUtils.getEnum; import static com.android.car.media.testmediaapp.loader.TmaLoaderUtils.getEnumArray; +import static com.android.car.media.testmediaapp.loader.TmaLoaderUtils.getInt; import static com.android.car.media.testmediaapp.loader.TmaLoaderUtils.getString; import android.util.Log; @@ -55,6 +56,7 @@ class TmaMediaItemReader { PLAYABLE_HINT, BROWSABLE_HINT, METADATA, + SELF_UPDATE_MS, CHILDREN, INCLUDE, CUSTOM_ACTIONS, @@ -114,6 +116,7 @@ class TmaMediaItemReader { getEnum(json, Keys.PLAYABLE_HINT, mContentStyles, ContentStyle.NONE), getEnum(json, Keys.BROWSABLE_HINT, mContentStyles, ContentStyle.NONE), mMediaMetadataReader.fromJson(json.getJSONObject(Keys.METADATA.name())), + getInt(json, Keys.SELF_UPDATE_MS), getEnumArray(json, Keys.CUSTOM_ACTIONS, mCustomActions), mediaEvents, mediaItems, getString(json, Keys.INCLUDE)); } catch (JSONException e) { diff --git a/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefsFragment.java b/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefsFragment.java index 066cc9c..d3e681e 100644 --- a/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefsFragment.java +++ b/TestMediaApp/src/com/android/car/media/testmediaapp/prefs/TmaPrefsFragment.java @@ -16,8 +16,12 @@ package com.android.car.media.testmediaapp.prefs; +import android.Manifest; +import android.app.Activity; import android.content.Context; +import android.content.pm.PackageManager; import android.os.Bundle; +import android.widget.Toast; import androidx.preference.DropDownPreference; import androidx.preference.Preference; @@ -30,6 +34,8 @@ import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaLoginEventOrder; import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaReplyDelay; import com.android.car.media.testmediaapp.prefs.TmaPrefs.PrefEntry; +import java.util.function.Consumer; + public class TmaPrefsFragment extends PreferenceFragmentCompat { @Override @@ -49,6 +55,8 @@ public class TmaPrefsFragment extends PreferenceFragmentCompat { prefs.mAssetReplyDelay, TmaReplyDelay.values())); screen.addPreference(createEnumPref(context, "Login event order", prefs.mLoginEventOrder, TmaLoginEventOrder.values())); + screen.addPreference(createClickPref(context, "Request location perm", + this::requestPermissions)); setPreferenceScreen(screen); } @@ -72,4 +80,25 @@ public class TmaPrefsFragment extends PreferenceFragmentCompat { prefWidget.setEntryValues(entryValues); return prefWidget; } + + private Preference createClickPref(Context context, String title, Consumer<Context> runnable) { + Preference prefWidget = new Preference(context); + prefWidget.setTitle(title); + prefWidget.setOnPreferenceClickListener(pref -> { + runnable.accept(context); + return true; + }); + return prefWidget; + } + + private void requestPermissions(Context context) { + if (context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED) { + Toast.makeText(context, "Location permission already granted", Toast.LENGTH_SHORT) + .show(); + } else { + ((Activity) context).requestPermissions( + new String[] {Manifest.permission.ACCESS_FINE_LOCATION}, 1); + } + } } |
