Home | History | Annotate | Download | only in tv
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.tv;
     18 
     19 import android.content.Context;
     20 import android.content.Intent;
     21 import android.media.tv.TvContentRating;
     22 import android.media.tv.TvInputManager;
     23 import android.os.Environment;
     24 import android.os.Handler;
     25 import android.os.UserHandle;
     26 import android.text.TextUtils;
     27 import android.util.AtomicFile;
     28 import android.util.Slog;
     29 import android.util.Xml;
     30 
     31 import com.android.internal.util.FastXmlSerializer;
     32 import com.android.internal.util.XmlUtils;
     33 
     34 import libcore.io.IoUtils;
     35 
     36 import org.xmlpull.v1.XmlPullParser;
     37 import org.xmlpull.v1.XmlPullParserException;
     38 import org.xmlpull.v1.XmlSerializer;
     39 
     40 import java.io.BufferedInputStream;
     41 import java.io.BufferedOutputStream;
     42 import java.io.File;
     43 import java.io.FileNotFoundException;
     44 import java.io.FileOutputStream;
     45 import java.io.IOException;
     46 import java.io.InputStream;
     47 import java.util.ArrayList;
     48 import java.util.Collections;
     49 import java.util.List;
     50 
     51 /**
     52  * Manages persistent state recorded by the TV input manager service as an XML file. This class is
     53  * not thread-safe thus caller must acquire lock on the data store before accessing it. File format:
     54  * <code>
     55  * &lt;tv-input-manager-state>
     56  *   &lt;blocked-ratings>
     57  *     &lt;rating string="XXXX" />
     58  *   &lt;/blocked-ratings>
     59  *   &lt;parental-control enabled="YYYY" />
     60  * &lt;/tv-input-manager-state>
     61  * </code>
     62  */
     63 final class PersistentDataStore {
     64     private static final String TAG = "TvInputManagerService";
     65 
     66     private final Context mContext;
     67 
     68     private final Handler mHandler = new Handler();
     69 
     70     // The atomic file used to safely read or write the file.
     71     private final AtomicFile mAtomicFile;
     72 
     73     private final List<TvContentRating> mBlockedRatings =
     74             Collections.synchronizedList(new ArrayList<TvContentRating>());
     75 
     76     private boolean mBlockedRatingsChanged;
     77 
     78     private boolean mParentalControlsEnabled;
     79 
     80     private boolean mParentalControlsEnabledChanged;
     81 
     82     // True if the data has been loaded.
     83     private boolean mLoaded;
     84 
     85     public PersistentDataStore(Context context, int userId) {
     86         mContext = context;
     87         File userDir = Environment.getUserSystemDirectory(userId);
     88         if (!userDir.exists()) {
     89             if (!userDir.mkdirs()) {
     90                 throw new IllegalStateException("User dir cannot be created: " + userDir);
     91             }
     92         }
     93         mAtomicFile = new AtomicFile(new File(userDir, "tv-input-manager-state.xml"));
     94     }
     95 
     96     public boolean isParentalControlsEnabled() {
     97         loadIfNeeded();
     98         return mParentalControlsEnabled;
     99     }
    100 
    101     public void setParentalControlsEnabled(boolean enabled) {
    102         loadIfNeeded();
    103         if (mParentalControlsEnabled != enabled) {
    104             mParentalControlsEnabled = enabled;
    105             mParentalControlsEnabledChanged = true;
    106             postSave();
    107         }
    108     }
    109 
    110     public boolean isRatingBlocked(TvContentRating rating) {
    111         loadIfNeeded();
    112         synchronized (mBlockedRatings) {
    113             for (TvContentRating blcokedRating : mBlockedRatings) {
    114                 if (rating.contains(blcokedRating)) {
    115                     return true;
    116                 }
    117             }
    118         }
    119         return false;
    120     }
    121 
    122     public TvContentRating[] getBlockedRatings() {
    123         loadIfNeeded();
    124         return mBlockedRatings.toArray(new TvContentRating[mBlockedRatings.size()]);
    125     }
    126 
    127     public void addBlockedRating(TvContentRating rating) {
    128         loadIfNeeded();
    129         if (rating != null && !mBlockedRatings.contains(rating)) {
    130             mBlockedRatings.add(rating);
    131             mBlockedRatingsChanged = true;
    132             postSave();
    133         }
    134     }
    135 
    136     public void removeBlockedRating(TvContentRating rating) {
    137         loadIfNeeded();
    138         if (rating != null && mBlockedRatings.contains(rating)) {
    139             mBlockedRatings.remove(rating);
    140             mBlockedRatingsChanged = true;
    141             postSave();
    142         }
    143     }
    144 
    145     private void loadIfNeeded() {
    146         if (!mLoaded) {
    147             load();
    148             mLoaded = true;
    149         }
    150     }
    151 
    152     private void clearState() {
    153         mBlockedRatings.clear();
    154         mParentalControlsEnabled = false;
    155     }
    156 
    157     private void load() {
    158         clearState();
    159 
    160         final InputStream is;
    161         try {
    162             is = mAtomicFile.openRead();
    163         } catch (FileNotFoundException ex) {
    164             return;
    165         }
    166 
    167         XmlPullParser parser;
    168         try {
    169             parser = Xml.newPullParser();
    170             parser.setInput(new BufferedInputStream(is), null);
    171             loadFromXml(parser);
    172         } catch (IOException | XmlPullParserException ex) {
    173             Slog.w(TAG, "Failed to load tv input manager persistent store data.", ex);
    174             clearState();
    175         } finally {
    176             IoUtils.closeQuietly(is);
    177         }
    178     }
    179 
    180     private void postSave() {
    181         mHandler.removeCallbacks(mSaveRunnable);
    182         mHandler.post(mSaveRunnable);
    183     }
    184 
    185     /**
    186      * Runnable posted when the state needs to be saved. This is used to prevent unnecessary file
    187      * operations when multiple settings change in rapid succession.
    188      */
    189     private final Runnable mSaveRunnable = new Runnable() {
    190         @Override
    191         public void run() {
    192             save();
    193         }
    194     };
    195 
    196     private void save() {
    197         final FileOutputStream os;
    198         try {
    199             os = mAtomicFile.startWrite();
    200             boolean success = false;
    201             try {
    202                 XmlSerializer serializer = new FastXmlSerializer();
    203                 serializer.setOutput(new BufferedOutputStream(os), "utf-8");
    204                 saveToXml(serializer);
    205                 serializer.flush();
    206                 success = true;
    207             } finally {
    208                 if (success) {
    209                     mAtomicFile.finishWrite(os);
    210                     broadcastChangesIfNeeded();
    211                 } else {
    212                     mAtomicFile.failWrite(os);
    213                 }
    214             }
    215         } catch (IOException ex) {
    216             Slog.w(TAG, "Failed to save tv input manager persistent store data.", ex);
    217         }
    218     }
    219 
    220     private void broadcastChangesIfNeeded() {
    221         if (mParentalControlsEnabledChanged) {
    222             mParentalControlsEnabledChanged = false;
    223             mContext.sendBroadcastAsUser(new Intent(
    224                     TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED), UserHandle.ALL);
    225         }
    226         if (mBlockedRatingsChanged) {
    227             mBlockedRatingsChanged = false;
    228             mContext.sendBroadcastAsUser(new Intent(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED),
    229                     UserHandle.ALL);
    230         }
    231     }
    232 
    233     private static final String TAG_TV_INPUT_MANAGER_STATE = "tv-input-manager-state";
    234     private static final String TAG_BLOCKED_RATINGS = "blocked-ratings";
    235     private static final String TAG_RATING = "rating";
    236     private static final String TAG_PARENTAL_CONTROLS = "parental-controls";
    237     private static final String ATTR_STRING = "string";
    238     private static final String ATTR_ENABLED = "enabled";
    239 
    240     private void loadFromXml(XmlPullParser parser)
    241             throws IOException, XmlPullParserException {
    242         XmlUtils.beginDocument(parser, TAG_TV_INPUT_MANAGER_STATE);
    243         final int outerDepth = parser.getDepth();
    244         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
    245             if (parser.getName().equals(TAG_BLOCKED_RATINGS)) {
    246                 loadBlockedRatingsFromXml(parser);
    247             } else if (parser.getName().equals(TAG_PARENTAL_CONTROLS)) {
    248                 String enabled = parser.getAttributeValue(null, ATTR_ENABLED);
    249                 if (TextUtils.isEmpty(enabled)) {
    250                     throw new XmlPullParserException(
    251                             "Missing " + ATTR_ENABLED + " attribute on " + TAG_PARENTAL_CONTROLS);
    252                 }
    253                 mParentalControlsEnabled = Boolean.valueOf(enabled);
    254             }
    255         }
    256     }
    257 
    258     private void loadBlockedRatingsFromXml(XmlPullParser parser)
    259             throws IOException, XmlPullParserException {
    260         final int outerDepth = parser.getDepth();
    261         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
    262             if (parser.getName().equals(TAG_RATING)) {
    263                 String ratingString = parser.getAttributeValue(null, ATTR_STRING);
    264                 if (TextUtils.isEmpty(ratingString)) {
    265                     throw new XmlPullParserException(
    266                             "Missing " + ATTR_STRING + " attribute on " + TAG_RATING);
    267                 }
    268                 mBlockedRatings.add(TvContentRating.unflattenFromString(ratingString));
    269             }
    270         }
    271     }
    272 
    273     private void saveToXml(XmlSerializer serializer) throws IOException {
    274         serializer.startDocument(null, true);
    275         serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
    276         serializer.startTag(null, TAG_TV_INPUT_MANAGER_STATE);
    277         serializer.startTag(null, TAG_BLOCKED_RATINGS);
    278         synchronized (mBlockedRatings) {
    279             for (TvContentRating rating : mBlockedRatings) {
    280                 serializer.startTag(null, TAG_RATING);
    281                 serializer.attribute(null, ATTR_STRING, rating.flattenToString());
    282                 serializer.endTag(null, TAG_RATING);
    283             }
    284         }
    285         serializer.endTag(null, TAG_BLOCKED_RATINGS);
    286         serializer.startTag(null, TAG_PARENTAL_CONTROLS);
    287         serializer.attribute(null, ATTR_ENABLED, Boolean.toString(mParentalControlsEnabled));
    288         serializer.endTag(null, TAG_PARENTAL_CONTROLS);
    289         serializer.endTag(null, TAG_TV_INPUT_MANAGER_STATE);
    290         serializer.endDocument();
    291     }
    292 }
    293