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.nio.charset.StandardCharsets;
48import java.util.ArrayList;
49import java.util.Collections;
50import 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
74    private final List<TvContentRating> mBlockedRatings =
75            Collections.synchronizedList(new ArrayList<TvContentRating>());
76
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;
88        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"));
95    }
96
97    public boolean isParentalControlsEnabled() {
98        loadIfNeeded();
99        return mParentalControlsEnabled;
100    }
101
102    public void setParentalControlsEnabled(boolean enabled) {
103        loadIfNeeded();
104        if (mParentalControlsEnabled != enabled) {
105            mParentalControlsEnabled = enabled;
106            mParentalControlsEnabledChanged = true;
107            postSave();
108        }
109    }
110
111    public boolean isRatingBlocked(TvContentRating rating) {
112        loadIfNeeded();
113        synchronized (mBlockedRatings) {
114            for (TvContentRating blockedRating : mBlockedRatings) {
115                if (rating.contains(blockedRating)) {
116                    return true;
117                }
118            }
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) {
129        loadIfNeeded();
130        if (rating != null && !mBlockedRatings.contains(rating)) {
131            mBlockedRatings.add(rating);
132            mBlockedRatingsChanged = true;
133            postSave();
134        }
135    }
136
137    public void removeBlockedRating(TvContentRating rating) {
138        loadIfNeeded();
139        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();
171            parser.setInput(new BufferedInputStream(is), StandardCharsets.UTF_8.name());
172            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();
204                serializer.setOutput(new BufferedOutputStream(os), StandardCharsets.UTF_8.name());
205                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                }
254                mParentalControlsEnabled = Boolean.valueOf(enabled);
255            }
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);
279        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            }
285        }
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}
294