diff --git a/core/api/current.txt b/core/api/current.txt index 9f8e61f8732e6..d4b5e30e5660b 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -26305,6 +26305,13 @@ package android.media.tv.interactive { public final class TvIAppManager { } + public abstract class TvIAppService extends android.app.Service { + ctor public TvIAppService(); + method public final android.os.IBinder onBind(android.content.Intent); + field public static final String SERVICE_INTERFACE = "android.media.tv.interactive.TvIAppService"; + field public static final String SERVICE_META_DATA = "android.media.tv.interactive.app"; + } + } package android.mtp { diff --git a/media/java/android/media/tv/interactive/TvIAppInfo.java b/media/java/android/media/tv/interactive/TvIAppInfo.java new file mode 100644 index 0000000000000..b3b5aeee86b6e --- /dev/null +++ b/media/java/android/media/tv/interactive/TvIAppInfo.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2021 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.tv.interactive; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * This class is used to specify meta information of a TV interactive app. + * @hide + */ +public final class TvIAppInfo implements Parcelable { + private static final boolean DEBUG = false; + private static final String TAG = "TvIAppInfo"; + + private final ResolveInfo mService; + private final String mId; + private List mTypes = new ArrayList<>(); + + private TvIAppInfo(ResolveInfo service, String id, List types) { + mService = service; + mId = id; + mTypes = types; + } + + protected TvIAppInfo(Parcel in) { + mService = ResolveInfo.CREATOR.createFromParcel(in); + mId = in.readString(); + in.readStringList(mTypes); + } + + public static final Creator CREATOR = new Creator() { + @Override + public TvIAppInfo createFromParcel(Parcel in) { + return new TvIAppInfo(in); + } + + @Override + public TvIAppInfo[] newArray(int size) { + return new TvIAppInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + mService.writeToParcel(dest, flags); + dest.writeString(mId); + dest.writeStringList(mTypes); + } + + public String getId() { + return mId; + } + + /** + * Returns the information of the service that implements this TV IApp service. + */ + public ServiceInfo getServiceInfo() { + return mService.serviceInfo; + } + + /** + * A convenience builder for creating {@link TvIAppInfo} objects. + */ + public static final class Builder { + private static final String XML_START_TAG_NAME = "tv-iapp"; + private final Context mContext; + private final ResolveInfo mResolveInfo; + private final List mTypes = new ArrayList<>(); + + /** + * Constructs a new builder for {@link TvIAppInfo}. + * + * @param context A Context of the application package implementing this class. + * @param component The name of the application component to be used for the + * {@link TvIAppService}. + */ + public Builder(Context context, ComponentName component) { + if (context == null) { + throw new IllegalArgumentException("context cannot be null."); + } + Intent intent = new Intent(TvIAppService.SERVICE_INTERFACE).setComponent(component); + mResolveInfo = context.getPackageManager().resolveService(intent, + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); + if (mResolveInfo == null) { + throw new IllegalArgumentException("Invalid component. Can't find the service."); + } + mContext = context; + } + + /** + * Creates a {@link TvIAppInfo} instance with the specified fields. Most of the information + * is obtained by parsing the AndroidManifest and {@link TvIAppService#SERVICE_META_DATA} + * for the {@link TvIAppService} this TV IApp implements. + * + * @return TvIAppInfo containing information about this TV IApp service. + */ + public TvIAppInfo build() { + ComponentName componentName = new ComponentName(mResolveInfo.serviceInfo.packageName, + mResolveInfo.serviceInfo.name); + String id; + id = generateIAppServiceId(componentName); + parseServiceMetadata(); + return new TvIAppInfo(mResolveInfo, id, mTypes); + } + + private static String generateIAppServiceId(ComponentName name) { + return name.flattenToShortString(); + } + + private void parseServiceMetadata() { + ServiceInfo si = mResolveInfo.serviceInfo; + PackageManager pm = mContext.getPackageManager(); + try (XmlResourceParser parser = + si.loadXmlMetaData(pm, TvIAppService.SERVICE_META_DATA)) { + if (parser == null) { + throw new IllegalStateException("No " + TvIAppService.SERVICE_META_DATA + + " meta-data found for " + si.name); + } + + Resources res = pm.getResourcesForApplication(si.applicationInfo); + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // move to the START_TAG + } + + String nodeName = parser.getName(); + if (!XML_START_TAG_NAME.equals(nodeName)) { + throw new IllegalStateException("Meta-data does not start with " + + XML_START_TAG_NAME + " tag for " + si.name); + } + + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.TvIAppService); + CharSequence[] types = sa.getTextArray( + com.android.internal.R.styleable.TvIAppService_supportedTypes); + for (CharSequence cs : types) { + mTypes.add(cs.toString()); + } + + sa.recycle(); + } catch (IOException | XmlPullParserException e) { + throw new IllegalStateException( + "Failed reading meta-data for " + si.packageName, e); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException("No resources found for " + si.packageName, e); + } + } + } +} diff --git a/media/java/android/media/tv/interactive/TvIAppService.java b/media/java/android/media/tv/interactive/TvIAppService.java index b385d9c7c11a3..8863729bfab3f 100644 --- a/media/java/android/media/tv/interactive/TvIAppService.java +++ b/media/java/android/media/tv/interactive/TvIAppService.java @@ -38,22 +38,32 @@ import java.util.List; /** * The TvIAppService class represents a TV interactive applications RTE. - * @hide */ public abstract class TvIAppService extends Service { private static final boolean DEBUG = false; private static final String TAG = "TvIAppService"; - private final Handler mServiceHandler = new ServiceHandler(); + // TODO: cleanup and unhide APIs. /** - * This is the interface name that a service implementing an environment to run Tv IApp should - * say that it support -- that is, this is the action it uses for its intent filter. To be - * supported, the service must also require the BIND_TV_IAPP permission so that other - * applications cannot abuse it. + * This is the interface name that a service implementing a TV IApp service should say that it + * supports -- that is, this is the action it uses for its intent filter. To be supported, the + * service must also require the android.Manifest.permission#BIND_TV_IAPP permission so + * that other applications cannot abuse it. */ - public static final String SERVICE_INTERFACE = "android.media.tv.TvIAppService"; + public static final String SERVICE_INTERFACE = "android.media.tv.interactive.TvIAppService"; + /** + * Name under which a TvIAppService component publishes information about itself. This meta-data + * must reference an XML resource containing an + * <{@link android.R.styleable#TvIAppService tv-iapp}> + * tag. + */ + public static final String SERVICE_META_DATA = "android.media.tv.interactive.app"; + + private final Handler mServiceHandler = new ServiceHandler(); + + /** @hide */ @Override public final IBinder onBind(Intent intent) { ITvIAppService.Stub tvIAppServiceBinder = new ITvIAppService.Stub() { @@ -83,12 +93,17 @@ public abstract class TvIAppService extends Service { * * @param iAppServiceId The ID of the TV IApp associated with the session. * @param type The type of the TV IApp associated with the session. + * @hide */ @Nullable - public abstract Session onCreateSession(@NonNull String iAppServiceId, int type); + public Session onCreateSession(@NonNull String iAppServiceId, int type) { + // TODO: make it abstract when unhide + return null; + } /** * Base class for derived classes to implement to provide a TV interactive app session. + * @hide */ public abstract static class Session implements KeyEvent.Callback { private final Object mLock = new Object(); @@ -113,6 +128,7 @@ public abstract class TvIAppService extends Service { /** * Starts TvIAppService session. + * @hide */ public void onStartIApp() { } @@ -144,6 +160,7 @@ public abstract class TvIAppService extends Service { /** * Releases TvIAppService session. + * @hide */ public void onRelease() { } @@ -245,6 +262,7 @@ public abstract class TvIAppService extends Service { /** * Implements the internal ITvIAppSession interface. + * @hide */ public static class ITvIAppSessionWrapper extends ITvIAppSession.Stub { private final Session mSessionImpl; diff --git a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java index cf212dffb3737..8bbac7a024aae 100644 --- a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java @@ -18,31 +18,41 @@ package com.android.server.tv.interactive; import android.annotation.Nullable; import android.app.ActivityManager; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.media.tv.interactive.ITvIAppClient; import android.media.tv.interactive.ITvIAppManager; import android.media.tv.interactive.ITvIAppService; import android.media.tv.interactive.ITvIAppServiceCallback; import android.media.tv.interactive.ITvIAppSession; import android.media.tv.interactive.ITvIAppSessionCallback; +import android.media.tv.interactive.TvIAppInfo; import android.media.tv.interactive.TvIAppService; import android.os.Binder; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; +import android.util.ArrayMap; import android.util.SparseArray; import android.view.Surface; import com.android.internal.annotations.GuardedBy; +import com.android.internal.content.PackageMonitor; import com.android.server.SystemService; import com.android.server.utils.Slogf; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -82,6 +92,75 @@ public class TvIAppManagerService extends SystemService { mContext = context; } + @GuardedBy("mLock") + private void buildTvIAppServiceListLocked(int userId, String[] updatedPackages) { + UserState userState = getOrCreateUserStateLocked(userId); + userState.mPackageSet.clear(); + + if (DEBUG) { + Slogf.d(TAG, "buildTvIAppServiceListLocked"); + } + PackageManager pm = mContext.getPackageManager(); + List services = pm.queryIntentServicesAsUser( + new Intent(TvIAppService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, + userId); + List iAppList = new ArrayList<>(); + + for (ResolveInfo ri : services) { + ServiceInfo si = ri.serviceInfo; + // TODO: add BIND_TV_IAPP permission and check it here + + ComponentName component = new ComponentName(si.packageName, si.name); + try { + TvIAppInfo info = new TvIAppInfo.Builder(mContext, component).build(); + iAppList.add(info); + } catch (Exception e) { + Slogf.e(TAG, "failed to load TV IApp service " + si.name, e); + continue; + } + userState.mPackageSet.add(si.packageName); + } + + // sort the iApp list by iApp service id + Collections.sort(iAppList, Comparator.comparing(TvIAppInfo::getId)); + Map iAppMap = new HashMap<>(); + ArrayMap tiasAppCount = new ArrayMap<>(iAppMap.size()); + for (TvIAppInfo info : iAppList) { + String iAppServiceId = info.getId(); + if (DEBUG) { + Slogf.d(TAG, "add " + iAppServiceId); + } + // Running count of IApp for each IApp service + Integer count = tiasAppCount.get(iAppServiceId); + count = count == null ? 1 : count + 1; + tiasAppCount.put(iAppServiceId, count); + TvIAppState iAppState = userState.mIAppMap.get(iAppServiceId); + if (iAppState == null) { + iAppState = new TvIAppState(); + } + iAppState.mInfo = info; + iAppState.mUid = getIAppUid(info); + iAppMap.put(iAppServiceId, iAppState); + iAppState.mIAppNumber = count; + } + + // TODO: notify iApp added / removed + + userState.mIAppMap.clear(); + userState.mIAppMap = iAppMap; + } + + private int getIAppUid(TvIAppInfo info) { + try { + return getContext().getPackageManager().getApplicationInfo( + info.getServiceInfo().packageName, 0).uid; + } catch (PackageManager.NameNotFoundException e) { + Slogf.w(TAG, "Unable to get UID for " + info, e); + return Process.INVALID_UID; + } + } + @Override public void onStart() { if (DEBUG) { @@ -90,6 +169,96 @@ public class TvIAppManagerService extends SystemService { publishBinderService(Context.TV_IAPP_SERVICE, new BinderService()); } + @Override + public void onBootPhase(int phase) { + if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { + registerBroadcastReceivers(); + } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) { + synchronized (mLock) { + buildTvIAppServiceListLocked(mCurrentUserId, null); + } + } + } + + private void registerBroadcastReceivers() { + PackageMonitor monitor = new PackageMonitor() { + private void buildTvIAppServiceList(String[] packages) { + int userId = getChangingUserId(); + synchronized (mLock) { + if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { + buildTvIAppServiceListLocked(userId, packages); + } + } + } + + @Override + public void onPackageUpdateFinished(String packageName, int uid) { + if (DEBUG) Slogf.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")"); + // This callback is invoked when the TV iApp service is reinstalled. + // In this case, isReplacing() always returns true. + buildTvIAppServiceList(new String[] { packageName }); + } + + @Override + public void onPackagesAvailable(String[] packages) { + if (DEBUG) { + Slogf.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")"); + } + // This callback is invoked when the media on which some packages exist become + // available. + if (isReplacing()) { + buildTvIAppServiceList(packages); + } + } + + @Override + public void onPackagesUnavailable(String[] packages) { + // This callback is invoked when the media on which some packages exist become + // unavailable. + if (DEBUG) { + Slogf.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages) + + ")"); + } + if (isReplacing()) { + buildTvIAppServiceList(packages); + } + } + + @Override + public void onSomePackagesChanged() { + if (DEBUG) Slogf.d(TAG, "onSomePackagesChanged()"); + if (isReplacing()) { + if (DEBUG) Slogf.d(TAG, "Skipped building TV iApp list due to replacing"); + // When the package is updated, buildTvIAppServiceListLocked is called in other + // methods instead. + return; + } + buildTvIAppServiceList(null); + } + + @Override + public boolean onPackageChanged(String packageName, int uid, String[] components) { + // The iApp list needs to be updated in any cases, regardless of whether + // it happened to the whole package or a specific component. Returning true so that + // the update can be handled in {@link #onSomePackagesChanged}. + return true; + } + }; + monitor.register(mContext, null, UserHandle.ALL, true); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_USER_SWITCHED); + intentFilter.addAction(Intent.ACTION_USER_REMOVED); + intentFilter.addAction(Intent.ACTION_USER_STARTED); + intentFilter.addAction(Intent.ACTION_USER_STOPPED); + mContext.registerReceiverAsUser(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // TODO: handle switch / start / stop user + } + }, UserHandle.ALL, intentFilter, null, null); + } + private SessionState getSessionState(IBinder sessionToken) { // TODO: implement user state and get session from it. return null; @@ -462,19 +631,20 @@ public class TvIAppManagerService extends SystemService { // A mapping from the token of a TV IApp session to its state. private final Map mSessionStateMap = new HashMap<>(); + // A set of all TV IApp service packages. + private final Set mPackageSet = new HashSet<>(); + private UserState(int userId) { mUserId = userId; } } private static final class TvIAppState { - private final String mIAppServiceId; - private final ComponentName mComponentName; - - TvIAppState(String id, ComponentName componentName) { - mIAppServiceId = id; - mComponentName = componentName; - } + private String mIAppServiceId; + private ComponentName mComponentName; + private TvIAppInfo mInfo; + private int mUid; + private int mIAppNumber; } private final class SessionState implements IBinder.DeathRecipient {