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