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