| /* |
| * 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.nio.charset.StandardCharsets; |
| 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: |
| * <code> |
| * <tv-input-manager-state> |
| * <blocked-ratings> |
| * <rating string="XXXX" /> |
| * </blocked-ratings> |
| * <parental-control enabled="YYYY" /> |
| * </tv-input-manager-state> |
| * </code> |
| */ |
| 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<TvContentRating> mBlockedRatings = |
| Collections.synchronizedList(new ArrayList<TvContentRating>()); |
| |
| 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"), "tv-input-state"); |
| } |
| |
| 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 blockedRating : mBlockedRatings) { |
| if (rating.contains(blockedRating)) { |
| 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), StandardCharsets.UTF_8.name()); |
| 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), StandardCharsets.UTF_8.name()); |
| 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.parseBoolean(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(); |
| } |
| } |