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 androidx.media; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.os.Bundle; 27 import android.support.v4.media.session.MediaSessionCompat; 28 import android.text.TextUtils; 29 30 import androidx.annotation.IntDef; 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.RestrictTo; 34 35 import java.lang.annotation.Retention; 36 import java.lang.annotation.RetentionPolicy; 37 import java.util.List; 38 39 /** 40 * Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}. 41 * If it's representing a session service, it may not be ongoing. 42 * <p> 43 * This may be passed to apps by the session owner to allow them to create a 44 * {@link MediaController2} to communicate with the session. 45 * <p> 46 * It can be also obtained by {@link MediaSessionManager}. 47 */ 48 // New version of MediaSession.Token for following reasons 49 // - Stop implementing Parcelable for updatable support 50 // - Represent session and library service (formerly browser service) in one class. 51 // Previously MediaSession.Token was for session and ComponentName was for service. 52 public final class SessionToken2 { 53 /** 54 * @hide 55 */ 56 @RestrictTo(LIBRARY_GROUP) 57 @Retention(RetentionPolicy.SOURCE) 58 @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE}) 59 public @interface TokenType { 60 } 61 62 /** 63 * Type for {@link MediaSession2}. 64 */ 65 public static final int TYPE_SESSION = 0; 66 67 /** 68 * Type for {@link MediaSessionService2}. 69 */ 70 public static final int TYPE_SESSION_SERVICE = 1; 71 72 /** 73 * Type for {@link MediaLibraryService2}. 74 */ 75 public static final int TYPE_LIBRARY_SERVICE = 2; 76 77 //private final SessionToken2Provider mProvider; 78 79 // From the return value of android.os.Process.getUidForName(String) when error 80 private static final int UID_UNKNOWN = -1; 81 82 private static final String KEY_UID = "android.media.token.uid"; 83 private static final String KEY_TYPE = "android.media.token.type"; 84 private static final String KEY_PACKAGE_NAME = "android.media.token.package_name"; 85 private static final String KEY_SERVICE_NAME = "android.media.token.service_name"; 86 private static final String KEY_ID = "android.media.token.id"; 87 private static final String KEY_SESSION_TOKEN = "android.media.token.session_token"; 88 89 private final int mUid; 90 private final @TokenType int mType; 91 private final String mPackageName; 92 private final String mServiceName; 93 private final String mId; 94 private final MediaSessionCompat.Token mSessionCompatToken; 95 private final ComponentName mComponentName; 96 97 /** 98 * Constructor for the token. You can only create token for session service or library service 99 * to use by {@link MediaController2} or {@link MediaBrowser2}. 100 * 101 * @param context The context. 102 * @param serviceComponent The component name of the media browser service. 103 */ SessionToken2(@onNull Context context, @NonNull ComponentName serviceComponent)104 public SessionToken2(@NonNull Context context, @NonNull ComponentName serviceComponent) { 105 this(context, serviceComponent, UID_UNKNOWN); 106 } 107 108 /** 109 * Constructor for the token. You can only create token for session service or library service 110 * to use by {@link MediaController2} or {@link MediaBrowser2}. 111 * 112 * @param context The context. 113 * @param serviceComponent The component name of the media browser service. 114 * @param uid uid of the app. 115 * @hide 116 */ 117 @RestrictTo(LIBRARY_GROUP) SessionToken2(@onNull Context context, @NonNull ComponentName serviceComponent, int uid)118 public SessionToken2(@NonNull Context context, @NonNull ComponentName serviceComponent, 119 int uid) { 120 if (serviceComponent == null) { 121 throw new IllegalArgumentException("serviceComponent shouldn't be null"); 122 } 123 mComponentName = serviceComponent; 124 mPackageName = serviceComponent.getPackageName(); 125 mServiceName = serviceComponent.getClassName(); 126 // Calculate uid if it's not specified. 127 final PackageManager manager = context.getPackageManager(); 128 if (uid < 0) { 129 try { 130 uid = manager.getApplicationInfo(mPackageName, 0).uid; 131 } catch (PackageManager.NameNotFoundException e) { 132 throw new IllegalArgumentException("Cannot find package " + mPackageName); 133 } 134 } 135 mUid = uid; 136 137 // Infer id and type from package name and service name 138 String id = getSessionIdFromService(manager, MediaLibraryService2.SERVICE_INTERFACE, 139 serviceComponent); 140 if (id != null) { 141 mId = id; 142 mType = TYPE_LIBRARY_SERVICE; 143 } else { 144 // retry with session service 145 mId = getSessionIdFromService(manager, MediaSessionService2.SERVICE_INTERFACE, 146 serviceComponent); 147 mType = TYPE_SESSION_SERVICE; 148 } 149 if (mId == null) { 150 throw new IllegalArgumentException("service " + mServiceName + " doesn't implement" 151 + " session service nor library service. Use service's full name."); 152 } 153 mSessionCompatToken = null; 154 } 155 156 /** 157 * @hide 158 */ 159 @RestrictTo(LIBRARY_GROUP) SessionToken2(int uid, int type, String packageName, String serviceName, String id, MediaSessionCompat.Token sessionCompatToken)160 SessionToken2(int uid, int type, String packageName, String serviceName, 161 String id, MediaSessionCompat.Token sessionCompatToken) { 162 mUid = uid; 163 mType = type; 164 mPackageName = packageName; 165 mServiceName = serviceName; 166 mComponentName = (mType == TYPE_SESSION) ? null 167 : new ComponentName(packageName, serviceName); 168 mId = id; 169 mSessionCompatToken = sessionCompatToken; 170 } 171 172 @Override hashCode()173 public int hashCode() { 174 final int prime = 31; 175 return mType 176 + prime * (mUid 177 + prime * (mPackageName.hashCode() 178 + prime * (mId.hashCode() 179 + prime * (mServiceName != null ? mServiceName.hashCode() : 0)))); 180 } 181 182 @Override equals(Object obj)183 public boolean equals(Object obj) { 184 if (!(obj instanceof SessionToken2)) { 185 return false; 186 } 187 SessionToken2 other = (SessionToken2) obj; 188 return mUid == other.mUid 189 && TextUtils.equals(mPackageName, other.mPackageName) 190 && TextUtils.equals(mServiceName, other.mServiceName) 191 && TextUtils.equals(mId, other.mId) 192 && mType == other.mType; 193 } 194 195 @Override toString()196 public String toString() { 197 return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType 198 + " service=" + mServiceName + " sessionCompatToken=" + mSessionCompatToken + "}"; 199 } 200 201 /** 202 * @return uid of the session 203 */ getUid()204 public int getUid() { 205 return mUid; 206 } 207 208 /** 209 * @return package name 210 */ getPackageName()211 public @NonNull String getPackageName() { 212 return mPackageName; 213 } 214 215 /** 216 * @return service name. Can be {@code null} for TYPE_SESSION. 217 */ getServiceName()218 public @Nullable String getServiceName() { 219 return mServiceName; 220 } 221 222 /** 223 * @hide 224 * @return component name of this session token. Can be null for TYPE_SESSION. 225 */ 226 @RestrictTo(LIBRARY_GROUP) getComponentName()227 public ComponentName getComponentName() { 228 return mComponentName; 229 } 230 231 /** 232 * @return id 233 */ getId()234 public String getId() { 235 return mId; 236 } 237 238 /** 239 * @return type of the token 240 * @see #TYPE_SESSION 241 * @see #TYPE_SESSION_SERVICE 242 * @see #TYPE_LIBRARY_SERVICE 243 */ getType()244 public @TokenType int getType() { 245 return mType; 246 } 247 248 /** 249 * Create a token from the bundle, exported by {@link #toBundle()}. 250 * 251 * @param bundle 252 * @return 253 */ fromBundle(@onNull Bundle bundle)254 public static SessionToken2 fromBundle(@NonNull Bundle bundle) { 255 if (bundle == null) { 256 return null; 257 } 258 final int uid = bundle.getInt(KEY_UID); 259 final @TokenType int type = bundle.getInt(KEY_TYPE, -1); 260 final String packageName = bundle.getString(KEY_PACKAGE_NAME); 261 final String serviceName = bundle.getString(KEY_SERVICE_NAME); 262 final String id = bundle.getString(KEY_ID); 263 final MediaSessionCompat.Token token = bundle.getParcelable(KEY_SESSION_TOKEN); 264 265 // Sanity check. 266 switch (type) { 267 case TYPE_SESSION: 268 if (token == null) { 269 throw new IllegalArgumentException("Unexpected token for session," 270 + " SessionCompat.Token=" + token); 271 } 272 break; 273 case TYPE_SESSION_SERVICE: 274 case TYPE_LIBRARY_SERVICE: 275 if (TextUtils.isEmpty(serviceName)) { 276 throw new IllegalArgumentException("Session service needs service name"); 277 } 278 break; 279 default: 280 throw new IllegalArgumentException("Invalid type"); 281 } 282 if (TextUtils.isEmpty(packageName) || id == null) { 283 throw new IllegalArgumentException("Package name nor ID cannot be null."); 284 } 285 return new SessionToken2(uid, type, packageName, serviceName, id, token); 286 } 287 288 /** 289 * Create a {@link Bundle} from this token to share it across processes. 290 * @return Bundle 291 */ toBundle()292 public Bundle toBundle() { 293 Bundle bundle = new Bundle(); 294 bundle.putInt(KEY_UID, mUid); 295 bundle.putString(KEY_PACKAGE_NAME, mPackageName); 296 bundle.putString(KEY_SERVICE_NAME, mServiceName); 297 bundle.putString(KEY_ID, mId); 298 bundle.putInt(KEY_TYPE, mType); 299 bundle.putParcelable(KEY_SESSION_TOKEN, mSessionCompatToken); 300 return bundle; 301 } 302 303 /** 304 * @hide 305 */ 306 @RestrictTo(LIBRARY_GROUP) getSessionId(ResolveInfo resolveInfo)307 public static String getSessionId(ResolveInfo resolveInfo) { 308 if (resolveInfo == null || resolveInfo.serviceInfo == null) { 309 return null; 310 } else if (resolveInfo.serviceInfo.metaData == null) { 311 return ""; 312 } else { 313 return resolveInfo.serviceInfo.metaData.getString( 314 MediaSessionService2.SERVICE_META_DATA, ""); 315 } 316 } 317 getSessionCompatToken()318 MediaSessionCompat.Token getSessionCompatToken() { 319 return mSessionCompatToken; 320 } 321 getSessionIdFromService(PackageManager manager, String serviceInterface, ComponentName serviceComponent)322 private static String getSessionIdFromService(PackageManager manager, String serviceInterface, 323 ComponentName serviceComponent) { 324 Intent serviceIntent = new Intent(serviceInterface); 325 // Use queryIntentServices to find services with MediaLibraryService2.SERVICE_INTERFACE. 326 // We cannot use resolveService with intent specified class name, because resolveService 327 // ignores actions if Intent.setClassName() is specified. 328 serviceIntent.setPackage(serviceComponent.getPackageName()); 329 330 List<ResolveInfo> list = manager.queryIntentServices( 331 serviceIntent, PackageManager.GET_META_DATA); 332 if (list != null) { 333 for (int i = 0; i < list.size(); i++) { 334 ResolveInfo resolveInfo = list.get(i); 335 if (resolveInfo == null || resolveInfo.serviceInfo == null) { 336 continue; 337 } 338 if (TextUtils.equals( 339 resolveInfo.serviceInfo.name, serviceComponent.getClassName())) { 340 return getSessionId(resolveInfo); 341 } 342 } 343 } 344 return null; 345 } 346 } 347