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