/* * 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 com.android.server.tv; import android.content.Context; import android.content.Intent; import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; import android.os.Environment; import android.os.Handler; import android.os.UserHandle; import android.text.TextUtils; import android.util.AtomicFile; import android.util.Slog; import android.util.Xml; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.XmlUtils; import libcore.io.IoUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Manages persistent state recorded by the TV input manager service as an XML file. This class is * not thread-safe thus caller must acquire lock on the data store before accessing it. File format: * * <tv-input-manager-state> * <blocked-ratings> * <rating string="XXXX" /> * </blocked-ratings> * <parental-control enabled="YYYY" /> * </tv-input-manager-state> * */ final class PersistentDataStore { private static final String TAG = "TvInputManagerService"; private final Context mContext; private final Handler mHandler = new Handler(); // The atomic file used to safely read or write the file. private final AtomicFile mAtomicFile; private final List mBlockedRatings = Collections.synchronizedList(new ArrayList()); private boolean mBlockedRatingsChanged; private boolean mParentalControlsEnabled; private boolean mParentalControlsEnabledChanged; // True if the data has been loaded. private boolean mLoaded; public PersistentDataStore(Context context, int userId) { mContext = context; File userDir = Environment.getUserSystemDirectory(userId); if (!userDir.exists()) { if (!userDir.mkdirs()) { throw new IllegalStateException("User dir cannot be created: " + userDir); } } mAtomicFile = new AtomicFile(new File(userDir, "tv-input-manager-state.xml")); } public boolean isParentalControlsEnabled() { loadIfNeeded(); return mParentalControlsEnabled; } public void setParentalControlsEnabled(boolean enabled) { loadIfNeeded(); if (mParentalControlsEnabled != enabled) { mParentalControlsEnabled = enabled; mParentalControlsEnabledChanged = true; postSave(); } } public boolean isRatingBlocked(TvContentRating rating) { loadIfNeeded(); synchronized (mBlockedRatings) { for (TvContentRating blcokedRating : mBlockedRatings) { if (rating.contains(blcokedRating)) { return true; } } } return false; } public TvContentRating[] getBlockedRatings() { loadIfNeeded(); return mBlockedRatings.toArray(new TvContentRating[mBlockedRatings.size()]); } public void addBlockedRating(TvContentRating rating) { loadIfNeeded(); if (rating != null && !mBlockedRatings.contains(rating)) { mBlockedRatings.add(rating); mBlockedRatingsChanged = true; postSave(); } } public void removeBlockedRating(TvContentRating rating) { loadIfNeeded(); if (rating != null && mBlockedRatings.contains(rating)) { mBlockedRatings.remove(rating); mBlockedRatingsChanged = true; postSave(); } } private void loadIfNeeded() { if (!mLoaded) { load(); mLoaded = true; } } private void clearState() { mBlockedRatings.clear(); mParentalControlsEnabled = false; } private void load() { clearState(); final InputStream is; try { is = mAtomicFile.openRead(); } catch (FileNotFoundException ex) { return; } XmlPullParser parser; try { parser = Xml.newPullParser(); parser.setInput(new BufferedInputStream(is), null); loadFromXml(parser); } catch (IOException | XmlPullParserException ex) { Slog.w(TAG, "Failed to load tv input manager persistent store data.", ex); clearState(); } finally { IoUtils.closeQuietly(is); } } private void postSave() { mHandler.removeCallbacks(mSaveRunnable); mHandler.post(mSaveRunnable); } /** * Runnable posted when the state needs to be saved. This is used to prevent unnecessary file * operations when multiple settings change in rapid succession. */ private final Runnable mSaveRunnable = new Runnable() { @Override public void run() { save(); } }; private void save() { final FileOutputStream os; try { os = mAtomicFile.startWrite(); boolean success = false; try { XmlSerializer serializer = new FastXmlSerializer(); serializer.setOutput(new BufferedOutputStream(os), "utf-8"); saveToXml(serializer); serializer.flush(); success = true; } finally { if (success) { mAtomicFile.finishWrite(os); broadcastChangesIfNeeded(); } else { mAtomicFile.failWrite(os); } } } catch (IOException ex) { Slog.w(TAG, "Failed to save tv input manager persistent store data.", ex); } } private void broadcastChangesIfNeeded() { if (mParentalControlsEnabledChanged) { mParentalControlsEnabledChanged = false; mContext.sendBroadcastAsUser(new Intent( TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED), UserHandle.ALL); } if (mBlockedRatingsChanged) { mBlockedRatingsChanged = false; mContext.sendBroadcastAsUser(new Intent(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED), UserHandle.ALL); } } private static final String TAG_TV_INPUT_MANAGER_STATE = "tv-input-manager-state"; private static final String TAG_BLOCKED_RATINGS = "blocked-ratings"; private static final String TAG_RATING = "rating"; private static final String TAG_PARENTAL_CONTROLS = "parental-controls"; private static final String ATTR_STRING = "string"; private static final String ATTR_ENABLED = "enabled"; private void loadFromXml(XmlPullParser parser) throws IOException, XmlPullParserException { XmlUtils.beginDocument(parser, TAG_TV_INPUT_MANAGER_STATE); final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (parser.getName().equals(TAG_BLOCKED_RATINGS)) { loadBlockedRatingsFromXml(parser); } else if (parser.getName().equals(TAG_PARENTAL_CONTROLS)) { String enabled = parser.getAttributeValue(null, ATTR_ENABLED); if (TextUtils.isEmpty(enabled)) { throw new XmlPullParserException( "Missing " + ATTR_ENABLED + " attribute on " + TAG_PARENTAL_CONTROLS); } mParentalControlsEnabled = Boolean.valueOf(enabled); } } } private void loadBlockedRatingsFromXml(XmlPullParser parser) throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (parser.getName().equals(TAG_RATING)) { String ratingString = parser.getAttributeValue(null, ATTR_STRING); if (TextUtils.isEmpty(ratingString)) { throw new XmlPullParserException( "Missing " + ATTR_STRING + " attribute on " + TAG_RATING); } mBlockedRatings.add(TvContentRating.unflattenFromString(ratingString)); } } } private void saveToXml(XmlSerializer serializer) throws IOException { serializer.startDocument(null, true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startTag(null, TAG_TV_INPUT_MANAGER_STATE); serializer.startTag(null, TAG_BLOCKED_RATINGS); synchronized (mBlockedRatings) { for (TvContentRating rating : mBlockedRatings) { serializer.startTag(null, TAG_RATING); serializer.attribute(null, ATTR_STRING, rating.flattenToString()); serializer.endTag(null, TAG_RATING); } } serializer.endTag(null, TAG_BLOCKED_RATINGS); serializer.startTag(null, TAG_PARENTAL_CONTROLS); serializer.attribute(null, ATTR_ENABLED, Boolean.toString(mParentalControlsEnabled)); serializer.endTag(null, TAG_PARENTAL_CONTROLS); serializer.endTag(null, TAG_TV_INPUT_MANAGER_STATE); serializer.endDocument(); } }