1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.media.common.source;
18 
19 import static com.android.car.apps.common.util.CarAppsDebugUtils.idHash;
20 
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.os.Bundle;
24 import android.support.v4.media.MediaBrowserCompat;
25 import android.util.Log;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 
30 import com.android.car.media.common.MediaConstants;
31 
32 /**
33  * A helper class to connect to a single {@link MediaBrowserCompat}. Connecting to a new one
34  * automatically disconnects the previous browser. Changes of the currently connected browser are
35  * sent via {@link MediaBrowserConnector.Callback}.
36  */
37 
38 public class MediaBrowserConnector {
39 
40     private static final String TAG = "MediaBrowserConnector";
41 
42     /** The callback to receive the currently connected {@link MediaBrowserCompat}. */
43     public interface Callback {
44         /** When disconnecting, the given browser will be null. */
onConnectedBrowserChanged(@ullable MediaBrowserCompat browser)45         void onConnectedBrowserChanged(@Nullable MediaBrowserCompat browser);
46     }
47 
48     private final Context mContext;
49     private final Callback mCallback;
50     private final int mMaxBitmapSizePx;
51 
52     @Nullable private ComponentName mBrowseService;
53     @Nullable private MediaBrowserCompat mBrowser;
54 
55     /**
56      * Create a new MediaBrowserConnector.
57      *
58      * @param context The Context with which to build MediaBrowsers.
59      */
MediaBrowserConnector(@onNull Context context, @NonNull Callback callback)60     public MediaBrowserConnector(@NonNull Context context, @NonNull Callback callback) {
61         mContext = context;
62         mCallback = callback;
63         mMaxBitmapSizePx = mContext.getResources().getInteger(
64                 com.android.car.media.common.R.integer.media_items_bitmap_max_size_px);
65     }
66 
67     /** Counter so callbacks from obsolete connections can be ignored. */
68     private int mBrowserConnectionCallbackCounter = 0;
69 
70     private class BrowserConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
71 
72         private final int mSequenceNumber = ++mBrowserConnectionCallbackCounter;
73         private final String mCallbackPackage = mBrowseService.getPackageName();
74 
isValidCall(String method)75         private boolean isValidCall(String method) {
76             if (mSequenceNumber != mBrowserConnectionCallbackCounter) {
77                 Log.e(TAG, "Ignoring callback " + method + " for " + mCallbackPackage + " seq: "
78                         + mSequenceNumber + " current: " + mBrowserConnectionCallbackCounter
79                         + " current: " + mBrowseService.getPackageName());
80                 return false;
81             } else if (Log.isLoggable(TAG, Log.DEBUG)) {
82                 Log.d(TAG, method + " " + mBrowseService.getPackageName() + idHash(mBrowser));
83             }
84             return true;
85         }
86 
87         @Override
onConnected()88         public void onConnected() {
89             if (isValidCall("onConnected")) {
90                 mCallback.onConnectedBrowserChanged(mBrowser);
91             }
92         }
93 
94         @Override
onConnectionFailed()95         public void onConnectionFailed() {
96             if (isValidCall("onConnectionFailed")) {
97                 mCallback.onConnectedBrowserChanged(null);
98             }
99         }
100 
101         @Override
onConnectionSuspended()102         public void onConnectionSuspended() {
103             if (isValidCall("onConnectionSuspended")) {
104                 mCallback.onConnectedBrowserChanged(null);
105             }
106         }
107     }
108 
109     /**
110      * Creates and connects a new {@link MediaBrowserCompat} if the given {@link ComponentName}
111      * isn't null. If needed, the previous browser is disconnected.
112      * @param browseService the ComponentName of the media browser service.
113      * @see MediaBrowserCompat#MediaBrowserCompat(Context, ComponentName,
114      * MediaBrowserCompat.ConnectionCallback, android.os.Bundle)
115      */
connectTo(@ullable ComponentName browseService)116     public void connectTo(@Nullable ComponentName browseService) {
117         if (mBrowser != null && mBrowser.isConnected()) {
118             if (Log.isLoggable(TAG, Log.DEBUG)) {
119                 Log.d(TAG, "Disconnecting: " + mBrowseService.getPackageName() + idHash(mBrowser));
120             }
121             mCallback.onConnectedBrowserChanged(null);
122             mBrowser.disconnect();
123         }
124 
125         mBrowseService = browseService;
126         if (mBrowseService != null) {
127             mBrowser = createMediaBrowser(mBrowseService, new BrowserConnectionCallback());
128             if (Log.isLoggable(TAG, Log.DEBUG)) {
129                 Log.d(TAG, "Connecting to: " + mBrowseService.getPackageName() + idHash(mBrowser));
130             }
131             try {
132                 mBrowser.connect();
133             } catch (IllegalStateException ex) {
134                 // Is this comment still valid ?
135                 // Ignore: MediaBrowse could be in an intermediate state (not connected, but not
136                 // disconnected either.). In this situation, trying to connect again can throw
137                 // this exception, but there is no way to know without trying.
138                 Log.e(TAG, "Connection exception: " + ex);
139             }
140         } else {
141             mBrowser = null;
142         }
143     }
144 
145     // Override for testing.
146     @NonNull
createMediaBrowser(@onNull ComponentName browseService, @NonNull MediaBrowserCompat.ConnectionCallback callback)147     protected MediaBrowserCompat createMediaBrowser(@NonNull ComponentName browseService,
148             @NonNull MediaBrowserCompat.ConnectionCallback callback) {
149         Bundle rootHints = new Bundle();
150         rootHints.putInt(MediaConstants.EXTRA_MEDIA_ART_SIZE_HINT_PIXELS, mMaxBitmapSizePx);
151         return new MediaBrowserCompat(mContext, browseService, callback, rootHints);
152     }
153 }
154