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