diff options
| -rw-r--r-- | api/current.txt | 20 | ||||
| -rw-r--r-- | core/java/android/app/ContextImpl.java | 6 | ||||
| -rw-r--r-- | core/java/android/content/Context.java | 10 | ||||
| -rw-r--r-- | core/java/android/provider/Settings.java | 14 | ||||
| -rw-r--r-- | media/java/android/media/tv/ITvInputClient.aidl | 1 | ||||
| -rw-r--r-- | media/java/android/media/tv/ITvInputSessionCallback.aidl | 1 | ||||
| -rw-r--r-- | media/java/android/media/tv/TvContentRating.java | 112 | ||||
| -rw-r--r-- | media/java/android/media/tv/TvInputManager.java | 38 | ||||
| -rw-r--r-- | media/java/android/media/tv/TvInputService.java | 43 | ||||
| -rw-r--r-- | media/java/android/media/tv/TvParentalControlManager.java | 268 | ||||
| -rw-r--r-- | media/java/android/media/tv/TvView.java | 21 | ||||
| -rw-r--r-- | services/core/java/com/android/server/tv/TvInputManagerService.java | 17 |
12 files changed, 519 insertions, 32 deletions
diff --git a/api/current.txt b/api/current.txt index 633d0b246e3b..c33e7e41f65a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -7213,6 +7213,7 @@ package android.content { field public static final java.lang.String TELEPHONY_SERVICE = "phone"; field public static final java.lang.String TEXT_SERVICES_MANAGER_SERVICE = "textservices"; field public static final java.lang.String TV_INPUT_SERVICE = "tv_input"; + field public static final java.lang.String TV_PARENTAL_CONTROL_SERVICE = "tv_parental_control"; field public static final java.lang.String UI_MODE_SERVICE = "uimode"; field public static final java.lang.String USB_SERVICE = "usb"; field public static final java.lang.String USER_SERVICE = "user"; @@ -16402,10 +16403,12 @@ package android.media.session { package android.media.tv { - public class TvContentRating { + public final class TvContentRating { ctor public TvContentRating(java.lang.String); ctor public TvContentRating(java.lang.String, java.lang.String[]); method public java.lang.String flattenToString(); + method public java.lang.String getRating(); + method public java.util.List<java.lang.String> getSubRatings(); method public static android.media.tv.TvContentRating unflattenFromString(java.lang.String); field public static final java.lang.String RATING_KR_12 = "RATING_KR_12"; field public static final java.lang.String RATING_KR_15 = "RATING_KR_15"; @@ -16600,6 +16603,7 @@ package android.media.tv { public abstract class TvInputService.Session implements android.view.KeyEvent.Callback { ctor public TvInputService.Session(); method public void dispatchChannelRetuned(android.net.Uri); + method public void dispatchContentBlocked(android.media.tv.TvContentRating); method public void dispatchTrackInfoChanged(java.util.List<android.media.tv.TvTrackInfo>); method public void dispatchVideoAvailable(); method public void dispatchVideoUnavailable(int); @@ -16621,6 +16625,19 @@ package android.media.tv { method public void setOverlayViewEnabled(boolean); } + public final class TvParentalControlManager { + method public void addParentalControlCallback(android.media.tv.TvParentalControlManager.ParentalControlCallback, android.os.Handler); + method public final boolean isEnabled(); + method public final boolean isRatingBlocked(android.media.tv.TvContentRating); + method public void removeParentalControlCallback(android.media.tv.TvParentalControlManager.ParentalControlCallback); + } + + public static abstract class TvParentalControlManager.ParentalControlCallback { + ctor public TvParentalControlManager.ParentalControlCallback(); + method public void onBlockedRatingsChanged(); + method public void onEnabledChanged(boolean); + } + public final class TvTrackInfo implements android.os.Parcelable { method public boolean containsKey(java.lang.String); method public int describeContents(); @@ -16678,6 +16695,7 @@ package android.media.tv { public static abstract class TvView.TvInputListener { ctor public TvView.TvInputListener(); method public void onChannelRetuned(java.lang.String, android.net.Uri); + method public void onContentBlocked(java.lang.String, android.media.tv.TvContentRating); method public void onError(java.lang.String, int); method public void onTrackInfoChanged(java.lang.String, java.util.List<android.media.tv.TvTrackInfo>); method public void onVideoAvailable(java.lang.String); diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index cbfde14fa361..cc3ad23bdf0c 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -74,6 +74,7 @@ import android.media.AudioManager; import android.media.MediaRouter; import android.media.session.MediaSessionManager; import android.media.tv.ITvInputManager; +import android.media.tv.TvParentalControlManager; import android.media.tv.TvInputManager; import android.net.ConnectivityManager; import android.net.IConnectivityManager; @@ -729,6 +730,11 @@ class ContextImpl extends Context { return new TvInputManager(service, UserHandle.myUserId()); }}); + registerService(TV_PARENTAL_CONTROL_SERVICE, new ServiceFetcher() { + public Object getService(ContextImpl ctx) { + return new TvParentalControlManager(ctx); + }}); + registerService(NETWORK_SCORE_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { return new NetworkScoreManager(ctx); diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 1dd018f3d1df..5db653fd0206 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2822,6 +2822,16 @@ public abstract class Context { public static final String TV_INPUT_SERVICE = "tv_input"; /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.media.tv.TvParentalControlManager} for obtaining parental + * control settings and listening to their changes. + * + * @see #getSystemService + * @see android.media.tv.TvParentalControlManager + */ + public static final String TV_PARENTAL_CONTROL_SERVICE = "tv_parental_control"; + + /** * {@link android.net.NetworkScoreManager} for managing network scoring. * @see #getSystemService * @see android.net.NetworkScoreManager diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index bc069cab1200..7fa06a73eff3 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3627,6 +3627,20 @@ public final class Settings { */ public static final String PARENTAL_CONTROL_REDIRECT_URL = "parental_control_redirect_url"; + + /** + * Whether the TV parental control is enabled. + * @hide + */ + public static final String TV_PARENTAL_CONTROL_ENABLED = "tv_parental_control_enabled"; + + /** + * List of TV content ratings blocked by the user. (comma-delimited) + * @hide + */ + public static final String TV_PARENTAL_CONTROL_BLOCKED_RATINGS = + "tv_parental_control_blocked_ratings"; + /** * Settings classname to launch when Settings is clicked from All * Applications. Needed because of user testing between the old diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl index 423e317ede69..403b1ba4d9f0 100644 --- a/media/java/android/media/tv/ITvInputClient.aidl +++ b/media/java/android/media/tv/ITvInputClient.aidl @@ -36,4 +36,5 @@ oneway interface ITvInputClient { void onTrackInfoChanged(in List<TvTrackInfo> tracks, int seq); void onVideoAvailable(int seq); void onVideoUnavailable(int reason, int seq); + void onContentBlocked(in String rating, int seq); } diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl index e0036e1366b8..f52959517b62 100644 --- a/media/java/android/media/tv/ITvInputSessionCallback.aidl +++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl @@ -33,4 +33,5 @@ oneway interface ITvInputSessionCallback { void onTrackInfoChanged(in List<TvTrackInfo> tracks); void onVideoAvailable(); void onVideoUnavailable(int reason); + void onContentBlocked(in String rating); } diff --git a/media/java/android/media/tv/TvContentRating.java b/media/java/android/media/tv/TvContentRating.java index 905b0bd5629e..986a7d942933 100644 --- a/media/java/android/media/tv/TvContentRating.java +++ b/media/java/android/media/tv/TvContentRating.java @@ -16,18 +16,20 @@ package android.media.tv; +import android.annotation.SystemApi; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Log; import java.util.Arrays; -import java.util.HashMap; +import java.util.Collections; import java.util.List; import java.util.Map; /** * A class representing a TV content rating. */ -public class TvContentRating { +public final class TvContentRating { private static final String TAG = "TvContentRating"; private static final int RATING_PREFIX_LENGTH = 10; @@ -124,10 +126,10 @@ public class TvContentRating { // A mapping from two-letter country code (ISO 3166-1 alpha-2) to its rating-to-sub-ratings map. // This is used for validating the builder parameters. private static final Map<String, Map<String, String[]>> sRatings - = new HashMap<String, Map<String, String[]>>(); + = new ArrayMap<String, Map<String, String[]>>(); static { - Map<String, String[]> usRatings = new HashMap<String, String[]>(); + Map<String, String[]> usRatings = new ArrayMap<String, String[]>(); usRatings.put(RATING_US_TV_Y, null); usRatings.put(RATING_US_TV_Y7, new String[] { SUBRATING_US_FV }); usRatings.put(RATING_US_TV_G, null); @@ -139,7 +141,7 @@ public class TvContentRating { SUBRATING_US_L, SUBRATING_US_S, SUBRATING_US_V }); sRatings.put(PREFIX_RATING_US, usRatings); - Map<String, String[]> krRatings = new HashMap<String, String[]>(); + Map<String, String[]> krRatings = new ArrayMap<String, String[]>(); krRatings.put(RATING_KR_ALL, null); krRatings.put(RATING_KR_7, null); krRatings.put(RATING_KR_12, null); @@ -157,8 +159,7 @@ public class TvContentRating { * @param rating The rating constant defined in this class. */ public TvContentRating(String rating) { - mRating = rating; - mSubRatings = null; + this(rating, null); } /** @@ -168,11 +169,11 @@ public class TvContentRating { * @param subRatings The String array of sub-rating constants defined in this class. */ public TvContentRating(String rating, String[] subRatings) { - mRating = rating; - mSubRatings = subRatings; - if (TextUtils.isEmpty(mRating)) { + if (TextUtils.isEmpty(rating)) { throw new IllegalArgumentException("rating cannot be null"); } + mRating = rating; + mSubRatings = subRatings; String prefix = ""; if (mRating.length() > RATING_PREFIX_LENGTH) { prefix = mRating.substring(0, RATING_PREFIX_LENGTH); @@ -188,6 +189,10 @@ public class TvContentRating { } else { List<String> validSubRatingList = Arrays.asList(subRatings); for (String sr : mSubRatings) { + if (TextUtils.isEmpty(sr)) { + throw new IllegalArgumentException( + "subRatings cannot contain empty elements"); + } if (!validSubRatingList.contains(sr)) { Log.w(TAG, "Invalid subrating: " + sr); break; @@ -201,6 +206,52 @@ public class TvContentRating { } /** + * Returns the main rating constant. + * + * @return the rating string that starts with "RATING_" prefix as defined in this class. + */ + public String getMainRating() { + return mRating; + } + + /** + * Returns the list of sub-rating constants. + * + * @return the unmodifiable {@code List} of sub-rating strings that start with "SUBRATING_" + * prefix as defined in this class. + */ + public List<String> getSubRatings() { + if (mSubRatings == null) { + return null; + } + return Collections.unmodifiableList(Arrays.asList(mSubRatings)); + } + + + /** + * Returns a String that unambiguously describes both the rating and sub-rating information + * contained in the TvContentRating. You can later recover the TvContentRating from this string + * through {@link #unflattenFromString}. + * + * @return a new String holding rating/sub-rating information, which can later be stored in the + * database and settings. + * @see #unflattenFromString + */ + public String flattenToString() { + // TODO: Consider removing all obvious/redundant sub-strings including "RATING" and + // "SUBRATING" and find out a storage-efficient string format such as: + // <country>-<primary>/<sub1>/<sub2>/<sub3> + StringBuilder builder = new StringBuilder(mRating); + if (mSubRatings != null) { + for (String subRating : mSubRatings) { + builder.append(DELIMITER); + builder.append(subRating); + } + } + return builder.toString(); + } + + /** * Recovers a TvContentRating from a String that was previously created with * {@link #flattenToString}. * @@ -226,20 +277,35 @@ public class TvContentRating { } /** - * @return a String that unambiguously describes both the rating and sub-rating information - * contained in the TvContentRating. You can later recover the TvContentRating from this - * string through {@link #unflattenFromString}. - * @see #unflattenFromString + * Returns true if this rating has the same main rating as the specified rating and when this + * rating's sub-ratings contain the other's. + * <p> + * For example, a TvContentRating object that represents TV-PG with S(Sexual content) and + * V(Violence) contains TV-PG, TV-PG/S, TV-PG/V and itself. + * </p> + * + * @param rating The {@link TvContentRating} to check. + * @return {@code true} if this object contains {@code rating}, {@code false} otherwise. + * @hide */ - public String flattenToString() { - StringBuffer ratingStr = new StringBuffer(); - ratingStr.append(mRating); - if (mSubRatings != null) { - for (String subRating : mSubRatings) { - ratingStr.append(DELIMITER); - ratingStr.append(subRating); - } + @SystemApi + public final boolean contains(TvContentRating rating) { + if (rating == null) { + throw new IllegalArgumentException("rating cannot be null"); + } + if (!rating.getMainRating().equals(mRating)) { + return false; + } + List<String> subRatings = getSubRatings(); + List<String> subRatingsOther = rating.getSubRatings(); + if (subRatings == null && subRatingsOther == null) { + return true; + } else if (subRatings == null && subRatingsOther != null) { + return false; + } else if (subRatings != null && subRatingsOther == null) { + return true; + } else { + return subRatings.containsAll(subRatingsOther); } - return ratingStr.toString(); } } diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index 79a83b0c6b2f..10c8ac71e51d 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -36,7 +36,6 @@ import android.view.Surface; import android.view.View; import java.util.ArrayList; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -49,10 +48,13 @@ import java.util.Map; public final class TvInputManager { private static final String TAG = "TvInputManager"; + static final int VIDEO_UNAVAILABLE_REASON_START = 0; + static final int VIDEO_UNAVAILABLE_REASON_END = 3; + /** * A generic reason. Video is not available due to an unspecified error. */ - public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = 0; + public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = VIDEO_UNAVAILABLE_REASON_START; /** * Video is not available because the TV input is tuning to another channel. */ @@ -65,7 +67,7 @@ public final class TvInputManager { * Video is not available because the TV input stopped the playback temporarily to buffer more * data. */ - public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = 3; + public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = VIDEO_UNAVAILABLE_REASON_END; /** * The TV input is connected. @@ -185,6 +187,15 @@ public final class TvInputManager { } /** + * This is called when the current program content is blocked by parental controls. + * + * @param session A {@link TvInputManager.Session} associated with this callback + * @param rating The content ration of the blocked program. + */ + public void onContentBlocked(Session session, TvContentRating rating) { + } + + /** * This is called when a custom event has been sent from this session. * * @param session A {@link TvInputManager.Session} associated with this callback @@ -263,6 +274,15 @@ public final class TvInputManager { }); } + public void postContentBlocked(final TvContentRating rating) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onContentBlocked(mSession, rating); + } + }); + } + public void postSessionEvent(final String eventType, final Bundle eventArgs) { mHandler.post(new Runnable() { @Override @@ -403,6 +423,18 @@ public final class TvInputManager { } @Override + public void onContentBlocked(String rating, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postContentBlocked(TvContentRating.unflattenFromString(rating)); + } + } + + @Override public void onSessionEvent(String eventType, Bundle eventArgs, int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java index 320632022a07..6670c9c231a4 100644 --- a/media/java/android/media/tv/TvInputService.java +++ b/media/java/android/media/tv/TvInputService.java @@ -18,7 +18,6 @@ package android.media.tv; import android.annotation.SuppressLint; import android.app.Service; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.PixelFormat; @@ -267,6 +266,42 @@ public abstract class TvInputService extends Service { } /** + * Informs the application that the current program content is blocked by parent controls. + * <p> + * Each TV input service is required to query the system whether the user is allowed to + * watch the current program before showing it to the user if the parental control is turned + * on, which can be checked by calling {@link TvParentalControlManager#isEnabled}. Whether + * the TV input service should block the content or not is determined by invoking + * {@link TvParentalControlManager#isRatingBlocked} with the content rating for the current + * program. Then the TvParentalControlManager makes a judgment based on the user blocked + * ratings stored in the secure settings and returns the result. If the rating in question + * turns out to be blocked, the TV input service must immediately block the content and call + * this method with the content rating of the current program to prompt the PIN verification + * screen. + * </p><p> + * Each TV input service also needs to continuously listen to any changes made to the + * parental control settings by registering a + * {@link TvParentalControlManager.ParentalControlCallback} to the manager and immediately + * reevaluate the current program with the new parental control settings. + * </p> + * + * @param rating The content rating for the current TV program. + */ + public void dispatchContentBlocked(final TvContentRating rating) { + mHandler.post(new Runnable() { + @Override + public void run() { + try { + if (DEBUG) Log.d(TAG, "dispatchContentBlocked"); + mSessionCallback.onContentBlocked(rating.flattenToString()); + } catch (RemoteException e) { + Log.w(TAG, "error in dispatchContentBlocked"); + } + } + }); + } + + /** * Informs the application that video is not available, so the TV input cannot continue * playing the TV stream. * @@ -279,10 +314,8 @@ public abstract class TvInputService extends Service { * </ul> */ public void dispatchVideoUnavailable(final int reason) { - if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN - && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNE - && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL - && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) { + if (reason < TvInputManager.VIDEO_UNAVAILABLE_REASON_START + || reason > TvInputManager.VIDEO_UNAVAILABLE_REASON_END) { throw new IllegalArgumentException("Unknown reason: " + reason); } mHandler.post(new Runnable() { diff --git a/media/java/android/media/tv/TvParentalControlManager.java b/media/java/android/media/tv/TvParentalControlManager.java new file mode 100644 index 000000000000..37419910d749 --- /dev/null +++ b/media/java/android/media/tv/TvParentalControlManager.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2014 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; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.text.TextUtils; + +import com.android.internal.annotations.GuardedBy; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * Contains methods for accessing and monitoring the user's parental control settings. + * <p> + * To obtain a handle to the TV parental control manager, do the following: + * <p> + * <code> + * <pre>TvParentalControlManager tvParentalControlManager = + * (TvParentalControlManager) context.getSystemService(Context.TV_PARENTAL_CONTROL_SERVICE); + * </pre> + * </code> + */ +public final class TvParentalControlManager { + /** Default parental control enabled value. */ + private static final int DEFAULT_ENABLED = 0; + + private final Handler mHandler = new Handler(); + + private final ContentResolver mContentResolver; + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private final List<ParentalControlCallbackRecord> mParentalControlCallbackRecordList = + new LinkedList<ParentalControlCallbackRecord>(); + + @GuardedBy("mLock") + private final List<TvContentRating> mBlockedRatings = new ArrayList<TvContentRating>(); + + @GuardedBy("mLock") + private String mBlockedRatingsString; + + /** + * Creates a new parental control manager for the specified context. + * + * @hide + */ + public TvParentalControlManager(Context context) { + mContentResolver = context.getContentResolver(); + } + + /** + * Returns the user's parental control enabled state. + * + * @return {@code true} if the user enabled the parental control, {@code false} otherwise. + */ + public final boolean isEnabled() { + return Settings.Secure.getInt(mContentResolver, Settings.Secure.TV_PARENTAL_CONTROL_ENABLED, + DEFAULT_ENABLED) == 1; + } + + /** + * Checks whether a given TV content rating is blocked by the user. + * + * @param rating The TV content rating to check. + * @return {@code true} if blocked, {@code false} if not blocked or parental control is + * disabled. + */ + public final boolean isRatingBlocked(TvContentRating rating) { + if (!isEnabled()) { + // Parental control is disabled. Enjoy watching good stuff. + return false; + } + + // Update the blocked ratings only when they change. + final String blockedRatingsString = Settings.Secure.getString(mContentResolver, + Settings.Secure.TV_PARENTAL_CONTROL_BLOCKED_RATINGS); + synchronized (mLock) { + if (!TextUtils.equals(blockedRatingsString, mBlockedRatingsString)) { + mBlockedRatingsString = blockedRatingsString; + updateBlockedRatingsLocked(); + } + for (TvContentRating blockedRating : mBlockedRatings) { + if (rating.contains(blockedRating)) { + return true; + } + } + } + return false; + } + + private void updateBlockedRatingsLocked() { + mBlockedRatings.clear(); + if (TextUtils.isEmpty(mBlockedRatingsString)) { + return; + } + for (String blockedRatingString : mBlockedRatingsString.split("\\s*,\\s*")) { + mBlockedRatings.add(TvContentRating.unflattenFromString(blockedRatingString)); + } + } + + /** + * Adds a callback for monitoring the changes in the user's parental control settings. + * + * @param callback The callback to add. + * @param handler a {@link Handler} that the settings change will be delivered to. + */ + public void addParentalControlCallback(ParentalControlCallback callback, + Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + synchronized (mLock) { + if (mParentalControlCallbackRecordList.isEmpty()) { + registerObserver(Settings.Secure.TV_PARENTAL_CONTROL_ENABLED); + registerObserver(Settings.Secure.TV_PARENTAL_CONTROL_BLOCKED_RATINGS); + } + mParentalControlCallbackRecordList.add( + new ParentalControlCallbackRecord(callback, handler)); + } + } + + private void registerObserver(String key) { + mContentResolver.registerContentObserver(Settings.Secure.getUriFor(key), false, + mContentObserver); + } + + /** + * Removes a callback previously added using {@link #addParentalControlCallback}. + * + * @param callback The callback to remove. + */ + public void removeParentalControlCallback(ParentalControlCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + synchronized (mLock) { + for (Iterator<ParentalControlCallbackRecord> it = + mParentalControlCallbackRecordList.iterator(); it.hasNext();) { + ParentalControlCallbackRecord record = it.next(); + if (record.getCallback() == callback) { + it.remove(); + break; + } + } + } + } + + private void notifyEnabledChanged() { + final boolean enabled = isEnabled(); + synchronized (mLock) { + for (ParentalControlCallbackRecord record : mParentalControlCallbackRecordList) { + record.postEnabledChanged(enabled); + } + } + } + + private final ContentObserver mContentObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + final String uriPath = uri.getPath(); + final String name = uriPath.substring(uriPath.lastIndexOf('/') + 1); + if (Settings.Secure.TV_PARENTAL_CONTROL_ENABLED.equals(name)) { + notifyEnabledChanged(); + } else if (Settings.Secure.TV_PARENTAL_CONTROL_BLOCKED_RATINGS.equals(name)) { + // We only need a single callback when multiple ratings change in rapid + // succession. + mHandler.removeCallbacks(mBlockedRatingsChangedRunnable); + mHandler.post(mBlockedRatingsChangedRunnable); + } + } + }; + + /** + * Runnable posted when user blocked ratings change. This is used to prevent unnecessary change + * notifications when multiple ratings change in rapid succession. + */ + private final Runnable mBlockedRatingsChangedRunnable = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + for (ParentalControlCallbackRecord record : mParentalControlCallbackRecordList) { + record.postBlockedRatingsChanged(); + } + } + } + }; + + /** + * Callback for changes in parental control settings, including enabled state. + */ + public static abstract class ParentalControlCallback { + /** + * Called when the parental control enabled state changes. + * + * @param enabled the user's parental control enabled state + */ + public void onEnabledChanged(boolean enabled) {} + + /** + * Called when the user blocked ratings change. + * <p> + * When this is invoked, one should immediately call + * {@link TvParentalControlManager#isRatingBlocked} to reevaluate the current content since + * the user might have changed her mind and blocked the rating for the content. + * + * @see TvParentalControlManager#isRatingBlocked + */ + public void onBlockedRatingsChanged() {} + } + + private static final class ParentalControlCallbackRecord { + private final ParentalControlCallback mCallback; + private final Handler mHandler; + + public ParentalControlCallbackRecord(ParentalControlCallback callback, Handler handler) { + mCallback = callback; + mHandler = handler; + } + + public ParentalControlCallback getCallback() { + return mCallback; + } + + public void postEnabledChanged(final boolean enabled) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onEnabledChanged(enabled); + } + }); + } + + public void postBlockedRatingsChanged() { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onBlockedRatingsChanged(); + } + }); + } + } +} diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java index 4ac1ba422a7a..12a696141597 100644 --- a/media/java/android/media/tv/TvView.java +++ b/media/java/android/media/tv/TvView.java @@ -548,6 +548,15 @@ public class TvView extends ViewGroup { } /** + * This is called when the current program content is blocked by parental controls. + * + * @param inputId The ID of the TV input bound to this view. + * @param rating The content rating of the blocked program. + */ + public void onContentBlocked(String inputId, TvContentRating rating) { + } + + /** * This is invoked when a custom event from the bound TV input is sent to this view. * * @param eventType The type of the event. @@ -648,6 +657,7 @@ public class TvView extends ViewGroup { } } + @Override public void onVideoAvailable(Session session) { if (DEBUG) { Log.d(TAG, "onVideoAvailable()"); @@ -657,6 +667,7 @@ public class TvView extends ViewGroup { } } + @Override public void onVideoUnavailable(Session session, int reason) { if (DEBUG) { Log.d(TAG, "onVideoUnavailable(" + reason + ")"); @@ -667,6 +678,16 @@ public class TvView extends ViewGroup { } @Override + public void onContentBlocked(Session session, TvContentRating rating) { + if (DEBUG) { + Log.d(TAG, "onContentBlocked()"); + } + if (mListener != null) { + mListener.onContentBlocked(mInputId, rating); + } + } + + @Override public void onSessionEvent(TvInputManager.Session session, String eventType, Bundle eventArgs) { if (this != mSessionCallback) { diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 20fdefa32c7a..c2d5052c9f3a 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -520,6 +520,23 @@ public final class TvInputManagerService extends SystemService { } @Override + public void onContentBlocked(String rating) { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "onContentBlocked()"); + } + if (sessionState.mSession == null || sessionState.mClient == null) { + return; + } + try { + sessionState.mClient.onContentBlocked(rating, sessionState.mSeq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onContentBlocked"); + } + } + } + + @Override public void onSessionEvent(String eventType, Bundle eventArgs) { synchronized (mLock) { if (DEBUG) { |