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