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