OSDN Git Service

TIF: Add TvParentalControlManager
authorJae Seo <jaeseo@google.com>
Mon, 14 Jul 2014 23:07:19 +0000 (16:07 -0700)
committerJae Seo <jaeseo@google.com>
Thu, 17 Jul 2014 23:29:36 +0000 (16:29 -0700)
Each TV input service is now 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 TvParentalControlManager.isEnabled(). Whether the TV input
service should block the content or not is determined by invoking
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.

Each TV input service also needs to continuously listen to any changes
made to the parental control settings by registering a
TvParentalControlManager.ParentalControlCallback() to the manager and
immediately reevaluate the current program with the new parental control
settings.

Bug: 13172379
Change-Id: I8e1900d4b8d28c56798986d5c3906bd418ab97ac

12 files changed:
api/current.txt
core/java/android/app/ContextImpl.java
core/java/android/content/Context.java
core/java/android/provider/Settings.java
media/java/android/media/tv/ITvInputClient.aidl
media/java/android/media/tv/ITvInputSessionCallback.aidl
media/java/android/media/tv/TvContentRating.java
media/java/android/media/tv/TvInputManager.java
media/java/android/media/tv/TvInputService.java
media/java/android/media/tv/TvParentalControlManager.java [new file with mode: 0644]
media/java/android/media/tv/TvView.java
services/core/java/com/android/server/tv/TvInputManagerService.java

index 633d0b2..c33e7e4 100644 (file)
@@ -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);
index cbfde14..cc3ad23 100644 (file)
@@ -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);
index 1dd018f..5db653f 100644 (file)
@@ -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
index bc069ca..7fa06a7 100644 (file)
@@ -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
index 423e317..403b1ba 100644 (file)
@@ -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);
 }
index e0036e1..f529595 100644 (file)
@@ -33,4 +33,5 @@ oneway interface ITvInputSessionCallback {
     void onTrackInfoChanged(in List<TvTrackInfo> tracks);
     void onVideoAvailable();
     void onVideoUnavailable(int reason);
+    void onContentBlocked(in String rating);
 }
index 905b0bd..986a7d9 100644 (file)
 
 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();
     }
 }
index 79a83b0..10c8ac7 100644 (file)
@@ -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);
index 3206320..6670c9c 100644 (file)
@@ -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 (file)
index 0000000..3741991
--- /dev/null
@@ -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();
+                }
+            });
+        }
+    }
+}
index 4ac1ba4..12a6961 100644 (file)
@@ -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) {
index 20fdefa..c2d5052 100644 (file)
@@ -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) {