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