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