From 17d47989ee53c9e54f250d29a343ba949edf0ff9 Mon Sep 17 00:00:00 2001
From: Yao Chen
Date: Thu, 17 Jul 2014 14:39:34 -0700
Subject: [PATCH] Updated media browsing API & tests.
Change-Id: I3a1e3eead99a98b1890e05843dd90203cee517b7
---
Android.mk | 8 +-
api/current.txt | 71 ++
.../media/browse/IMediaBrowserService.aidl | 20 +
.../browse/IMediaBrowserServiceCallbacks.aidl | 24 +
.../android/media/browse/MediaBrowser.java | 704 ++++++++++++++++++
.../media/browse/MediaBrowserItem.java | 241 ++++++
.../media/browse/MediaBrowserService.java | 363 +++++++++
.../android/server/ConnectivityService.java | 4 +-
tests/MusicBrowserDemo/Android.mk | 35 +
tests/MusicBrowserDemo/AndroidManifest.xml | 45 ++
.../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4805 bytes
.../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2592 bytes
.../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 5246 bytes
.../res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 14755 bytes
tests/MusicBrowserDemo/res/values/strings.xml | 21 +
tests/MusicBrowserDemo/res/values/styles.xml | 36 +
.../musicbrowserdemo/AppListFragment.java | 149 ++++
.../musicbrowserdemo/BrowserListFragment.java | 212 ++++++
.../musicbrowserdemo/MainActivity.java | 48 ++
tests/MusicServiceDemo/Android.mk | 35 +
tests/MusicServiceDemo/AndroidManifest.xml | 55 ++
tests/MusicServiceDemo/proguard-project.txt | 20 +
.../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4805 bytes
.../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2592 bytes
.../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 5246 bytes
.../res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 14755 bytes
.../res/drawable-xxhdpi/thumbsup.png | Bin 0 -> 1632 bytes
.../res/layout/activity_main.xml | 23 +
.../res/layout/fragment_main.xml | 32 +
tests/MusicServiceDemo/res/values/colors.xml | 22 +
tests/MusicServiceDemo/res/values/dimens.xml | 23 +
tests/MusicServiceDemo/res/values/strings.xml | 25 +
tests/MusicServiceDemo/res/values/styles.xml | 36 +
.../musicservicedemo/BrowserService.java | 246 ++++++
.../musicservicedemo/MainActivity.java | 101 +++
.../android/musicservicedemo/Utils.java | 77 ++
.../browser/MusicProvider.java | 266 +++++++
.../browser/MusicProviderTask.java | 70 ++
.../browser/MusicProviderTaskListener.java | 24 +
.../musicservicedemo/browser/MusicTrack.java | 138 ++++
40 files changed, 3169 insertions(+), 5 deletions(-)
create mode 100644 media/java/android/media/browse/IMediaBrowserService.aidl
create mode 100644 media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl
create mode 100644 media/java/android/media/browse/MediaBrowser.java
create mode 100644 media/java/android/media/browse/MediaBrowserItem.java
create mode 100644 media/java/android/media/browse/MediaBrowserService.java
create mode 100644 tests/MusicBrowserDemo/Android.mk
create mode 100644 tests/MusicBrowserDemo/AndroidManifest.xml
create mode 100644 tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png
create mode 100644 tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png
create mode 100644 tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png
create mode 100644 tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png
create mode 100644 tests/MusicBrowserDemo/res/values/strings.xml
create mode 100644 tests/MusicBrowserDemo/res/values/styles.xml
create mode 100644 tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java
create mode 100644 tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/BrowserListFragment.java
create mode 100644 tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/MainActivity.java
create mode 100644 tests/MusicServiceDemo/Android.mk
create mode 100644 tests/MusicServiceDemo/AndroidManifest.xml
create mode 100644 tests/MusicServiceDemo/proguard-project.txt
create mode 100644 tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png
create mode 100644 tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png
create mode 100644 tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png
create mode 100644 tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png
create mode 100644 tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png
create mode 100644 tests/MusicServiceDemo/res/layout/activity_main.xml
create mode 100644 tests/MusicServiceDemo/res/layout/fragment_main.xml
create mode 100644 tests/MusicServiceDemo/res/values/colors.xml
create mode 100644 tests/MusicServiceDemo/res/values/dimens.xml
create mode 100644 tests/MusicServiceDemo/res/values/strings.xml
create mode 100644 tests/MusicServiceDemo/res/values/styles.xml
create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java
create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java
create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java
create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java
create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java
create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java
create mode 100644 tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java
diff --git a/Android.mk b/Android.mk
index 79d13d90663c5..7dfa6a0c67a86 100644
--- a/Android.mk
+++ b/Android.mk
@@ -322,15 +322,17 @@ LOCAL_SRC_FILES += \
media/java/android/media/IRemoteVolumeObserver.aidl \
media/java/android/media/IRingtonePlayer.aidl \
media/java/android/media/IVolumeController.aidl \
+ media/java/android/media/browse/IMediaBrowserService.aidl \
+ media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl \
+ media/java/android/media/projection/IMediaProjection.aidl \
+ media/java/android/media/projection/IMediaProjectionCallback.aidl \
+ media/java/android/media/projection/IMediaProjectionManager.aidl \
media/java/android/media/routing/IMediaRouteService.aidl \
media/java/android/media/routing/IMediaRouteClientCallback.aidl \
media/java/android/media/routing/IMediaRouter.aidl \
media/java/android/media/routing/IMediaRouterDelegate.aidl \
media/java/android/media/routing/IMediaRouterRoutingCallback.aidl \
media/java/android/media/routing/IMediaRouterStateCallback.aidl \
- media/java/android/media/projection/IMediaProjection.aidl \
- media/java/android/media/projection/IMediaProjectionCallback.aidl \
- media/java/android/media/projection/IMediaProjectionManager.aidl \
media/java/android/media/session/IActiveSessionsListener.aidl \
media/java/android/media/session/ISessionController.aidl \
media/java/android/media/session/ISessionControllerCallback.aidl \
diff --git a/api/current.txt b/api/current.txt
index e736d78aa24ba..5688d1420c6c1 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -16154,6 +16154,77 @@ package android.media.audiofx {
}
+package android.media.browse {
+
+ public final class MediaBrowser {
+ ctor public MediaBrowser(android.content.Context, android.content.ComponentName, android.media.browse.MediaBrowser.ConnectionCallback, android.os.Bundle);
+ method public void connect();
+ method public void disconnect();
+ method public android.net.Uri getRoot();
+ method public android.media.session.MediaSession.Token getSessionToken();
+ method public boolean isConnected();
+ method public void loadThumbnail(android.net.Uri, int, int, int, android.media.browse.MediaBrowser.ThumbnailCallback);
+ method public void subscribe(android.net.Uri, android.media.browse.MediaBrowser.SubscriptionCallback);
+ method public void unsubscribe(android.net.Uri);
+ }
+
+ public static class MediaBrowser.ConnectionCallback {
+ ctor public MediaBrowser.ConnectionCallback();
+ method public void onConnected();
+ method public void onConnectionFailed();
+ method public void onConnectionSuspended();
+ }
+
+ public static abstract class MediaBrowser.SubscriptionCallback {
+ ctor public MediaBrowser.SubscriptionCallback();
+ method public void onChildrenLoaded(android.net.Uri, java.util.List);
+ method public void onError(android.net.Uri);
+ }
+
+ public static abstract class MediaBrowser.ThumbnailCallback {
+ ctor public MediaBrowser.ThumbnailCallback();
+ method public void onError(android.net.Uri);
+ method public void onThumbnailLoaded(android.net.Uri, android.graphics.Bitmap);
+ }
+
+ public final class MediaBrowserItem implements android.os.Parcelable {
+ method public int describeContents();
+ method public android.os.Bundle getExtras();
+ method public int getFlags();
+ method public java.lang.CharSequence getSummary();
+ method public java.lang.CharSequence getTitle();
+ method public android.net.Uri getUri();
+ method public boolean isBrowsable();
+ method public boolean isPlayable();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator CREATOR;
+ field public static final int FLAG_BROWSABLE = 1; // 0x1
+ field public static final int FLAG_PLAYABLE = 2; // 0x2
+ }
+
+ public static final class MediaBrowserItem.Builder {
+ ctor public MediaBrowserItem.Builder(android.net.Uri, int, java.lang.CharSequence);
+ method public android.media.browse.MediaBrowserItem build();
+ method public android.media.browse.MediaBrowserItem.Builder setExtras(android.os.Bundle);
+ method public android.media.browse.MediaBrowserItem.Builder setSummary(java.lang.CharSequence);
+ }
+
+ public abstract class MediaBrowserService extends android.app.Service {
+ ctor public MediaBrowserService();
+ method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
+ method public android.media.session.MediaSession.Token getSessionToken();
+ method public void notifyChange();
+ method public void notifyChildrenChanged(android.net.Uri);
+ method public android.os.IBinder onBind(android.content.Intent);
+ method public abstract android.net.Uri onGetRoot(java.lang.String, int, android.os.Bundle);
+ method public abstract android.graphics.Bitmap onGetThumbnail(android.net.Uri, int, int, int);
+ method public abstract java.util.List onLoadChildren(android.net.Uri);
+ method public void setSessionToken(android.media.session.MediaSession.Token);
+ field public static final java.lang.String SERVICE_ACTION = "android.media.browse.MediaBrowserService";
+ }
+
+}
+
package android.media.effect {
public abstract class Effect {
diff --git a/media/java/android/media/browse/IMediaBrowserService.aidl b/media/java/android/media/browse/IMediaBrowserService.aidl
new file mode 100644
index 0000000000000..4b2cb9d2bc134
--- /dev/null
+++ b/media/java/android/media/browse/IMediaBrowserService.aidl
@@ -0,0 +1,20 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+
+package android.media.browse;
+
+import android.media.browse.IMediaBrowserServiceCallbacks;
+import android.net.Uri;
+import android.os.Bundle;
+
+/**
+ * Media API allows clients to browse through hierarchy of a user’s media collection,
+ * playback a specific media entry and interact with the now playing queue.
+ * @hide
+ */
+oneway interface IMediaBrowserService {
+ void connect(String pkg, in Bundle rootHints, IMediaBrowserServiceCallbacks callbacks);
+ void disconnect(IMediaBrowserServiceCallbacks callbacks);
+
+ void addSubscription(in Uri uri, IMediaBrowserServiceCallbacks callbacks);
+ void removeSubscription(in Uri uri, IMediaBrowserServiceCallbacks callbacks);
+}
\ No newline at end of file
diff --git a/media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl b/media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl
new file mode 100644
index 0000000000000..ead76243e7cd1
--- /dev/null
+++ b/media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl
@@ -0,0 +1,24 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+
+package android.media.browse;
+
+import android.content.pm.ParceledListSlice;
+import android.media.session.MediaSession;
+import android.net.Uri;
+
+/**
+ * Media API allows clients to browse through hierarchy of a user’s media collection,
+ * playback a specific media entry and interact with the now playing queue.
+ * @hide
+ */
+oneway interface IMediaBrowserServiceCallbacks {
+ /**
+ * Invoked when the connected has been established.
+ * @param root The root Uri for browsing.
+ * @param session The {@link MediaSession.Token media session token} that can be used to control
+ * the playback of the media app.
+ */
+ void onConnect(in Uri root, in MediaSession.Token session);
+ void onConnectFailed();
+ void onLoadChildren(in Uri uri, in ParceledListSlice list);
+}
diff --git a/media/java/android/media/browse/MediaBrowser.java b/media/java/android/media/browse/MediaBrowser.java
new file mode 100644
index 0000000000000..beec5f9a80f68
--- /dev/null
+++ b/media/java/android/media/browse/MediaBrowser.java
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2014 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 android.media.browse;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ParceledListSlice;
+import android.graphics.Bitmap;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Browses media content offered by a link MediaBrowserService.
+ *
+ * This object is not thread-safe. All calls should happen on the thread on which the browser
+ * was constructed.
+ *
+ */
+public final class MediaBrowser {
+ private static final String TAG = "MediaBrowser";
+ private static final boolean DBG = false;
+
+ private static final int CONNECT_STATE_DISCONNECTED = 0;
+ private static final int CONNECT_STATE_CONNECTING = 1;
+ private static final int CONNECT_STATE_CONNECTED = 2;
+ private static final int CONNECT_STATE_SUSPENDED = 3;
+
+ private final Context mContext;
+ private final ComponentName mServiceComponent;
+ private final ConnectionCallback mCallback;
+ private final Bundle mRootHints;
+ private final Handler mHandler = new Handler();
+ private final ArrayMap mSubscriptions =
+ new ArrayMap();
+
+ private int mState = CONNECT_STATE_DISCONNECTED;
+ private MediaServiceConnection mServiceConnection;
+ private IMediaBrowserService mServiceBinder;
+ private IMediaBrowserServiceCallbacks mServiceCallbacks;
+ private Uri mRootUri;
+ private MediaSession.Token mMediaSessionToken;
+
+ /**
+ * Creates a media browser for the specified media browse service.
+ *
+ * @param context The context.
+ * @param serviceComponent The component name of the media browse service.
+ * @param callback The connection callback.
+ * @param rootHints An optional bundle of service-specific arguments to send
+ * to the media browse service when connecting and retrieving the root uri
+ * for browsing, or null if none. The contents of this bundle may affect
+ * the information returned when browsing.
+ */
+ public MediaBrowser(Context context, ComponentName serviceComponent,
+ ConnectionCallback callback, Bundle rootHints) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+ if (serviceComponent == null) {
+ throw new IllegalArgumentException("service component must not be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("connection callback must not be null");
+ }
+ mContext = context;
+ mServiceComponent = serviceComponent;
+ mCallback = callback;
+ mRootHints = rootHints;
+ }
+
+ /**
+ * Connects to the media browse service.
+ *
+ * The connection callback specified in the constructor will be invoked
+ * when the connection completes or fails.
+ *
+ */
+ public void connect() {
+ if (mState != CONNECT_STATE_DISCONNECTED) {
+ throw new IllegalStateException("connect() called while not disconnected (state="
+ + getStateLabel(mState) + ")");
+ }
+ // TODO: remove this extra check.
+ if (DBG) {
+ if (mServiceConnection != null) {
+ throw new RuntimeException("mServiceConnection should be null. Instead it is "
+ + mServiceConnection);
+ }
+ }
+ if (mServiceBinder != null) {
+ throw new RuntimeException("mServiceBinder should be null. Instead it is "
+ + mServiceBinder);
+ }
+ if (mServiceCallbacks != null) {
+ throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
+ + mServiceCallbacks);
+ }
+
+ mState = CONNECT_STATE_CONNECTING;
+
+ final Intent intent = new Intent(MediaBrowserService.SERVICE_ACTION);
+ intent.setComponent(mServiceComponent);
+
+ final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection();
+
+ try {
+ mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ } catch (Exception ex) {
+ Log.e(TAG, "Failed binding to service " + mServiceComponent);
+
+ // Tell them that it didn't work. We are already on the main thread,
+ // but we don't want to do callbacks inside of connect(). So post it,
+ // and then check that we are on the same ServiceConnection. We know
+ // we won't also get an onServiceConnected or onServiceDisconnected,
+ // so we won't be doing double callbacks.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Ensure that nobody else came in or tried to connect again.
+ if (thisConnection == mServiceConnection) {
+ forceCloseConnection();
+ mCallback.onConnectionFailed();
+ }
+ }
+ });
+ }
+
+ if (DBG) {
+ Log.d(TAG, "connect...");
+ dump();
+ }
+ }
+
+ /**
+ * Disconnects from the media browse service.
+ * @more
+ * After this, no more callbacks will be received.
+ */
+ public void disconnect() {
+ // It's ok to call this any state, because allowing this lets apps not have
+ // to check isConnected() unnecessarily. They won't appreciate the extra
+ // assertions for this. We do everything we can here to go back to a sane state.
+ if (mServiceCallbacks != null) {
+ try {
+ mServiceBinder.disconnect(mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // We are disconnecting anyway. Log, just for posterity but it's not
+ // a big problem.
+ Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+ }
+ }
+ forceCloseConnection();
+
+ if (DBG) {
+ Log.d(TAG, "disconnect...");
+ dump();
+ }
+ }
+
+ /**
+ * Null out the variables and unbind from the service. This doesn't include
+ * calling disconnect on the service, because we only try to do that in the
+ * clean shutdown cases.
+ *
+ * Everywhere that calls this EXCEPT for disconnect() should follow it with
+ * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
+ * for a clean shutdown, but everywhere else is a dirty shutdown and should
+ * notify the app.
+ */
+ private void forceCloseConnection() {
+ if (mServiceConnection != null) {
+ mContext.unbindService(mServiceConnection);
+ }
+ mState = CONNECT_STATE_DISCONNECTED;
+ mServiceConnection = null;
+ mServiceBinder = null;
+ mServiceCallbacks = null;
+ mRootUri = null;
+ mMediaSessionToken = null;
+ }
+
+ /**
+ * Returns whether the browser is connected to the service.
+ */
+ public boolean isConnected() {
+ return mState == CONNECT_STATE_CONNECTED;
+ }
+
+ /**
+ * Gets the root Uri.
+ *
+ * Note that the root uri may become invalid or change when when the
+ * browser is disconnected.
+ *
+ *
+ * @throws IllegalStateException if not connected.
+ */
+ public @NonNull Uri getRoot() {
+ if (mState != CONNECT_STATE_CONNECTED) {
+ throw new IllegalStateException("getSessionToken() called while not connected (state="
+ + getStateLabel(mState) + ")");
+ }
+ return mRootUri;
+ }
+
+ /**
+ * Gets the media session token associated with the media browser.
+ *
+ * Note that the session token may become invalid or change when when the
+ * browser is disconnected.
+ *
+ *
+ * @return The session token for the browser, never null.
+ *
+ * @throws IllegalStateException if not connected.
+ */
+ public @NonNull MediaSession.Token getSessionToken() {
+ if (mState != CONNECT_STATE_CONNECTED) {
+ throw new IllegalStateException("getSessionToken() called while not connected (state="
+ + mState + ")");
+ }
+ return mMediaSessionToken;
+ }
+
+ /**
+ * Queries for information about the media items that are contained within
+ * the specified Uri and subscribes to receive updates when they change.
+ *
+ * The list of subscriptions is maintained even when not connected and is
+ * restored after reconnection. It is ok to subscribe while not connected
+ * but the results will not be returned until the connection completes.
+ *
+ * If the uri is already subscribed with a different callback then the new
+ * callback will replace the previous one.
+ *
+ *
+ * @param parentUri The uri of the parent media item whose list of children
+ * will be subscribed.
+ * @param callback The callback to receive the list of children.
+ */
+ public void subscribe(@NonNull Uri parentUri, @NonNull SubscriptionCallback callback) {
+ // Check arguments.
+ if (parentUri == null) {
+ throw new IllegalArgumentException("parentUri is null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback is null");
+ }
+
+ // Update or create the subscription.
+ Subscription sub = mSubscriptions.get(parentUri);
+ boolean newSubscription = sub == null;
+ if (newSubscription) {
+ sub = new Subscription(parentUri);
+ mSubscriptions.put(parentUri, sub);
+ }
+ sub.callback = callback;
+
+ // If we are connected, tell the service that we are watching. If we aren't
+ // connected, the service will be told when we connect.
+ if (mState == CONNECT_STATE_CONNECTED && newSubscription) {
+ try {
+ mServiceBinder.addSubscription(parentUri, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "addSubscription failed with RemoteException parentUri=" + parentUri);
+ }
+ }
+ }
+
+ /**
+ * Unsubscribes for changes to the children of the specified Uri.
+ *
+ * The query callback will no longer be invoked for results associated with
+ * this Uri once this method returns.
+ *
+ *
+ * @param parentUri The uri of the parent media item whose list of children
+ * will be unsubscribed.
+ */
+ public void unsubscribe(@NonNull Uri parentUri) {
+ // Check arguments.
+ if (parentUri == null) {
+ throw new IllegalArgumentException("parentUri is null");
+ }
+
+ // Remove from our list.
+ final Subscription sub = mSubscriptions.remove(parentUri);
+
+ // Tell the service if necessary.
+ if (mState == CONNECT_STATE_CONNECTED && sub != null) {
+ try {
+ mServiceBinder.removeSubscription(parentUri, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "removeSubscription failed with RemoteException parentUri=" + parentUri);
+ }
+ }
+ }
+
+ /**
+ * Loads the thumbnail of a media item.
+ *
+ * @param uri The uri of the media item.
+ * @param width The preferred width of the icon in dp.
+ * @param height The preferred width of the icon in dp.
+ * @param density The preferred density of the icon. Must be one of the android
+ * density buckets.
+ * @param callback The callback to receive the thumbnail.
+ *
+ * @throws IllegalStateException if not connected. TODO: Is this restriction necessary?
+ */
+ public void loadThumbnail(@NonNull Uri uri, int width, int height, int density,
+ @NonNull ThumbnailCallback callback) {
+ throw new RuntimeException("implement me");
+ }
+
+ /**
+ * For debugging.
+ */
+ private static String getStateLabel(int state) {
+ switch (state) {
+ case CONNECT_STATE_DISCONNECTED:
+ return "CONNECT_STATE_DISCONNECTED";
+ case CONNECT_STATE_CONNECTING:
+ return "CONNECT_STATE_CONNECTING";
+ case CONNECT_STATE_CONNECTED:
+ return "CONNECT_STATE_CONNECTED";
+ case CONNECT_STATE_SUSPENDED:
+ return "CONNECT_STATE_SUSPENDED";
+ default:
+ return "UNKNOWN/" + state;
+ }
+ }
+
+ private final void onServiceConnected(final IMediaBrowserServiceCallbacks callback,
+ final Uri root, final MediaSession.Token session) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Check to make sure there hasn't been a disconnect or a different
+ // ServiceConnection.
+ if (!isCurrent(callback, "onConnect")) {
+ return;
+ }
+ // Don't allow them to call us twice.
+ if (mState != CONNECT_STATE_CONNECTING) {
+ Log.w(TAG, "onConnect from service while mState="
+ + getStateLabel(mState) + "... ignoring");
+ return;
+ }
+ mRootUri = root;
+ mMediaSessionToken = session;
+ mState = CONNECT_STATE_CONNECTED;
+
+ if (DBG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ mCallback.onConnected();
+
+ // we may receive some subscriptions before we are connected, so re-subscribe
+ // everything now
+ for (Uri uri : mSubscriptions.keySet()) {
+ try {
+ mServiceBinder.addSubscription(uri, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "addSubscription failed with RemoteException parentUri=" + uri);
+ }
+ }
+
+ }
+ });
+ }
+
+ private final void onConnectionFailed(final IMediaBrowserServiceCallbacks callback) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Log.e(TAG, "onConnectFailed for " + mServiceComponent);
+
+ // Check to make sure there hasn't been a disconnect or a different
+ // ServiceConnection.
+ if (!isCurrent(callback, "onConnectFailed")) {
+ return;
+ }
+ // Don't allow them to call us twice.
+ if (mState != CONNECT_STATE_CONNECTING) {
+ Log.w(TAG, "onConnect from service while mState="
+ + getStateLabel(mState) + "... ignoring");
+ return;
+ }
+
+ // Clean up
+ forceCloseConnection();
+
+ // Tell the app.
+ mCallback.onConnectionFailed();
+ }
+ });
+ }
+
+ private final void onLoadChildren(final IMediaBrowserServiceCallbacks callback, final Uri uri,
+ final ParceledListSlice list) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Check that there hasn't been a disconnect or a different
+ // ServiceConnection.
+ if (!isCurrent(callback, "onLoadChildren")) {
+ return;
+ }
+
+ List data = list.getList();
+ if (DBG) {
+ Log.d(TAG, "onLoadChildren for " + mServiceComponent + " uri=" + uri);
+ }
+ if (data == null) {
+ data = Collections.emptyList();
+ }
+
+ // Check that the subscription is still subscribed.
+ final Subscription subscription = mSubscriptions.get(uri);
+ if (subscription == null) {
+ if (DBG) {
+ Log.d(TAG, "onLoadChildren for uri that isn't subscribed uri="
+ + uri);
+ }
+ return;
+ }
+
+ // Tell the app.
+ subscription.callback.onChildrenLoaded(uri, data);
+ }
+ });
+ }
+
+
+ /**
+ * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
+ */
+ private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) {
+ if (mServiceCallbacks != callback) {
+ if (mState != CONNECT_STATE_DISCONNECTED) {
+ Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
+ + mServiceCallbacks + " this=" + this);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private ServiceCallbacks getNewServiceCallbacks() {
+ return new ServiceCallbacks(this);
+ }
+
+ /**
+ * Log internal state.
+ * @hide
+ */
+ void dump() {
+ Log.d(TAG, "MediaBrowser...");
+ Log.d(TAG, " mServiceComponent=" + mServiceComponent);
+ Log.d(TAG, " mCallback=" + mCallback);
+ Log.d(TAG, " mRootHints=" + mRootHints);
+ Log.d(TAG, " mState=" + getStateLabel(mState));
+ Log.d(TAG, " mServiceConnection=" + mServiceConnection);
+ Log.d(TAG, " mServiceBinder=" + mServiceBinder);
+ Log.d(TAG, " mServiceCallbacks=" + mServiceCallbacks);
+ Log.d(TAG, " mRootUri=" + mRootUri);
+ Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken);
+ }
+
+
+ /**
+ * Callbacks for connection related events.
+ */
+ public static class ConnectionCallback {
+ /**
+ * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
+ */
+ public void onConnected() {
+ }
+
+ /**
+ * Invoked when the client is disconnected from the media browser.
+ */
+ public void onConnectionSuspended() {
+ }
+
+ /**
+ * Invoked when the connection to the media browser failed.
+ */
+ public void onConnectionFailed() {
+ }
+ }
+
+ /**
+ * Callbacks for subscription related events.
+ */
+ public static abstract class SubscriptionCallback {
+ /**
+ * Called when the list of children is loaded or updated.
+ */
+ public void onChildrenLoaded(@NonNull Uri parentUri,
+ @NonNull List children) {
+ }
+
+ /**
+ * Called when the Uri doesn't exist or other errors in subscribing.
+ *
+ * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
+ * called, because some errors may heal themselves.
+ *
+ */
+ public void onError(@NonNull Uri uri) {
+ }
+ }
+
+ /**
+ * Callbacks for thumbnail loading.
+ */
+ public static abstract class ThumbnailCallback {
+ /**
+ * Called when the thumbnail is loaded.
+ */
+ public void onThumbnailLoaded(@NonNull Uri uri, @NonNull Bitmap bitmap) {
+ }
+
+ /**
+ * Called when the Uri doesn’t exist or the bitmap cannot be loaded.
+ */
+ public void onError(@NonNull Uri uri) {
+ }
+ }
+
+ /**
+ * ServiceConnection to the other app.
+ */
+ private class MediaServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder binder) {
+ if (DBG) {
+ Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
+ + " binder=" + binder);
+ dump();
+ }
+
+ // Make sure we are still the current connection, and that they haven't called
+ // disconnect().
+ if (!isCurrent("onServiceConnected")) {
+ return;
+ }
+
+ // Save their binder
+ mServiceBinder = IMediaBrowserService.Stub.asInterface(binder);
+
+ // We make a new mServiceCallbacks each time we connect so that we can drop
+ // responses from previous connections.
+ mServiceCallbacks = getNewServiceCallbacks();
+
+ // Call connect, which is async. When we get a response from that we will
+ // say that we're connected.
+ try {
+ if (DBG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ mServiceBinder.connect(mContext.getPackageName(), mRootHints, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Connect failed, which isn't good. But the auto-reconnect on the service
+ // will take over and we will come back. We will also get the
+ // onServiceDisconnected, which has all the cleanup code. So let that do it.
+ Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+ if (DBG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DBG) {
+ Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
+ + " this=" + this + " mServiceConnection=" + mServiceConnection);
+ dump();
+ }
+
+ // Make sure we are still the current connection, and that they haven't called
+ // disconnect().
+ if (!isCurrent("onServiceDisconnected")) {
+ return;
+ }
+
+ // Clear out what we set in onServiceConnected
+ mServiceBinder = null;
+ mServiceCallbacks = null;
+
+ // And tell the app that it's suspended.
+ mState = CONNECT_STATE_SUSPENDED;
+ mCallback.onConnectionSuspended();
+ }
+
+ /**
+ * Return true if this is the current ServiceConnection. Also logs if it's not.
+ */
+ private boolean isCurrent(String funcName) {
+ if (mServiceConnection != this) {
+ if (mState != CONNECT_STATE_DISCONNECTED) {
+ // Check mState, because otherwise this log is noisy.
+ Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
+ + mServiceConnection + " this=" + this);
+ }
+ return false;
+ }
+ return true;
+ }
+ };
+
+ /**
+ * Callbacks from the service.
+ */
+ private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub {
+ private WeakReference mMediaBrowser;
+
+ public ServiceCallbacks(MediaBrowser mediaBrowser) {
+ mMediaBrowser = new WeakReference(mediaBrowser);
+ }
+
+ /**
+ * The other side has acknowledged our connection. The parameters to this function
+ * are the initial data as requested.
+ */
+ @Override
+ public void onConnect(final Uri root, final MediaSession.Token session) {
+ MediaBrowser mediaBrowser = mMediaBrowser.get();
+ if (mediaBrowser != null) {
+ mediaBrowser.onServiceConnected(this, root, session);
+ }
+ }
+
+ /**
+ * The other side does not like us. Tell the app via onConnectionFailed.
+ */
+ @Override
+ public void onConnectFailed() {
+ MediaBrowser mediaBrowser = mMediaBrowser.get();
+ if (mediaBrowser != null) {
+ mediaBrowser.onConnectionFailed(this);
+ }
+ }
+
+ @Override
+ public void onLoadChildren(final Uri uri, final ParceledListSlice list) {
+ MediaBrowser mediaBrowser = mMediaBrowser.get();
+ if (mediaBrowser != null) {
+ mediaBrowser.onLoadChildren(this, uri, list);
+ }
+ }
+ }
+
+ private static class Subscription {
+ final Uri uri;
+ SubscriptionCallback callback;
+
+ Subscription(Uri u) {
+ this.uri = u;
+ }
+ }
+}
diff --git a/media/java/android/media/browse/MediaBrowserItem.java b/media/java/android/media/browse/MediaBrowserItem.java
new file mode 100644
index 0000000000000..119f687308a3a
--- /dev/null
+++ b/media/java/android/media/browse/MediaBrowserItem.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2014 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 android.media.browse;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.net.Uri;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Describes a media item in the list of items offered by a {@link MediaBrowserService}.
+ */
+public final class MediaBrowserItem implements Parcelable {
+ private final Uri mUri;
+ private final int mFlags;
+ private final CharSequence mTitle;
+ private final CharSequence mSummary;
+ private final Bundle mExtras;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
+ public @interface Flags { }
+
+ /**
+ * Flag: Indicates that the item has children of its own.
+ */
+ public static final int FLAG_BROWSABLE = 1 << 0;
+
+ /**
+ * Flag: Indicates that the item is playable.
+ *
+ * The Uri of this item may be passed to link android.media.session.MediaController#play(Uri)
+ * to start playing it.
+ *
+ */
+ public static final int FLAG_PLAYABLE = 1 << 1;
+
+ /**
+ * Initialize a MediaBrowserItem object.
+ */
+ private MediaBrowserItem(@NonNull Uri uri, int flags, @NonNull CharSequence title,
+ CharSequence summary, Bundle extras) {
+ if (uri == null) {
+ throw new IllegalArgumentException("uri can not be null");
+ }
+ if (title == null) {
+ throw new IllegalArgumentException("title can not be null");
+ }
+ mUri = uri;
+ mFlags = flags;
+ mTitle = title;
+ mSummary = summary;
+ mExtras = extras;
+ }
+
+ /**
+ * Private constructor.
+ */
+ private MediaBrowserItem(Parcel in) {
+ mUri = Uri.CREATOR.createFromParcel(in);
+ mFlags = in.readInt();
+ mTitle = in.readCharSequence();
+ if (in.readInt() != 0) {
+ mSummary = in.readCharSequence();
+ } else {
+ mSummary = null;
+ }
+ if (in.readInt() != 0) {
+ mExtras = Bundle.CREATOR.createFromParcel(in);
+ } else {
+ mExtras = null;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ mUri.writeToParcel(out, flags);
+ out.writeInt(mFlags);
+ out.writeCharSequence(mTitle);
+ if (mSummary != null) {
+ out.writeInt(1);
+ out.writeCharSequence(mSummary);
+ } else {
+ out.writeInt(0);
+ }
+ if (mExtras != null) {
+ out.writeInt(1);
+ mExtras.writeToParcel(out, flags);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ @Override
+ public MediaBrowserItem createFromParcel(Parcel in) {
+ return new MediaBrowserItem(in);
+ }
+
+ @Override
+ public MediaBrowserItem[] newArray(int size) {
+ return new MediaBrowserItem[size];
+ }
+ };
+
+ /**
+ * Gets the Uri of the item.
+ */
+ public @NonNull Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Gets the flags of the item.
+ */
+ public @Flags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns whether this item is browsable.
+ * @see #FLAG_BROWSABLE
+ */
+ public boolean isBrowsable() {
+ return (mFlags & FLAG_BROWSABLE) != 0;
+ }
+
+ /**
+ * Returns whether this item is playable.
+ * @see #FLAG_PLAYABLE
+ */
+ public boolean isPlayable() {
+ return (mFlags & FLAG_PLAYABLE) != 0;
+ }
+
+ /**
+ * Gets the title of the item.
+ * @more
+ * The title will be shown as the first line of text when
+ * describing each item to the user.
+ */
+ public @NonNull CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Gets summary of the item, or null if none.
+ * @more
+ * The summary will be shown as the second line of text when
+ * describing each item to the user.
+ */
+ public @Nullable CharSequence getSummary() {
+ return mSummary;
+ }
+
+ /**
+ * Gets additional service-specified extras about the
+ * item or its content, or null if none.
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Builder for {@link MediaBrowserItem} objects.
+ */
+ public static final class Builder {
+ private final Uri mUri;
+ private final int mFlags;
+ private final CharSequence mTitle;
+ private CharSequence mSummary;
+ private Bundle mExtras;
+
+ /**
+ * Creates an item builder.
+ */
+ public Builder(@NonNull Uri uri, @Flags int flags, @NonNull CharSequence title) {
+ if (uri == null) {
+ throw new IllegalArgumentException("uri can not be null");
+ }
+ if (title == null) {
+ throw new IllegalArgumentException("title can not be null");
+ }
+ mUri = uri;
+ mFlags = flags;
+ mTitle = title;
+ }
+
+ /**
+ * Sets summary of the item, or null if none.
+ */
+ public @NonNull Builder setSummary(@Nullable CharSequence summary) {
+ mSummary = summary;
+ return this;
+ }
+
+ /**
+ * Sets additional service-specified extras about the
+ * item or its content, or null if none.
+ */
+ public @NonNull Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds the item.
+ */
+ public @NonNull MediaBrowserItem build() {
+ return new MediaBrowserItem(mUri, mFlags, mTitle, mSummary, mExtras);
+ }
+ }
+}
+
diff --git a/media/java/android/media/browse/MediaBrowserService.java b/media/java/android/media/browse/MediaBrowserService.java
new file mode 100644
index 0000000000000..ceb4b03b88490
--- /dev/null
+++ b/media/java/android/media/browse/MediaBrowserService.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2014 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 android.media.browse;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.graphics.Bitmap;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Base class for media browse services.
+ *
+ * Media browse services enable applications to browse media content provided by an application
+ * and ask the application to start playing it. They may also be used to control content that
+ * is already playing by way of a {@link MediaSession}.
+ *
+ *
+ * To extend this class, you must declare the service in your manifest file with
+ * an intent filter with the {@link #SERVICE_ACTION} action.
+ *
+ * For example:
+ *
+ * <service android:name=".MyMediaBrowserService"
+ * android:label="@string/service_name" >
+ * <intent-filter>
+ * <action android:name="android.media.browse.MediaBrowserService" />
+ * </intent-filter>
+ * </service>
+ *
+ *
+ */
+public abstract class MediaBrowserService extends Service {
+ private static final String TAG = "MediaBrowserService";
+
+ private final ArrayMap mConnections = new ArrayMap();
+ private final Handler mHandler = new Handler();
+ private ServiceBinder mBinder;
+ MediaSession.Token mSession;
+
+ /**
+ * All the info about a connection.
+ */
+ private class ConnectionRecord {
+ String pkg;
+ Bundle rootHints;
+ IMediaBrowserServiceCallbacks callbacks;
+ Uri root;
+ HashSet subscriptions = new HashSet();
+ }
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_ACTION = "android.media.browse.MediaBrowserService";
+
+ private class ServiceBinder extends IMediaBrowserService.Stub {
+ @Override
+ public void connect(final String pkg, final Bundle rootHints,
+ final IMediaBrowserServiceCallbacks callbacks) {
+
+ final int uid = Binder.getCallingUid();
+ if (!isValidPackage(pkg, uid)) {
+ throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
+ + " package=" + pkg);
+ }
+
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ // Clear out the old subscriptions. We are getting new ones.
+ mConnections.remove(b);
+
+ final ConnectionRecord connection = new ConnectionRecord();
+ connection.pkg = pkg;
+ connection.rootHints = rootHints;
+ connection.callbacks = callbacks;
+
+ connection.root = MediaBrowserService.this.onGetRoot(pkg, uid, rootHints);
+
+ // If they didn't return something, don't allow this client.
+ if (connection.root == null) {
+ Log.i(TAG, "No root for client " + pkg + " from service "
+ + getClass().getName());
+ try {
+ callbacks.onConnectFailed();
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
+ + "pkg=" + pkg);
+ }
+ } else {
+ try {
+ mConnections.put(b, connection);
+ callbacks.onConnect(connection.root, mSession);
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Calling onConnect() failed. Dropping client. "
+ + "pkg=" + pkg);
+ mConnections.remove(b);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void disconnect(final IMediaBrowserServiceCallbacks callbacks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ // Clear out the old subscriptions. We are getting new ones.
+ final ConnectionRecord old = mConnections.remove(b);
+ if (old != null) {
+ // TODO
+ }
+ }
+ });
+ }
+
+
+ @Override
+ public void addSubscription(final Uri uri, final IMediaBrowserServiceCallbacks callbacks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ // Get the record for the connection
+ final ConnectionRecord connection = mConnections.get(b);
+ if (connection == null) {
+ Log.w(TAG, "addSubscription for callback that isn't registered uri="
+ + uri);
+ return;
+ }
+
+ MediaBrowserService.this.addSubscription(uri, connection);
+ }
+ });
+ }
+
+ @Override
+ public void removeSubscription(final Uri uri,
+ final IMediaBrowserServiceCallbacks callbacks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ ConnectionRecord connection = mConnections.get(b);
+ if (connection == null) {
+ Log.w(TAG, "removeSubscription for callback that isn't registered uri="
+ + uri);
+ return;
+ }
+ if (!connection.subscriptions.remove(uri)) {
+ Log.w(TAG, "removeSubscription called for " + uri
+ + " which is not subscribed");
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mBinder = new ServiceBinder();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (SERVICE_ACTION.equals(intent.getAction())) {
+ return mBinder;
+ }
+ return null;
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ }
+
+ /**
+ * Called to get the root uri for browsing by a particular client.
+ *
+ * The implementation should verify that the client package has
+ * permission to access browse media information before returning
+ * the root uri; it should return null if the client is not
+ * allowed to access this information.
+ *
+ *
+ * @param clientPackageName The package name of the application
+ * which is requesting access to browse media.
+ * @param clientUid The uid of the application which is requesting
+ * access to browse media.
+ * @param rootHints An optional bundle of service-specific arguments to send
+ * to the media browse service when connecting and retrieving the root uri
+ * for browsing, or null if none. The contents of this bundle may affect
+ * the information returned when browsing.
+ */
+ public abstract @Nullable Uri onGetRoot(@NonNull String clientPackageName, int clientUid,
+ @Nullable Bundle rootHints);
+
+ /**
+ * Called to get information about the children of a media item.
+ *
+ * @param parentUri The uri of the parent media item whose
+ * children are to be queried.
+ * @return The list of children, or null if the uri is invalid.
+ */
+ public abstract @Nullable List onLoadChildren(@NonNull Uri parentUri);
+
+ /**
+ * Called to get the thumbnail of a particular media item.
+ *
+ * @param uri The uri of the media item.
+ * @param width The requested width of the icon in dp.
+ * @param height The requested height of the icon in dp.
+ * @param density The requested density of the icon. This is the approximate density of the
+ * screen on which the icon will be displayed. This density will be one of
+ * the android density buckets.
+ * @return The file descriptor of the thumbnail, which may then be loaded
+ * using a bitmap factory, or null if the item does not have a thumbnail.
+ */
+ public abstract @Nullable Bitmap onGetThumbnail(@NonNull Uri uri,
+ int width, int height, int density);
+
+ /**
+ * Call to set the media session.
+ *
+ * This must be called before onCreate returns.
+ *
+ * @return The media session token, must not be null.
+ */
+ public void setSessionToken(MediaSession.Token token) {
+ if (token == null) {
+ throw new IllegalStateException(this.getClass().getName()
+ + ".onCreateSession() set invalid MediaSession.Token");
+ }
+ mSession = token;
+ }
+
+ /**
+ * Gets the session token, or null if it has not yet been created
+ * or if it has been destroyed.
+ */
+ public @Nullable MediaSession.Token getSessionToken() {
+ return mSession;
+ }
+
+ /**
+ * Notifies all connected media browsers that the content of
+ * the browse service has changed in some way.
+ * This will cause browsers to fetch subscribed content again.
+ */
+ public void notifyChange() {
+ throw new RuntimeException("implement me");
+ }
+
+ /**
+ * Notifies all connected media browsers that the children of
+ * the specified Uri have changed in some way.
+ * This will cause browsers to fetch subscribed content again.
+ *
+ * @param parentUri The uri of the parent media item whose
+ * children changed.
+ */
+ public void notifyChildrenChanged(@NonNull Uri parentUri) {
+ throw new RuntimeException("implement me");
+ }
+
+ /**
+ * Return whether the given package is one of the ones that is owned by the uid.
+ */
+ private boolean isValidPackage(String pkg, int uid) {
+ if (pkg == null) {
+ return false;
+ }
+ final PackageManager pm = getPackageManager();
+ final String[] packages = pm.getPackagesForUid(uid);
+ final int N = packages.length;
+ for (int i=0; i
+ * Callers must make sure that this connection is still connected.
+ *
+ * TODO: Think about caching and combining these calls.
+ */
+ private void performLoadChildren(Uri uri, ConnectionRecord connection) {
+ final List list = onLoadChildren(uri);
+ if (list == null) {
+ throw new IllegalStateException("onLoadChildren returned null for uri " + uri);
+ }
+ final ParceledListSlice pls = new ParceledListSlice(list);
+ try {
+ connection.callbacks.onLoadChildren(uri, pls);
+ } catch (RemoteException ex) {
+ // The other side is in the process of crashing.
+ Log.w(TAG, "Calling onLoadChildren() failed for uri=" + uri
+ + " package=" + connection.pkg);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 9945909a00711..ca9f6eb82781c 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -185,8 +185,8 @@ import javax.net.ssl.SSLSession;
public class ConnectivityService extends IConnectivityManager.Stub {
private static final String TAG = "ConnectivityService";
- private static final boolean DBG = true;
- private static final boolean VDBG = true; // STOPSHIP
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false; // STOPSHIP
// network sampling debugging
private static final boolean SAMPLE_DBG = false;
diff --git a/tests/MusicBrowserDemo/Android.mk b/tests/MusicBrowserDemo/Android.mk
new file mode 100644
index 0000000000000..207774b07a050
--- /dev/null
+++ b/tests/MusicBrowserDemo/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2014 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := MusicBrowserDemo
+#LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-v4 \
+ android-support-v7-appcompat
+
+LOCAL_RESOURCE_DIR := \
+ $(LOCAL_PATH)/res \
+ frameworks/support/v7/appcompat/res
+LOCAL_PROGUARD_ENABLED := disabled
+#LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+LOCAL_AAPT_FLAGS := \
+ --auto-add-overlay \
+ --extra-packages android.support.v7.appcompat
+include $(BUILD_PACKAGE)
diff --git a/tests/MusicBrowserDemo/AndroidManifest.xml b/tests/MusicBrowserDemo/AndroidManifest.xml
new file mode 100644
index 0000000000000..d2acfe2ad871e
--- /dev/null
+++ b/tests/MusicBrowserDemo/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..47d6854e26d5da581349cb94029645a9b827ce8c
GIT binary patch
literal 4805
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
+
+
+
+ Music Browser
+
+
diff --git a/tests/MusicBrowserDemo/res/values/styles.xml b/tests/MusicBrowserDemo/res/values/styles.xml
new file mode 100644
index 0000000000000..b83662d6fdec0
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/values/styles.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java
new file mode 100644
index 0000000000000..c0f3a7f75825d
--- /dev/null
+++ b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 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.example.android.musicbrowserdemo;
+
+import android.content.Context;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.browse.MediaBrowserService;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.ListFragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+// TODO: Include an icon.
+
+public class AppListFragment extends ListFragment {
+
+ private Adapter mAdapter;
+ private List- mItems;
+
+ public AppListFragment() {
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mAdapter = new Adapter();
+ setListAdapter(mAdapter);
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ final Item item = mItems.get(position);
+
+ Log.i("AppListFragment", "Item clicked: " + position + " -- " + item.component);
+
+ final BrowserListFragment fragment = new BrowserListFragment();
+
+ final Bundle args = new Bundle();
+ args.putParcelable(BrowserListFragment.ARG_COMPONENT, item.component);
+ fragment.setArguments(args);
+
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+ .addToBackStack(null)
+ .commit();
+ }
+
+ private static class Item {
+ final String label;
+ final ComponentName component;
+
+ Item(String l, ComponentName c) {
+ this.label = l;
+ this.component = c;
+ }
+ }
+
+ private class Adapter extends BaseAdapter {
+ private final LayoutInflater mInflater;
+
+ Adapter() {
+ super();
+
+ final Context context = getActivity();
+ mInflater = LayoutInflater.from(context);
+
+ // Load the data
+ final PackageManager pm = context.getPackageManager();
+ final Intent intent = new Intent(MediaBrowserService.SERVICE_ACTION);
+ final List list = pm.queryIntentServices(intent, 0);
+ final int N = list.size();
+ mItems = new ArrayList(N);
+ for (int i=0; i mItems = new ArrayList();
+ private ComponentName mComponent;
+ private Uri mUri;
+ private MediaBrowser mBrowser;
+
+ private static class Item {
+ final MediaBrowserItem media;
+
+ Item(MediaBrowserItem m) {
+ this.media = m;
+ }
+ }
+
+ public BrowserListFragment() {
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ Log.d(TAG, "onActivityCreated -- " + hashCode());
+ mAdapter = new Adapter();
+ setListAdapter(mAdapter);
+
+ // Get our arguments
+ final Bundle args = getArguments();
+ mComponent = args.getParcelable(ARG_COMPONENT);
+ mUri = args.getParcelable(ARG_URI);
+
+ // A hint about who we are, so the service can customize the results if it wants to.
+ final Bundle rootHints = new Bundle();
+ rootHints.putBoolean(HINT_DISPLAY, true);
+
+ mBrowser = new MediaBrowser(getActivity(), mComponent, mConnectionCallbacks, rootHints);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mBrowser.connect();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mBrowser.disconnect();
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ final Item item = mItems.get(position);
+
+ Log.i("BrowserListFragment", "Item clicked: " + position + " -- "
+ + mAdapter.getItem(position).media.getUri());
+
+ final BrowserListFragment fragment = new BrowserListFragment();
+
+ final Bundle args = new Bundle();
+ args.putParcelable(BrowserListFragment.ARG_COMPONENT, mComponent);
+ args.putParcelable(BrowserListFragment.ARG_URI, item.media.getUri());
+ fragment.setArguments(args);
+
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+ .addToBackStack(null)
+ .commit();
+
+ }
+
+ final MediaBrowser.ConnectionCallback mConnectionCallbacks
+ = new MediaBrowser.ConnectionCallback() {
+ @Override
+ public void onConnected() {
+ Log.d(TAG, "mConnectionCallbacks.onConnected");
+ if (mUri == null) {
+ mUri = mBrowser.getRoot();
+ }
+ mBrowser.subscribe(mUri, new MediaBrowser.SubscriptionCallback() {
+ @Override
+ public void onChildrenLoaded(Uri parentUri, List children) {
+ Log.d(TAG, "onChildrenLoaded parentUri=" + parentUri
+ + " children= " + children);
+ mItems.clear();
+ final int N = children.size();
+ for (int i=0; i
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/MusicServiceDemo/proguard-project.txt b/tests/MusicServiceDemo/proguard-project.txt
new file mode 100644
index 0000000000000..f2fe1559a2178
--- /dev/null
+++ b/tests/MusicServiceDemo/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..47d6854e26d5da581349cb94029645a9b827ce8c
GIT binary patch
literal 4805
zcmV;$5<2aPP)iP-
zK~#9!?VNddRpp(>Kkr#?Zf%RWv%K@ixtClZ#G4QT
zQ|9+PCwDvdyzlwG-{1QFZs7l=OA&oJn5>$F$SLONsV6#6Ez{O
zb|om+3yH32B281A`It7+G-Z<%7~zn;!<2FtioV_L&e(FY{#0nhoDyI3n#QI=aiGd4
z$NuAi(_>$j0DZJHml?yd5Hq$@zotOze|xD3@Qp&;FD#z=6}PTVla~Kxo%T_h2!RZN
z9Re!=(vVn&jl_z~N=4Nbgf37MK}i6SfGHC&@Ej0o(nmqV+Vsd1Ti$%NgdlKwOu2p{
z>nrQHdEU{>k3l6b<%qE(vvkkQo#En_2Il0C{M>&gxtK1@Q(1~p4|Wi21EiFIh-~{hW}QFGhVnzl9-Z`1ZB_fa>s)&e)ft5kRmsbOK~g$%KK}M-2Fe7OFMtyTSb`7&A#_4@QuXgP(zNQLbj?@Q{9eI}Z$2|HBj@ge_&zAmg>dods-b<$
zAi3o##MJ#58wyl?dZ~4E<(O{|&eVRUnkUnRSr5{>L_v!RT7L{Rv#F}V`r(M8{lB(0
zOivG)+KsBydRhv3rAr4F<_%7G&m$VsFq>sxN93J546tkv0#r4|^J)D=yG(C7
zSQEOfU|vm$6VYGI-0$yV<;q^PK~*cVm7KXJ?XsnVCXF2A-s%yjz-krFB`zUrl*nO1
zYXJ6s3bl=J*JKciKp;W&%66H_q2s|R6XqQ+j^0bJFv$2IgUXYLQQv5gn_oyREEt)(
zElFzt(=VL$>N{|33AXJpM7W*VpEwSicEg9Iuz4@+I|et6gSCr6Q9yCjUZN#QznMYl
zA>ZU1AMuqOcr`-|%a&5VrLoH~wXQx4eAo|tRs(=s_gwPW2_sV8O;Vc#hCdSK^$j?-
z?SP|cpd>=mDR}c;SiS+ujzD9Zqh3)-^_&xB3JgCGY!qdjEIX*0IHpg%k$Ghv=E>ntZMkiBkG6ai`iU_r?~
zcy%4Tw-c%xz#lyKdsdG_El1si*<|HpX5F{#1$}$=+_qoU|H;G42ike!iEbL{#rHB~
z{@nh1l3jiec0j)vwx!wc7}k}WVC6=rZHla%+QBT@%{BSrM0jga)JUJt`z0D};iUFo
zt*jsSgIB1IJCgo*F)aI$DgdQuR??G+E+bW10lhav9Vy{t6MT0G6wHTdkHPbQcEHh8
z=cY`{VMQyiJE|4EN_`9br5LJCo%wk@HYEy7G&lqpoB?&sU|4a^LVzOMNKa0@|4)l}a?WH~
zE$$G8Q=vEygM@&?zm=M}!#0Kw)PB(~nTwWqDgs)uM%8HHVFOfXcU?ggJ}O!=Ssr?L5w%%Hot1513(%
zJ`@pg;hNgv!c!9A-kGrJ8TjOX;FZVV{ySj!5FiAy-9cMZdpM{W)>2YD%$>^OSPaf#
z(7+53+2GYl&B;_|VzwEGi^HC-Sf!~jpdZYb0AHH{v#tTv4OY-$k_3ctrdhZk{~-{V
zZAMO}GSjQusUjQSzznEuI18ZY8!HxZm)Dz;A?;>FALZz79j?oRTdsi_SHq-yaHj!b
zutE+viU6Tpq=m5!&Fk%#v0_p4UAO-muSBspL&bjcI0Dibl))^Q;=j;!iEr^DBTO0%
zuP%h2%!BFIf;$2514hJ+D_rcw5-!Cr24ye{(jr*I0Hg?*JPKvOoTdy$Wi?M;}*DgaB2TcoOx(1#_A*CXYgi2AE1TrNjJ#JW)ib7mptk$mjW_14M=eX0wi3TjYp+W2;{hdF#uhg=e$Tw787($xzu&{ON0;TxGi7qk_mvUq4QX?eoL>K9?m^c6pa9-#tE(BLF
zK-ex-x?(ZWV#{ViNRU@BKvLR@a%K|dyi^^tY*~PmSFRC4V%uF*6oW?RRmLj_A0=p<
zS&?zTh%~f?!UV7pm$w!~iv98;gtImx7{+wUM6nEtWUu}ptx{g53{oAtAQU=Ugp`P_
zPh6>rb1yJ+&Rf9
z!73M47s4>X*G^?DCK^DX8VM-PE$j*zOm^v)2WbQl48zZs!7o=qOW@pdehp-%z~{0Y
z;4VZ$1~j)&3MlPWF*fLL)yVIE5vAVGj+_1<0bAh?0X8pYj3n(i&#z7J&JBqzw7pEFp9W}e)npxEA;H&uYpd-{7}T&`1?AC%G2rExYuK0h3)iJv`ZOS(7y0)M3EL!R;>
z_E)hHXuthO9iN{DNR4dLE&Y(y)5r6Z+yQts6|I|kj6VpEz2Nlmw1>_;W%(vZ&|%bI
zr&X>qvFr;oxI?oXmmd$d5r$hejkGTPkkzqRoRL;UcQB9Ea;Tz;9ZhYhPJ>_kKDpip
zvwsS|UK3F%ao!2B_Zaxv&!Zwbq$UxYN5`+(3XC7-j4k4uw`^)dt*Bzh(OM40?)RUQ
zcL@R0$DsUWJ5L>`K}Z`bUL)5YfFJ%2KHB9>(lF2Cb)t)5pEukm-3#M$JLc3Kxpo8;
z3@2{;AZ@GzHHg1#=c(yqA}x*QOI87%pO5%!F~$3jvAN#&*(w&D$hLBLV?zfU5!jGaD6G|WtFV1ZTxIcV%bn$-Lq5zAU)XuDM~9v
zICGi*C3tB8j2zgx!Hw?a7u^UOo`DDN?C5ojhxxUQP*%z64P{h}&Lub`gLCRZ=cv%}
zCV2RHB+&TwR(?J-i<{DtNlZ@cLfxd3GxZ1|>8OM^@vN1Lp!fC9!)@2Y>S9>G%jsKG
zg_Cfb}j?+{)&O`MliU
z%lEF&!`8IU3kyPlG;jCukrEPJaP{y=6Q|kvIcdpo->q=poiUuZobydHqE6jNH%tqA
z&r!7E3SNHy6E@5Dk?I%DWzTu5hqevkR#@^K^rv6r$6LyI_h2oGl%2bMRlqePdO#lO
zbahO<%JE+3J5N=dIaxt4{mx;9ocE$Vjonxjd8iiU|
zTZdKKu!3CO@%uO
z;rEZi?Ngk9@nVA`CDi+{Htt1PwUuwZv4J-i-h-8siQKq5K2Q6Z^5nfJiP#?
zD^vLWjpLA6{SfCQoC-9zKykSfRUbX+OmJ|2C(4~V#_>R!t9ypEr()Sq*Noh}2l3`s
z?pwZ|RZrea+oEOh?f=D+Jxqqi_SeJ$Yxer*A){Z%O2%HiXEtXZ*%
zM^Vt#
zt)KfN&hGu|j+pOA2pUMAAFJvVYI!xHrje4@KjPoZkFh6YQd>A0v!vp}J5_pB5he^n
z15(N=(Qdnrsgv_rn3=+u;e$vWoZ;kD>B-{G7$70QJ_2GGJTVu2@d^?E#t&s&;b`t0kj})M
z{#=vhC9_`=^h<&i4~%9j(-@*CD2gEAba>!&Z-i_c+cc?e4RZL?I<{@vL$RjPTwX;j&hB~W#xYo%
z_c^K*JN9MAp!12YItBch1*lTu3WQ16QGsUK(Va8Y$f3kI4J#5_TG7IWreIw^8Z*&F
zNC?8~O0lBXzL;`JwwPrt;)nl&>TgF0nkZot!UjS{w!DQvx^$#V1Gk2qPbQW2;Iuux)5r7ffnOo`YkuLxVpbKPAY3(LNDX77x@-@S8(nPu+I?6O4o!r43b
z-1GSTzVm(G`Od+UOeS$Vi}!X0FYHCbLE!BdMi)=6seCLL@P8)YGirT4Po>}IpXl|#
zr|}a>#AET;$#^{3+#5IRdwOHp?^}xyt9XC3^XQ(FRp`6_@V{xQ7xS}}hzt4+U
zz?0|#Ok(O|G5I?gv+;TSfDxv;iuo)?(i=ts{@#wR*x%bade)CyaC9qi49S)UO+dgu
z+B)sQ%2A=`!v4fvUh@hNzf8cL1Y0Nu4~dKbeC0?OW$21|4>q@Se{ag-V{Z(WNWlaI
zeE$nIGp9}*wXP&&&Na+d0>4E2=(#Z~@CHdF3ezY>d$+OY_{o;1?_Jq&xKJVm5wNuW
zrwNbOj1FxGcsnDW*hK=Ti43REtdjl;^d?5MHC*UkHsy&^uNFcgF9Cx0p850SXDiFF
z%8zyfF=`#=a6-=*%K-dQ#86^3U%^ktFFf%hl1OAUxbEkG1PIT+XAQGzDZ
z$5ODJUpJR)MJs*&AIFKEG3%4lJP3qK(dR8i<7IQp_%EM*k|bO;<{Cz&`6Dk!S67Zq
zEGd$sn(eLthVtP&hdfXZyzwFM`T^k7d0^$Ez}*v*2>7p{$mm1m$dOBk;UuxRu0&1(
z?BIF-`SOL6#spURJ&Z1WHx6Dw@-#udd)NlO?PDEarK?dF|vLiISnlMG0laVFI2(-pu5+t
zwFzMnrafzWmlNYlnUSe~GUvOqBJu4YcYY+g-9{X+KE@U3PGt~o4U0Hy?HFLi!@$lT
z0;|3PM3`x~3b6>2u(6GmUpZ~lD?nzRovX1ZS{j_Wb0ho6;
z@cBBRZn6zpZJU^h)0Tur%3!=z`KXCWhfq8OB!hEM@<#EY>=w2|O_uWur?m$MCSe;4sl23cIo|
z(`YRC6!4vez+KdOm_&v{giRQQmE$oeb1wbil6W7RjW34%2wEwzDsNDx(8g!pIeg#+
zN7#f>ScO@2juRWDCAcr(*Mi)D`ZZ!oOm0wpgAZa(cbBJj=^_X#8*tmZM!;7RL@oCW
zmyWkiwJ-`R5n0hFy#gUWYHkDk%#i?LC4sCJF#LX$c|944$*rUGEF{7xto;cna0KC=
zwPp?Se|;fF3v0^!9TV~-sQiTf+~OjPDVXGO&55=B_Y8m37=CRCqp&(7I~!TDy$fw|
z6D7>6ZmM)J#o}=hZ9Gyd{}~%)I3bFBse}WH7$K~Dx8l7+AaS)D=X>KAQ_M`_W{9%Q
z-~A2Pvn)FR@Zjb@`EMhGiwEqiwen62$N%|Ysv3KXi1?4jM?y13vQ
zb_36BvNi9t>?EoE$DFtPPhk^LVP$J;n{=Gmyx4|+Ue(3gP4MHl4*^^DW`oZKH=p8!
zO&Eo>KY{qR_ptMPD`JH%rL>{rp>7j&^3wCtRUYTKy?7n9aF!nCh6hj4Y|iC^oL#4!
z-wG|+)#*236GmZWC5`FY<*xZbJzCCP!jUP}s2joTF%v|3gw&5tn#88JfM4#g59-p4
z4k_dB;Gu&>-yI;bSzVjxnNO<9Z3X0Nr_kBT`vqYXR<^NB$npUl(U{%!oIQ_SyeX&~
zRpOpSG6G;7fvi=CBghiNyjo(QH>9usiD8X80)V2B#c=4sDFu$xAwK|ko2mI8i
zvUNFbKG9whjj%6CNHoS*=h^=8=D%z(+PtRL01u|l#|~M$$F;xmoin`QIM#rAVdIxB
z$9&W(O(4}puGad_5j;aj-o)#UAkWFxou$>-0OV--WC9&|ay9c^ws%CoKgYANN}-}V
zrvF|74xL7iFtJ}NOIA3@G$Csh*r4-|d$Et5o?|f?w$djnY4o!+2i{b>icIH`+4ce6
zwNU{dve`yg>hXqRsC!kUY=ToIl9vs
z!^BG(R#Z4ir`q+ty${HKr;P>fpJ894I3YA^rUG8%u(El}Y)%2koABQEH{fgR+QcH;
zXNzpfNkC0fr+&qasx?coW^xr4%^VN!xY2+jN2Yye4>`0qlP1&7Xrs&E!)K5<(S&Vl
zHe(G(6Znq)rsK@wN)d)?f1S&>yi2?)f*cv0BXf?YS
z)h{v)QK&rLi0=9Zto!v&Y-C^3vFx7oa?5}O)FrhN)H$LH?#HL^t;G*`xioE36?6Ft
zfZ^7-QgR;~sOf$~$zudHoUuea#{q^|ij(vES{TP4H?WTEW+`I(TkR;?2
z)Hh05qAMQ37nn1B^Nv!~hyntYg>KiZPwcosE-E}hVfQAINLSG@^*$xF0P-97o2=#15sR*AD
zMxg44>M(Cq1haiU)EFL=a>=4jG*ax~ze{Ekt?@V-TH0`6_lJ1v*f}(^e_N`M8sdcl
zXIo1KO(5OTHY*X4X$4udGAeqf{;gf!*=M*0Rt)DlSc#altfH(+iP@lmvyUZ1Cg2R9
zC%FiyB9;j0siHdhjzsZWBos|l=m{|MIVjuYmHz?htk1I0000(t=7`oEXlGh%ZrS6*?}>HA%
z6Jrxhz);K(La@ya0~rWwLd;e;0k2@ZFY+cKYqg|Sch5P$)uwl~x__%%M7QdzfA|0Y
zE$6&|t{Z_htBeJ`73b5U^z~>!F;5zz_i$ZU;jIWSD@Q4Cfy+dxNt3x#?eV*lg46
zcE=>A!(Qvi&6#Szu-k0#Idbf^HUkbN;Pt`n?(yvKdSUqB^SHZvwsg5YJ3JouR=3x?
zdF*#Ky-iz}mB!A6*mIr80H$ARjiJ=K=Nt^EV%_d3{DpN2lkbV_ojI+nPI`oN(zU>#~KAj1nNww>&zf
zuCgNkYF9z-cXRE&k%pH*yPJTXW`cf<=z2_ylmBBU8Tmj?3E(J$&sKnLk8z-_qx;z-
z4ae@Adilp2Gf7592)O-rBWJ;^nG1`H@~+6s@zvS9EkJjJA9P<&@>nPGq(QAKGIA^6
zwH2eY$Jp4?+WE-Jb?cYidTY8fk1XJT67c53l$ge3A$Rlk18e;
zVo+_VS2!xr>2)4xX>;GZ!J<4{M}-M|kAP2e
zYfltIvsVO&C}?PG>bSG!qAg3Qn!Jf7k?ehGB|tiU^Xd~$9bN8PS?K7TY3pgEn#m&I
z!%^!c!kbf$c6Z+Ty+>NEnf`+vYtv~X(ni3t8;$%QoK<&Ed9iV2jG=b9$x*%rt22o<HXxmZqsTKxzAGrDe4b?M`Uj*cn*R!<{~taQWU0BX=!
z7sKBh3O?St|L7$r|8Sq>9+Ra65PRF!)f0YPQ<=NeX|%eG?jxBquyLU6*CiTiUv4G3
zd_`^b4P8Hp&H`P&fRh^S3^z$cE
z?g6(iFbaNpe(iG=Mc%n~4_m#b_4CvR=~*Wu9GmwLb9%`qP2iX<7;1YFJoa)lwAx<1
z|BpMqbL&bUhwjNsN(5YTiIIEv_p4tRUTXiEy+>Y7N@YpD78X3&(hjWO3B2-m;7@<^
zpI>qYu>7Y$Nnuz93}<+9DscFy=am~)@Bh~0j|YNMVfQ8QHR)(5b-sP)su7P=me>X@
z_(zTc?{5WOUI)DXA+YTL(8V}kKynyHiYjbqYH*b$Af^hZ*BC0uFri6K_%lWu+BM#9n}@3^R_-(d5D-SOvW(x
zn*f3T{(U9?Qd8l$-U0geS+4WD*`l|zWxw$e@Df}4dmjT0EH>v5QWw_Ukz+$mh3onc
z?l0LwZ2Ik@_KN^!U_7(9aORYnyroV%1852vXQ6b_i0(KDyty7&{b%6A9Y9OE%o8zi
z=Hy`FXy={8=3PSc5qm7L|
z!-o}4ws~Z96Sgmr1x(_X!_4wmPXG`6UlI=w_#-|Dql&Vk$-lgydl9kwC3`{Kref=>
zjF+7U*Z3;qdZ&ZwBtPKmE1Q_trFIhcDPktm=Y^UIVEP!Kkh7F22{95@hVrta;YODG`+Ze3{JNbrpEwHm(Ye4Aw{U!LH*o9az{p|YclQ6guo7m%
zP8h~ZA+8FVH4AyQRmK8mE+;2?vcb~Dt4iP3^0{PaN)2$%iNLx41e`n(C}kTpSUrh)
znk5RBbKSyB*v*>dTgs$kjzVG*Am(0PjBCov3abZm>o4K@tP>g06yS4D0%nW@4W-~t
zulRdGVfB)W+O8p%F(>$A62Rg)##CZ{o|CayPg{CRYjNtOj9$(h?+5*&*}#O6VIgIU
z^NX4sdx5YMhP?h89#KaqQ3)Vp#)H@4q{ipnK4}cDbh(PC6k9S`hnRUY28J=ifzzk^Te?20>B+SUo1msqg;R16ehy$T)l3_HO=YJWv^i1HCNkAoA
zem09R6qYRGjXWXY(umleXlP&UhIH?9i!-Wh%pvK@Rur^Uv?@a6N0s61f;>`}ElUT3O077)24{mth@r3)rYv*B99uL3aw>~)Mqb_k_-cancKa
zEQKjkiBS0w1k~2TS?0nFyKOKvv(x}e_tU~smV;^XkT1IEu%l>*0P9>@hVdn7ow70DbN2rFTTAO`l
zQ`7p=Lr49)VLl@S+I+_)_U0n2v7A>f;CZ@*n4+x{TuR&r0nP2$&YQoQ?g9-&`dYrO
z4I0n9?cby!@PpN>7HXZq;DFW)U3@xMSqJS8v$}jDpt%FPm~Px2;Aw8fHf__?DzU*j
zAXC}3iu%`20WZF9dLL2oJjQx%AS60g{t+0)aq40A6stw$5vI*8_=IVPDEM#{1i
zE!0(JOBswoIqBNx-T{8`81T`~#G#~mkcvo)sMiToVH-|BxDoa|_zK={Y~%f8Y_N%1
znl^6y=bHY3pYD1JOJOQ(nO8^;2q(bX*oe*}&Dh#82xlZW?FN4LDzI)_x*_df`ofZ!
zZWFeAGQGTRxu31w0a8Ql+@JgW0d9(C6fY%F%fIbW
zzA$5bO3B21SUpwt!zwv#YHH~TGhrtTStcn*VHzP(3Fz%AolBm=3hqSzOnNNYUeL%E
z{M1_Dd9{Hwih^p>$5LIgC!{LcSEj8^$?ljmTR$-qcEXV5>5Qg+RL-ab^j5(t;+w
zb>yj3dcm$kz^XTUQ?%eiUQsm9L>|kTGyI!2QprZf{_1b}citqs#VOGGcOJyE!jMVE
z)FCkmAY;_rXutPGEZ=?*&GKdYJ5#2?(Z*f=muN68OGLi8j%YG_8h6gj0n{6k5v6-A
z_p)63ZF5&kGD(;i2`gc?;zcZFBJI6cMolj!0ljy3?>UIWeBsg4yV;WaV?6b|La?7w
z|8p+XGLedjwarx9fi61>SadN^O9R>;(N4hp0=UJ5~-rA&hy&?
zxv#5?p!5FwT+Tyi-J(~!W
z-L0)?_;4HUAOXv`c-LN099<6w7bb9?tnm&!AN(o(tnH{QM_f?@Iddj(>J
z{?q;Dgb4U=Pt3T#TKtK%{{6(oaR`l@QP4%HXQ>{7?-5qQjF;oKem&kN0fAny{8>CZ
zrV8IJEx_5@K1L$?oUoCx!?8oKffB4Ilh_
zrq4~iPq^3bLgyPBuuvGWOvidL>$7+X5uj9!Fx%y2sEE)0ale>{eJn3Ggb
zA9^9QJpF!mG~H5>Cck0i(yQ>_UmB0uGsXkuTu~6qg;Qbm?#@LI`Dd*{ixB&pi@~)8
z=lhR|)v@mz=qo1pbg$L&Iq|>trU&sOmf--8xPz`}~#b~AKQS_i+k`w{Tfh1_iQaq{><9>c29+^4@buzf}(c=x&WX@Fq
zy;ya_F3uK_6pfg*NJ?)znKIfxh|+nsi43kn<0|)-J=pkP&)|ysBjC6G>HXk(B~5@5
zAqn(zP+VAu(YJjc5713KRqALY^}bqdqSJ}N1uNtLHfSHJ29l3a3MQ`QW+L2hmS3u@!xUV_ocq&QHXRAo%n2}AI*G+rS
zz2g8@a?#fcmRI0wq)UsCnpi@BiqLDA+bm3s>4S`m#
z1p|s9F`|3$olUr9&0p~*>({`e-`)%?*b0!1Saa#Qnpl=3LQWgdRUSBzcER|LD%8#LdlxV|EmnNm$sHCj5N)eDMk
zsnv50PaFTY%#%yjU^&Z8Cm9Sjn6aId^*z&xkhBq?oPv6T40PG>GK~DmdH7eZ!~bUW
zFqDj@uU|!a^!8efdI!RF4Y-tAd!!a0*g3QRGc4ezkN)icfG
zl2!r&xsV8jFWA=2msywO=c9b#1-P2PpUV{pqehieHSy2}2~9cbP|wh)I8}Qgz_mVK
zEnTaWb!e7O13(?zwNg(a{!9c@O`s
zNmo}7c6W4QGY3*{KKdHg@-b3j9s-^HVqk%1ex_uEfa967OYmIATIz`qn?_rPTN9c2
zB;=o|n(AD+IDzkwD&gIu^aMlHcCJ;d2&gC1FEP--MZO-L^Qd>b(cIR}S;9taB;fBd
zJ&m#ay2{NRc^b%V*3K_;uhVqXYz3x7T2c47spWJSHjXAv#2me$zGf0h2V{t8Yr&UOs*+1)K!
z>izi~RjI1F>YTlQXRY;HYwfjQX3UJ4F*8oo5U29R<)2(f^s?;$y!_MEL>+-G1wzy{1u0;d=pIpmh)gXzk-GP2SV2ymj|0
z&f!!X_GxyrhzXt_>y4$4@|vFD`HTcQ*+=f-wZyL33BcBGE~cs`CS!?+ux(pJT`A&G
zONeFlfvEp^FdgTkJKtgH1D#ejALx8hY2WvCE>qC`y*YKxM;c#o&NBd-#-Up;q<3qY
z+PCfGPv4f{Be%^XnTk-EutjCkMqK=y7GnI%s;x%+`15m}x|&)$5sTT0Sj3J-9mlpU
zO9)FKgtjax99!C!l(Jlf1xhK;SAGyEjn+czK>L0WArlm0{pR`ecQP>U;O04Gh%LZo^8`$+kU+(b-j&0UutWX&{85ONFkykkh4)DjtGIE
z5kl*%RH|JooktVVC}k6LyV5^GB+Y5x$
zLMSOvN*hegNT4Ykcist~d+#k2nvM{?`Z+dzdjU&lRmw!nl~vVI!FekJA-=E5|K=-q
zka+9c*DS9{Cg&%jk=kg~nFVUT3jBF!zgnxH5*?%@eo6>cBhXQ_LJ%O(NTEaQl)hG?
z5hyJ%?^qxt8j;sp_A2Z_gf%FnHGr`Dq-D2&Jd)3Q-I;u$XDFRM@|$1n+W)b;$c(-L
z-Y40z&+6~X>rAew4t5WyH~rCJ@{epJvS>b^|Id55^UfIn9rt+byNkKv{r`i)qYo3k
z`UM{U_Cgj^#i_4Ih~zcz)
D%NyeHSTq*ov$M4-uC;7&ktJE}+AMZ#
zvCxtlfdcJ^hX<+vstDRsC|{!kVIDn&RwJ@OfRS^KMA`yjY4hkvqzf_v!U17}u+c~Y
zCGdRRT=Z!S6uSdoZx+(sX+`7h*<3#N^8SwOm4AMqFd9(*O#0#f5}iXCeYii%6@S!B
z{*ettuDXWDzxQL_{DH<974}+2Ma0FmEve8pTYol6Mg7BVBo;I{7e}qaH4&-Lb5qs=
zNhF0TBv3h51Q~}y9-#}Md>|MhNm`YjOY{F>Qe_#%RelyqpbHQb4WI%sLgs`lBX$--
z+E^ljC8G-?Zek8HwgDNrMIi@^e(tccidz;mI1m5BZ9M%~_moMctv|CBTDrF7S43Pj
zqr#5Yra5#z*#kKeSMTJWzn#tc6-ha(I%!ocNGY*q(=da2@w-bG&7D1KZ9E=bYNJ-5
zi>stAtg%FKu7!u72)bw_Oppha_ff;v#t4NMqv-sYXJS0IuE!zs8a;--3qT5ijM`Yy
zP(rzATml>d?Y1fr*@bZT`pA9RZ2n+td-u+BKX9;V_)B-a$?88mD7*X8!M1&Ab=zm#
z$vm=___7#c$@4P+8lI?^*U`Q|&z$RjKm1zHZ`O#K>n_$p{QK~0+wNK$OV+3CxGpSq
zbnp(>mDfcavCfeyDJejZ27fTXPb(v7kRe3G_<%ST7tiaNI$aJfV^B#O?4-m_Sy)LF
z5j-W+1y64;7S#iuuzsEuk;ZiApuhf(ow*U!_Cb1o^E**{^KMP3^jdzog1Jjv#M)gm
zDy)1w__YQRC}FX62j!}&GNGRCh_%rXYJpUlr0~1Ydawl=*u5Bk01Z&)rjSf0I>T<%j6mJtE9YWo)*q80)VIgb^gL7iyO{M
z#+?mP7cR2!&a*_J!X^hn27-(tCozi8hrdKm8ku=H>I@iKVNUQ`tF#;VK
z3UIb45!sy1`Y#-6Y~OP6M~^fcN%USBJheyeY3^5Nz4xF3(EG3AU;gz`06y_ArvZRE
ze!ZB6w~ergz6Z{xsxBqOhNp&C`OAO*7W<|%+LuRd@8(3@c~8_8YaNk81Vi8tdIT8}
zN}6%h)5-2u5#y^lArw|ZW2YQ#Rp99u`JL$$CG^`_9jPv#)OdNB{DXVTr7_Tq1hA
z2X*arFHJbN(=p_UvweO9K(^q?;!wsZz5eh2x2V4TtkyrCRVBY&TPZ)4bTVrlza3F%
zfnp~F83(jAEOE+g*EdG^=@sW1N>6POWE_f}h(ZgF-%irWtesUUKQ*gL{>M93cYOGV
zpSSBue^eOCIJtr+%jfcxR$+Q*18Q3q>#{xk?I%Gi&fuHtJz5rwhobc37m3U>@wP;jmaBJ+OgRHFnN4f$lVJTTjQOEKHU;Lsk
z?f-Pg%YB>P@t0j40A0Ua=H@)@ExhHR)|=0SvupYA$DiWg{_PC{pgTWg9zXZ3_aVgh
z4LkVRa?x<}{$Uh6`Sr!~=gzWjtW7x|OhnXrTW7!@DB@>rggTAX)oTC}Qenq^>`K=c
z39<#by?3zq@2x%R=a>9R5&}hQa|i)eiAD$rIa+97J3e-W8;B%1&*@6%`R}g&!gtPo
z|B>ebiVv@dr1Q$Fx#57)?dzdo9k<{9eeSvEBn6Px7a+v50HlAjL?+*QXgH$0^M{Mq
z*Cga;QZa5!xWyR3kR}-N(aM=oStT>4OfQs&c@?O@07?ol3^trAAw8@L8>>QOif(Q&
z$1i#_>I-ZCpmAI2>&n-ne2q>%6}Giw_-VwphMj}Eez5S$x(e$bs}thwaih7#M`ZUC`14FCT%ITg=~`zXGm158?@v
z6BkM%feti5MhT&u^EnaUrzp;C)cWZ;u+%}!
zZa(n<(!H}0HReS2{IFIuyt6Hw2H5f+_3LLk>}yX8OXd(Zph&TX~tXe*S~NL<`z@Z8;inALG&0p!0hk~>}zkq%G4qXE<
zlmpL)d^o+X4UQcGal(5}OH@waq2qQ+iiG>7qNo0^r@vPHFBku5-*aQv$lvOnI0>Y6
zJv>!JT-L+0->47~SIYUfbrk^~_~+!Rd3CY(S0$|*qqY|pI!!2fB&XnR*m0SYWm2T2
z69T3P`5|a+gXS)1>M-Cr&<4$&=Czg{V+V~l;t>NPAzqCXX^qM$A*B_yD-zWe_KiYB
z`yTkG#Nc(G%IuwgTUT-Ww=1opr<8L|uhvgi!KE%jbWSgTN@9zLOV}T>TbbGb>i@V!+^c#Y!{$!c}boru6V1Up3
ztR4OFdJED@2*Hd0R{7>RHO>d)5i#EudGOOdI4XE+P)pcIbCjwZowfXzVc04|f~8W~@CUL&`qtcl3#x2@%r!q82LW
z#hh5QvLf=9g>}I*pSq17SKU0&2k^}Qab@K@bE>q~>()&&fX+T_K02=sE?6BghBI)>
zWzMzLN&AC|m|f>udGIneXz?lzixaP&N-9N+W=>uXa-lTo=rht~S0n6bG>*N)T_e7k
z((;tlQ#nxl;3Sf*W-SDM#W>L@YY1I%$9Koo{k_|LGp@Cjb|$jmU-?r8;`y?89)Z
z%JrB5^yrI!+OP`TiZz_^B;xMkE^L2eIdiF2xQq5d(
zQSjvtJ|#K|DgE1R$LM+k6JC2VzF-hu4l+ksqr|80if|gPck0eP%_0`v$$ARt~
zUlrn^+~FTMc2%q=sJK;WU1kTqJ_)oHp8Y<2_KyXr+dQo|rvQ&@&$U-J)Y`X3UA;tz
zz(W@uw6+l^H{%Kd1Dw4_;DwiA+aVK5*wV-c&$vVABP
zI>Ess2w`V~kKaV%qBUF{jm9oaCTxtyx%jGv*2(AtDrEtdOPtA8O~XM}jB`+JQLckzg@vzf>K
zS;(dH(RV+fU%LeQ)c4?%@6sX?U;%3J93o2_xFHsi=SCtn1X)l@p6rJ6L|Sb!nUfGk
z{pfPAjQN4ON|-+j7S+R&*|2oBd7MAXJf^B)un&HJ7+i-bPJ9t&?JS>2Bu*@%&RyET
z4Q;(V31rTkqpgNY^ryae^l2IwK;QkCF)lj_Al{1BTPzI$OXsqnelKk;s8h(LW6eBdj`2%sl+8mHO+x)J&1|3ZoL0{^dW
zXGvAudRrplE{wU_NFS~A8#U7Yy3|vdiW-kOrQ)z`jseP9i;ap}HpkdE(TD++72bNo
zB^$%7JGKRuV0zN-1ZE9`EHT%nn20W{iWlGZSGNa!;=JGxz)c&FU;hOv>K=y#`ktxz
z#FsvVEHST*D_NV0i)*8iNYb*4;Cb>?2hW7eX^g;iZIiRSxFH0~+z~Z3e-_NH29XFs
z;u98AGc`X;_Lozxvc$BeZ3+TUT8=|B;wDpZ@7j$k*#;Z}ST-B^<4qGYvT*^la*nyE
zxhqCqn^VK`%D7r*OKl@inm3&7JZa3ywu}@>#-P5^NS9R$;GAW!@eEj455xOHeFGMM
z=#>(z6zNwAm@cU^fp