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.nio.charset.StandardCharsets;
     48 import java.util.ArrayList;
     49 import java.util.Collections;
     50 import java.util.List;
     51 
     52 /**
     53  * Manages persistent state recorded by the TV input manager service as an XML file. This class is
     54  * not thread-safe thus caller must acquire lock on the data store before accessing it. File format:
     55  * <code>
     56  * &lt;tv-input-manager-state>
     57  *   &lt;blocked-ratings>
     58  *     &lt;rating string="XXXX" />
     59  *   &lt;/blocked-ratings>
     60  *   &lt;parental-control enabled="YYYY" />
     61  * &lt;/tv-input-manager-state>
     62  * </code>
     63  */
     64 final class PersistentDataStore {
     65     private static final String TAG = "TvInputManagerService";
     66 
     67     private final Context mContext;
     68 
     69     private final Handler mHandler = new Handler();
     70 
     71     // The atomic file used to safely read or write the file.
     72     private final AtomicFile mAtomicFile;
     73 
     74     private final List<TvContentRating> mBlockedRatings =
     75             Collections.synchronizedList(new ArrayList<TvContentRating>());
     76 
     77     private boolean mBlockedRatingsChanged;
     78 
     79     private boolean mParentalControlsEnabled;
     80 
     81     private boolean mParentalControlsEnabledChanged;
     82 
     83     // True if the data has been loaded.
     84     private boolean mLoaded;
     85 
     86     public PersistentDataStore(Context context, int userId) {
     87         mContext = context;
     88         File userDir = Environment.getUserSystemDirectory(userId);
     89         if (!userDir.exists()) {
     90             if (!userDir.mkdirs()) {
     91                 throw new IllegalStateException("User dir cannot be created: " + userDir);
     92             }
     93         }
     94         mAtomicFile = new AtomicFile(new File(userDir, "tv-input-manager-state.xml"));
     95     }
     96 
     97     public boolean isParentalControlsEnabled() {
     98         loadIfNeeded();
     99         return mParentalControlsEnabled;
    100     }
    101 
    102     public void setParentalControlsEnabled(boolean enabled) {
    103         loadIfNeeded();
    104         if (mParentalControlsEnabled != enabled) {
    105             mParentalControlsEnabled = enabled;
    106             mParentalControlsEnabledChanged = true;
    107             postSave();
    108         }
    109     }
    110 
    111     public boolean isRatingBlocked(TvContentRating rating) {
    112         loadIfNeeded();
    113         synchronized (mBlockedRatings) {
    114             for (TvContentRating blockedRating : mBlockedRatings) {
    115                 if (rating.contains(blockedRating)) {
    116                     return true;
    117                 }
    118             }
    119         }
    120         return false;
    121     }
    122 
    123     public TvContentRating[] getBlockedRatings() {
    124         loadIfNeeded();
    125         return mBlockedRatings.toArray(new TvContentRating[mBlockedRatings.size()]);
    126     }
    127 
    128     public void addBlockedRating(TvContentRating rating) {
    129         loadIfNeeded();
    130         if (rating != null && !mBlockedRatings.contains(rating)) {
    131             mBlockedRatings.add(rating);
    132             mBlockedRatingsChanged = true;
    133             postSave();
    134         }
    135     }
    136 
    137     public void removeBlockedRating(TvContentRating rating) {
    138         loadIfNeeded();
    139         if (rating != null && mBlockedRatings.contains(rating)) {
    140             mBlockedRatings.remove(rating);
    141             mBlockedRatingsChanged = true;
    142             postSave();
    143         }
    144     }
    145 
    146     private void loadIfNeeded() {
    147         if (!mLoaded) {
    148             load();
    149             mLoaded = true;
    150         }
    151     }
    152 
    153     private void clearState() {
    154         mBlockedRatings.clear();
    155         mParentalControlsEnabled = false;
    156     }
    157 
    158     private void load() {
    159         clearState();
    160 
    161         final InputStream is;
    162         try {
    163             is = mAtomicFile.openRead();
    164         } catch (FileNotFoundException ex) {
    165             return;
    166         }
    167 
    168         XmlPullParser parser;
    169         try {
    170             parser = Xml.newPullParser();
    171             parser.setInput(new BufferedInputStream(is), StandardCharsets.UTF_8.name());
    172             loadFromXml(parser);
    173         } catch (IOException | XmlPullParserException ex) {
    174             Slog.w(TAG, "Failed to load tv input manager persistent store data.", ex);
    175             clearState();
    176         } finally {
    177             IoUtils.closeQuietly(is);
    178         }
    179     }
    180 
    181     private void postSave() {
    182         mHandler.removeCallbacks(mSaveRunnable);
    183         mHandler.post(mSaveRunnable);
    184     }
    185 
    186     /**
    187      * Runnable posted when the state needs to be saved. This is used to prevent unnecessary file
    188      * operations when multiple settings change in rapid succession.
    189      */
    190     private final Runnable mSaveRunnable = new Runnable() {
    191         @Override
    192         public void run() {
    193             save();
    194         }
    195     };
    196 
    197     private void save() {
    198         final FileOutputStream os;
    199         try {
    200             os = mAtomicFile.startWrite();
    201             boolean success = false;
    202             try {
    203                 XmlSerializer serializer = new FastXmlSerializer();
    204                 serializer.setOutput(new BufferedOutputStream(os), StandardCharsets.UTF_8.name());
    205                 saveToXml(serializer);
    206                 serializer.flush();
    207                 success = true;
    208             } finally {
    209                 if (success) {
    210                     mAtomicFile.finishWrite(os);
    211                     broadcastChangesIfNeeded();
    212                 } else {
    213                     mAtomicFile.failWrite(os);
    214                 }
    215             }
    216         } catch (IOException ex) {
    217             Slog.w(TAG, "Failed to save tv input manager persistent store data.", ex);
    218         }
    219     }
    220 
    221     private void broadcastChangesIfNeeded() {
    222         if (mParentalControlsEnabledChanged) {
    223             mParentalControlsEnabledChanged = false;
    224             mContext.sendBroadcastAsUser(new Intent(
    225                     TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED), UserHandle.ALL);
    226         }
    227         if (mBlockedRatingsChanged) {
    228             mBlockedRatingsChanged = false;
    229             mContext.sendBroadcastAsUser(new Intent(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED),
    230                     UserHandle.ALL);
    231         }
    232     }
    233 
    234     private static final String TAG_TV_INPUT_MANAGER_STATE = "tv-input-manager-state";
    235     private static final String TAG_BLOCKED_RATINGS = "blocked-ratings";
    236     private static final String TAG_RATING = "rating";
    237     private static final String TAG_PARENTAL_CONTROLS = "parental-controls";
    238     private static final String ATTR_STRING = "string";
    239     private static final String ATTR_ENABLED = "enabled";
    240 
    241     private void loadFromXml(XmlPullParser parser)
    242             throws IOException, XmlPullParserException {
    243         XmlUtils.beginDocument(parser, TAG_TV_INPUT_MANAGER_STATE);
    244         final int outerDepth = parser.getDepth();
    245         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
    246             if (parser.getName().equals(TAG_BLOCKED_RATINGS)) {
    247                 loadBlockedRatingsFromXml(parser);
    248             } else if (parser.getName().equals(TAG_PARENTAL_CONTROLS)) {
    249                 String enabled = parser.getAttributeValue(null, ATTR_ENABLED);
    250                 if (TextUtils.isEmpty(enabled)) {
    251                     throw new XmlPullParserException(
    252                             "Missing " + ATTR_ENABLED + " attribute on " + TAG_PARENTAL_CONTROLS);
    253                 }
    254                 mParentalControlsEnabled = Boolean.parseBoolean(enabled);
    255             }
    256         }
    257     }
    258 
    259     private void loadBlockedRatingsFromXml(XmlPullParser parser)
    260             throws IOException, XmlPullParserException {
    261         final int outerDepth = parser.getDepth();
    262         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
    263             if (parser.getName().equals(TAG_RATING)) {
    264                 String ratingString = parser.getAttributeValue(null, ATTR_STRING);
    265                 if (TextUtils.isEmpty(ratingString)) {
    266                     throw new XmlPullParserException(
    267                             "Missing " + ATTR_STRING + " attribute on " + TAG_RATING);
    268                 }
    269                 mBlockedRatings.add(TvContentRating.unflattenFromString(ratingString));
    270             }
    271         }
    272     }
    273 
    274     private void saveToXml(XmlSerializer serializer) throws IOException {
    275         serializer.startDocument(null, true);
    276         serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
    277         serializer.startTag(null, TAG_TV_INPUT_MANAGER_STATE);
    278         serializer.startTag(null, TAG_BLOCKED_RATINGS);
    279         synchronized (mBlockedRatings) {
    280             for (TvContentRating rating : mBlockedRatings) {
    281                 serializer.startTag(null, TAG_RATING);
    282                 serializer.attribute(null, ATTR_STRING, rating.flattenToString());
    283                 serializer.endTag(null, TAG_RATING);
    284             }
    285         }
    286         serializer.endTag(null, TAG_BLOCKED_RATINGS);
    287         serializer.startTag(null, TAG_PARENTAL_CONTROLS);
    288         serializer.attribute(null, ATTR_ENABLED, Boolean.toString(mParentalControlsEnabled));
    289         serializer.endTag(null, TAG_PARENTAL_CONTROLS);
    290         serializer.endTag(null, TAG_TV_INPUT_MANAGER_STATE);
    291         serializer.endDocument();
    292     }
    293 }
    294