• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.content.pm.ServiceInfo;
26 import android.graphics.Bitmap;
27 import android.graphics.drawable.Drawable;
28 import android.service.media.MediaBrowserService;
29 import android.text.TextUtils;
30 import android.util.Log;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.annotation.VisibleForTesting;
35 
36 import com.android.car.apps.common.BitmapUtils;
37 import com.android.car.apps.common.IconCropper;
38 import com.android.car.media.common.R;
39 
40 import java.net.URISyntaxException;
41 import java.util.List;
42 import java.util.Objects;
43 
44 
45 /**
46  * This represents a source of media content. It provides convenient methods to access media source
47  * metadata, such as application name and icon.
48  */
49 public class MediaSource {
50     private static final String TAG = "MediaSource";
51 
52     @NonNull
53     private final ComponentName mBrowseService;
54     @NonNull
55     private final CharSequence mDisplayName;
56     @NonNull
57     private final Drawable mIcon;
58     @NonNull
59     private final IconCropper mIconCropper;
60 
61     /**
62      * Creates a {@link MediaSource} for the given {@link ComponentName}
63      */
64     @Nullable
create(@onNull Context context, @NonNull ComponentName componentName)65     public static MediaSource create(@NonNull Context context,
66             @NonNull ComponentName componentName) {
67         ServiceInfo serviceInfo = getBrowseServiceInfo(context, componentName);
68 
69         String className = serviceInfo != null ? serviceInfo.name : null;
70         if (TextUtils.isEmpty(className)) {
71             Log.w(TAG,
72                     "No MediaBrowserService found in component " + componentName.flattenToString());
73             return null;
74         }
75 
76         try {
77             String packageName = componentName.getPackageName();
78             CharSequence displayName = extractDisplayName(context, serviceInfo, packageName);
79             Drawable icon = extractIcon(context, serviceInfo, packageName);
80             ComponentName browseService = new ComponentName(packageName, className);
81             return new MediaSource(browseService, displayName, icon, new IconCropper(context));
82         } catch (PackageManager.NameNotFoundException e) {
83             Log.w(TAG, "Component not found " + componentName.flattenToString());
84             return null;
85         }
86     }
87 
88     @VisibleForTesting
MediaSource(@onNull ComponentName browseService, @NonNull CharSequence displayName, @NonNull Drawable icon, @NonNull IconCropper iconCropper)89     public MediaSource(@NonNull ComponentName browseService, @NonNull CharSequence displayName,
90             @NonNull Drawable icon, @NonNull IconCropper iconCropper) {
91         mBrowseService = browseService;
92         mDisplayName = displayName;
93         mIcon = icon;
94         mIconCropper = iconCropper;
95     }
96 
97     /**
98      * @return the {@link ServiceInfo} corresponding to a {@link MediaBrowserService} in the media
99      * source, or null if the media source doesn't implement {@link MediaBrowserService}. A non-null
100      * result doesn't imply that this service is accessible. The consumer code should attempt to
101      * connect and handle rejections gracefully.
102      */
103     @Nullable
getBrowseServiceInfo(@onNull Context context, @NonNull ComponentName componentName)104     private static ServiceInfo getBrowseServiceInfo(@NonNull Context context,
105             @NonNull ComponentName componentName) {
106         PackageManager packageManager = context.getPackageManager();
107         Intent intent = new Intent();
108         intent.setAction(MediaBrowserService.SERVICE_INTERFACE);
109         intent.setPackage(componentName.getPackageName());
110         List<ResolveInfo> resolveInfos = packageManager.queryIntentServices(intent,
111                 PackageManager.GET_RESOLVED_FILTER);
112         if (resolveInfos == null || resolveInfos.isEmpty()) {
113             return null;
114         }
115         String className = componentName.getClassName();
116         if (TextUtils.isEmpty(className)) {
117             return resolveInfos.get(0).serviceInfo;
118         }
119         for (ResolveInfo resolveInfo : resolveInfos) {
120             ServiceInfo result = resolveInfo.serviceInfo;
121             if (result.name.equals(className)) {
122                 return result;
123             }
124         }
125         return null;
126     }
127 
128     /**
129      * @return a proper app name. Checks service label first. If failed, uses application label
130      * as fallback.
131      */
132     @NonNull
extractDisplayName(@onNull Context context, @Nullable ServiceInfo serviceInfo, @NonNull String packageName)133     private static CharSequence extractDisplayName(@NonNull Context context,
134             @Nullable ServiceInfo serviceInfo, @NonNull String packageName)
135             throws PackageManager.NameNotFoundException {
136         if (serviceInfo != null && serviceInfo.labelRes != 0) {
137             return serviceInfo.loadLabel(context.getPackageManager());
138         }
139         ApplicationInfo applicationInfo =
140                 context.getPackageManager().getApplicationInfo(packageName,
141                         PackageManager.GET_META_DATA);
142         return applicationInfo.loadLabel(context.getPackageManager());
143     }
144 
145     /**
146      * @return a proper icon. Checks service icon first. If failed, uses application icon as
147      * fallback.
148      */
149     @NonNull
extractIcon(@onNull Context context, @Nullable ServiceInfo serviceInfo, @NonNull String packageName)150     private static Drawable extractIcon(@NonNull Context context, @Nullable ServiceInfo serviceInfo,
151             @NonNull String packageName) throws PackageManager.NameNotFoundException {
152         Drawable appIcon = serviceInfo != null ? serviceInfo.loadIcon(context.getPackageManager())
153                 : context.getPackageManager().getApplicationIcon(packageName);
154 
155         return BitmapUtils.maybeFlagDrawable(context, appIcon);
156     }
157 
158     /**
159      * @return media source human readable name for display.
160      */
161     @NonNull
getDisplayName()162     public CharSequence getDisplayName() {
163         return mDisplayName;
164     }
165 
166     /**
167      * @return the package name of this media source.
168      */
169     @NonNull
getPackageName()170     public String getPackageName() {
171         return mBrowseService.getPackageName();
172     }
173 
174     /**
175      * @return a {@link ComponentName} referencing this media source's {@link MediaBrowserService}.
176      */
177     @NonNull
getBrowseServiceComponentName()178     public ComponentName getBrowseServiceComponentName() {
179         return mBrowseService;
180     }
181 
182     /**
183      * @return a {@link Drawable} as the media source's icon.
184      */
185     @NonNull
getIcon()186     public Drawable getIcon() {
187         return mIcon;
188     }
189 
190     /**
191      * Returns this media source's icon cropped to a predefined shape (see
192      * {@link #IconCropper(Context)} on where and how the shape is defined).
193      */
getCroppedPackageIcon()194     public Bitmap getCroppedPackageIcon() {
195         return mIconCropper.crop(mIcon);
196     }
197 
198     @Override
equals(Object o)199     public boolean equals(Object o) {
200         if (this == o) return true;
201         if (o == null || getClass() != o.getClass()) return false;
202         MediaSource that = (MediaSource) o;
203         return Objects.equals(mBrowseService, that.mBrowseService);
204     }
205 
206     @Override
hashCode()207     public int hashCode() {
208         return Objects.hash(mBrowseService);
209     }
210 
211     @Override
212     @NonNull
toString()213     public String toString() {
214         return mBrowseService.flattenToString();
215     }
216 
217     /**
218      * @return an intent to open the media source selector, or null if no source selector is
219      * configured.
220      * @param popup Whether the intent should point to the regular app selector (false), which
221      *              would open the selected media source in Media Center, or the "popup" version
222      *              (true), which would just select the source and dismiss itself.
223      */
224     @Nullable
getSourceSelectorIntent(Context context, boolean popup)225     public static Intent getSourceSelectorIntent(Context context, boolean popup) {
226         String uri = context.getString(popup ? R.string.launcher_popup_intent
227                 : R.string.launcher_intent);
228         try {
229             return uri != null && !uri.isEmpty() ? Intent.parseUri(uri, Intent.URI_INTENT_SCHEME)
230                     : null;
231         } catch (URISyntaxException e) {
232             throw new IllegalStateException("Wrong app-launcher intent: " + uri, e);
233         }
234     }
235 }
236