PersistentDataStore.java revision 7ed8901f120b21b4476c37b47791e25d659b21a5
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        File tvDir = new File("/data/system/tv/" + userId);
85        tvDir.mkdirs();
86        mAtomicFile = new AtomicFile(new File(tvDir, "tv-input-manager-state.xml"));
87    }
88
89    public boolean isParentalControlsEnabled() {
90        loadIfNeeded();
91        return mParentalControlsEnabled;
92    }
93
94    public void setParentalControlsEnabled(boolean enabled) {
95        loadIfNeeded();
96        if (mParentalControlsEnabled != enabled) {
97            mParentalControlsEnabled = enabled;
98            mParentalControlsEnabledChanged = true;
99            postSave();
100        }
101    }
102
103    public boolean isRatingBlocked(TvContentRating rating) {
104        loadIfNeeded();
105        for (TvContentRating blcokedRating : mBlockedRatings) {
106            if (rating.contains(blcokedRating)) {
107                return true;
108            }
109        }
110        return false;
111    }
112
113    public TvContentRating[] getBlockedRatings() {
114        loadIfNeeded();
115        return mBlockedRatings.toArray(new TvContentRating[mBlockedRatings.size()]);
116    }
117
118    public void addBlockedRating(TvContentRating rating) {
119        loadIfNeeded();
120        if (rating != null && !mBlockedRatings.contains(rating)) {
121            mBlockedRatings.add(rating);
122            mBlockedRatingsChanged = true;
123            postSave();
124        }
125    }
126
127    public void removeBlockedRating(TvContentRating rating) {
128        loadIfNeeded();
129        if (rating != null && mBlockedRatings.contains(rating)) {
130            mBlockedRatings.remove(rating);
131            mBlockedRatingsChanged = true;
132            postSave();
133        }
134    }
135
136    private void loadIfNeeded() {
137        if (!mLoaded) {
138            load();
139            mLoaded = true;
140        }
141    }
142
143    private void clearState() {
144        mBlockedRatings.clear();
145        mParentalControlsEnabled = false;
146    }
147
148    private void load() {
149        clearState();
150
151        final InputStream is;
152        try {
153            is = mAtomicFile.openRead();
154        } catch (FileNotFoundException ex) {
155            return;
156        }
157
158        XmlPullParser parser;
159        try {
160            parser = Xml.newPullParser();
161            parser.setInput(new BufferedInputStream(is), null);
162            loadFromXml(parser);
163        } catch (IOException | XmlPullParserException ex) {
164            Slog.w(TAG, "Failed to load tv input manager persistent store data.", ex);
165            clearState();
166        } finally {
167            IoUtils.closeQuietly(is);
168        }
169    }
170
171    private void postSave() {
172        mHandler.removeCallbacks(mSaveRunnable);
173        mHandler.post(mSaveRunnable);
174    }
175
176    /**
177     * Runnable posted when the state needs to be saved. This is used to prevent unnecessary file
178     * operations when multiple settings change in rapid succession.
179     */
180    private final Runnable mSaveRunnable = new Runnable() {
181        @Override
182        public void run() {
183            save();
184        }
185    };
186
187    private void save() {
188        final FileOutputStream os;
189        try {
190            os = mAtomicFile.startWrite();
191            boolean success = false;
192            try {
193                XmlSerializer serializer = new FastXmlSerializer();
194                serializer.setOutput(new BufferedOutputStream(os), "utf-8");
195                saveToXml(serializer);
196                serializer.flush();
197                success = true;
198            } finally {
199                if (success) {
200                    mAtomicFile.finishWrite(os);
201                    broadcastChangesIfNeeded();
202                } else {
203                    mAtomicFile.failWrite(os);
204                }
205            }
206        } catch (IOException ex) {
207            Slog.w(TAG, "Failed to save tv input manager persistent store data.", ex);
208        }
209    }
210
211    private void broadcastChangesIfNeeded() {
212        if (mParentalControlsEnabledChanged) {
213            mParentalControlsEnabledChanged = false;
214            mContext.sendBroadcastAsUser(new Intent(
215                    TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED), UserHandle.ALL);
216        }
217        if (mBlockedRatingsChanged) {
218            mBlockedRatingsChanged = false;
219            mContext.sendBroadcastAsUser(new Intent(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED),
220                    UserHandle.ALL);
221        }
222    }
223
224    private static final String TAG_TV_INPUT_MANAGER_STATE = "tv-input-manager-state";
225    private static final String TAG_BLOCKED_RATINGS = "blocked-ratings";
226    private static final String TAG_RATING = "rating";
227    private static final String TAG_PARENTAL_CONTROLS = "parental-controls";
228    private static final String ATTR_STRING = "string";
229    private static final String ATTR_ENABLED = "enabled";
230
231    private void loadFromXml(XmlPullParser parser)
232            throws IOException, XmlPullParserException {
233        XmlUtils.beginDocument(parser, TAG_TV_INPUT_MANAGER_STATE);
234        final int outerDepth = parser.getDepth();
235        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
236            if (parser.getName().equals(TAG_BLOCKED_RATINGS)) {
237                loadBlockedRatingsFromXml(parser);
238            } else if (parser.getName().equals(TAG_PARENTAL_CONTROLS)) {
239                String enabled = parser.getAttributeValue(null, ATTR_ENABLED);
240                if (TextUtils.isEmpty(enabled)) {
241                    throw new XmlPullParserException(
242                            "Missing " + ATTR_ENABLED + " attribute on " + TAG_PARENTAL_CONTROLS);
243                }
244                mParentalControlsEnabled = Boolean.valueOf(enabled);
245            }
246        }
247    }
248
249    private void loadBlockedRatingsFromXml(XmlPullParser parser)
250            throws IOException, XmlPullParserException {
251        final int outerDepth = parser.getDepth();
252        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
253            if (parser.getName().equals(TAG_RATING)) {
254                String ratingString = parser.getAttributeValue(null, ATTR_STRING);
255                if (TextUtils.isEmpty(ratingString)) {
256                    throw new XmlPullParserException(
257                            "Missing " + ATTR_STRING + " attribute on " + TAG_RATING);
258                }
259                mBlockedRatings.add(TvContentRating.unflattenFromString(ratingString));
260            }
261        }
262    }
263
264    private void saveToXml(XmlSerializer serializer) throws IOException {
265        serializer.startDocument(null, true);
266        serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
267        serializer.startTag(null, TAG_TV_INPUT_MANAGER_STATE);
268        serializer.startTag(null, TAG_BLOCKED_RATINGS);
269        for (TvContentRating rating : mBlockedRatings) {
270            serializer.startTag(null, TAG_RATING);
271            serializer.attribute(null, ATTR_STRING, rating.flattenToString());
272            serializer.endTag(null, TAG_RATING);
273        }
274        serializer.endTag(null, TAG_BLOCKED_RATINGS);
275        serializer.startTag(null, TAG_PARENTAL_CONTROLS);
276        serializer.attribute(null, ATTR_ENABLED, Boolean.toString(mParentalControlsEnabled));
277        serializer.endTag(null, TAG_PARENTAL_CONTROLS);
278        serializer.endTag(null, TAG_TV_INPUT_MANAGER_STATE);
279        serializer.endDocument();
280    }
281}
282