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