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 * <tv-input-manager-state> 55 * <blocked-ratings> 56 * <rating string="XXXX" /> 57 * </blocked-ratings> 58 * <parental-control enabled="YYYY" /> 59 * </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