1 /* 2 * Copyright (C) 2015 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 17 package com.android.tv.dvr; 18 19 import android.annotation.TargetApi; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.Context; 23 import android.database.ContentObserver; 24 import android.database.Cursor; 25 import android.media.tv.TvContract; 26 import android.net.Uri; 27 import android.os.AsyncTask; 28 import android.os.Build; 29 import android.os.Handler; 30 import android.os.Looper; 31 import android.support.annotation.MainThread; 32 import android.support.annotation.Nullable; 33 import android.support.annotation.VisibleForTesting; 34 import android.util.ArraySet; 35 import android.util.Log; 36 import android.util.Range; 37 38 import com.android.tv.common.SoftPreconditions; 39 import com.android.tv.common.recording.RecordedProgram; 40 import com.android.tv.dvr.ScheduledRecording.RecordingState; 41 import com.android.tv.dvr.provider.AsyncDvrDbTask; 42 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryTask; 43 import com.android.tv.util.AsyncDbTask; 44 import com.android.tv.util.Clock; 45 46 import java.util.ArrayList; 47 import java.util.Collections; 48 import java.util.HashMap; 49 import java.util.Iterator; 50 import java.util.List; 51 import java.util.Set; 52 53 /** 54 * DVR Data manager to handle recordings and schedules. 55 */ 56 @MainThread 57 @TargetApi(Build.VERSION_CODES.N) 58 public class DvrDataManagerImpl extends BaseDvrDataManager { 59 private static final String TAG = "DvrDataManagerImpl"; 60 private static final boolean DEBUG = false; 61 62 private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>(); 63 private final HashMap<Long, ScheduledRecording> mProgramId2ScheduledRecordings = 64 new HashMap<>(); 65 private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>(); 66 67 private final Context mContext; 68 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 69 private final ContentObserver mContentObserver = new ContentObserver(mMainThreadHandler) { 70 71 @Override 72 public void onChange(boolean selfChange) { 73 onChange(selfChange, null); 74 } 75 76 @Override 77 public void onChange(boolean selfChange, @Nullable final Uri uri) { 78 if (uri == null) { 79 // TODO reload everything. 80 } 81 AsyncRecordedProgramQueryTask task = new AsyncRecordedProgramQueryTask( 82 mContext.getContentResolver(), uri); 83 task.executeOnDbThread(); 84 mPendingTasks.add(task); 85 } 86 }; 87 88 private void onObservedChange(Uri uri, RecordedProgram recordedProgram) { 89 long id = ContentUris.parseId(uri); 90 if (DEBUG) { 91 Log.d(TAG, "changed recorded program #" + id + " to " + recordedProgram); 92 } 93 if (recordedProgram == null) { 94 RecordedProgram old = mRecordedPrograms.remove(id); 95 if (old != null) { 96 notifyRecordedProgramRemoved(old); 97 } else { 98 Log.w(TAG, "Could not find old version of deleted program #" + id); 99 } 100 } else { 101 RecordedProgram old = mRecordedPrograms.put(id, recordedProgram); 102 if (old == null) { 103 notifyRecordedProgramAdded(recordedProgram); 104 } else { 105 notifyRecordedProgramChanged(recordedProgram); 106 } 107 } 108 } 109 110 private boolean mDvrLoadFinished; 111 private boolean mRecordedProgramLoadFinished; 112 private final Set<AsyncTask> mPendingTasks = new ArraySet<>(); 113 114 public DvrDataManagerImpl(Context context, Clock clock) { 115 super(context, clock); 116 mContext = context; 117 } 118 119 public void start() { 120 AsyncDvrQueryTask mDvrQueryTask = new AsyncDvrQueryTask(mContext) { 121 122 @Override 123 protected void onCancelled(List<ScheduledRecording> scheduledRecordings) { 124 mPendingTasks.remove(this); 125 } 126 127 @Override 128 protected void onPostExecute(List<ScheduledRecording> result) { 129 mPendingTasks.remove(this); 130 mDvrLoadFinished = true; 131 for (ScheduledRecording r : result) { 132 mScheduledRecordings.put(r.getId(), r); 133 } 134 } 135 }; 136 mDvrQueryTask.executeOnDbThread(); 137 mPendingTasks.add(mDvrQueryTask); 138 AsyncRecordedProgramsQueryTask mRecordedProgramQueryTask = 139 new AsyncRecordedProgramsQueryTask(mContext.getContentResolver()); 140 mRecordedProgramQueryTask.executeOnDbThread(); 141 ContentResolver cr = mContext.getContentResolver(); 142 cr.registerContentObserver(TvContract.RecordedPrograms.CONTENT_URI, true, mContentObserver); 143 } 144 145 public void stop() { 146 ContentResolver cr = mContext.getContentResolver(); 147 cr.unregisterContentObserver(mContentObserver); 148 Iterator<AsyncTask> i = mPendingTasks.iterator(); 149 while (i.hasNext()) { 150 AsyncTask task = i.next(); 151 i.remove(); 152 task.cancel(true); 153 } 154 } 155 156 @Override 157 public boolean isInitialized() { 158 return mDvrLoadFinished && mRecordedProgramLoadFinished; 159 } 160 161 private List<ScheduledRecording> getScheduledRecordingsPrograms() { 162 if (!mDvrLoadFinished) { 163 return Collections.emptyList(); 164 } 165 ArrayList<ScheduledRecording> list = new ArrayList<>(mScheduledRecordings.size()); 166 list.addAll(mScheduledRecordings.values()); 167 Collections.sort(list, ScheduledRecording.START_TIME_COMPARATOR); 168 return list; 169 } 170 171 @Override 172 public List<RecordedProgram> getRecordedPrograms() { 173 if (!mRecordedProgramLoadFinished) { 174 return Collections.emptyList(); 175 } 176 return new ArrayList<>(mRecordedPrograms.values()); 177 } 178 179 @Override 180 public List<ScheduledRecording> getAllScheduledRecordings() { 181 return new ArrayList<>(mScheduledRecordings.values()); 182 } 183 184 protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int state) { 185 List<ScheduledRecording> result = new ArrayList<>(); 186 for (ScheduledRecording r : mScheduledRecordings.values()) { 187 if (r.getState() == state) { 188 result.add(r); 189 } 190 } 191 return result; 192 } 193 194 @Override 195 public List<SeasonRecording> getSeasonRecordings() { 196 // If we return dummy data here, we can implement UI part independently. 197 return Collections.emptyList(); 198 } 199 200 @Override 201 public long getNextScheduledStartTimeAfter(long startTime) { 202 return getNextStartTimeAfter(getScheduledRecordingsPrograms(), startTime); 203 } 204 205 @VisibleForTesting 206 static long getNextStartTimeAfter(List<ScheduledRecording> scheduledRecordings, long startTime) { 207 int start = 0; 208 int end = scheduledRecordings.size() - 1; 209 while (start <= end) { 210 int mid = (start + end) / 2; 211 if (scheduledRecordings.get(mid).getStartTimeMs() <= startTime) { 212 start = mid + 1; 213 } else { 214 end = mid - 1; 215 } 216 } 217 return start < scheduledRecordings.size() ? scheduledRecordings.get(start).getStartTimeMs() 218 : NEXT_START_TIME_NOT_FOUND; 219 } 220 221 @Override 222 public List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period) { 223 List<ScheduledRecording> result = new ArrayList<>(); 224 for (ScheduledRecording r : mScheduledRecordings.values()) { 225 if (r.isOverLapping(period)) { 226 result.add(r); 227 } 228 } 229 return result; 230 } 231 232 @Nullable 233 @Override 234 public ScheduledRecording getScheduledRecording(long recordingId) { 235 if (mDvrLoadFinished) { 236 return mScheduledRecordings.get(recordingId); 237 } 238 return null; 239 } 240 241 @Nullable 242 @Override 243 public ScheduledRecording getScheduledRecordingForProgramId(long programId) { 244 if (mDvrLoadFinished) { 245 return mProgramId2ScheduledRecordings.get(programId); 246 } 247 return null; 248 } 249 250 @Nullable 251 @Override 252 public RecordedProgram getRecordedProgram(long recordingId) { 253 return mRecordedPrograms.get(recordingId); 254 } 255 256 @Override 257 public void addScheduledRecording(final ScheduledRecording scheduledRecording) { 258 new AsyncDvrDbTask.AsyncAddRecordingTask(mContext) { 259 @Override 260 protected void onPostExecute(List<ScheduledRecording> scheduledRecordings) { 261 super.onPostExecute(scheduledRecordings); 262 SoftPreconditions.checkArgument(scheduledRecordings.size() == 1); 263 for (ScheduledRecording r : scheduledRecordings) { 264 if (r.getId() != -1) { 265 mScheduledRecordings.put(r.getId(), r); 266 if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { 267 mProgramId2ScheduledRecordings.put(r.getProgramId(), r); 268 } 269 notifyScheduledRecordingAdded(r); 270 } else { 271 Log.w(TAG, "Error adding " + r); 272 } 273 } 274 275 } 276 }.executeOnDbThread(scheduledRecording); 277 } 278 279 @Override 280 public void addSeasonRecording(SeasonRecording seasonRecording) { } 281 282 @Override 283 public void removeScheduledRecording(final ScheduledRecording scheduledRecording) { 284 new AsyncDvrDbTask.AsyncDeleteRecordingTask(mContext) { 285 @Override 286 protected void onPostExecute(List<Integer> counts) { 287 super.onPostExecute(counts); 288 SoftPreconditions.checkArgument(counts.size() == 1); 289 for (Integer c : counts) { 290 if (c == 1) { 291 mScheduledRecordings.remove(scheduledRecording.getId()); 292 if (scheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) { 293 mProgramId2ScheduledRecordings 294 .remove(scheduledRecording.getProgramId()); 295 } 296 //TODO change to notifyRecordingUpdated 297 notifyScheduledRecordingRemoved(scheduledRecording); 298 } else { 299 Log.w(TAG, "Error removing " + scheduledRecording); 300 } 301 } 302 303 } 304 }.executeOnDbThread(scheduledRecording); 305 } 306 307 @Override 308 public void removeSeasonSchedule(SeasonRecording seasonSchedule) { } 309 310 @Override 311 public void updateScheduledRecording(final ScheduledRecording scheduledRecording) { 312 new AsyncDvrDbTask.AsyncUpdateRecordingTask(mContext) { 313 @Override 314 protected void onPostExecute(List<Integer> counts) { 315 super.onPostExecute(counts); 316 SoftPreconditions.checkArgument(counts.size() == 1); 317 for (Integer c : counts) { 318 if (c == 1) { 319 ScheduledRecording oldScheduledRecording = mScheduledRecordings 320 .put(scheduledRecording.getId(), scheduledRecording); 321 long programId = scheduledRecording.getProgramId(); 322 if (oldScheduledRecording != null 323 && oldScheduledRecording.getProgramId() != programId 324 && oldScheduledRecording.getProgramId() 325 != ScheduledRecording.ID_NOT_SET) { 326 ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings 327 .get(oldScheduledRecording.getProgramId()); 328 if (oldValueForProgramId.getId() == scheduledRecording.getId()) { 329 //Only remove the old ScheduledRecording if it has the same ID as 330 // the new one. 331 mProgramId2ScheduledRecordings 332 .remove(oldScheduledRecording.getProgramId()); 333 } 334 } 335 if (programId != ScheduledRecording.ID_NOT_SET) { 336 mProgramId2ScheduledRecordings.put(programId, scheduledRecording); 337 } 338 //TODO change to notifyRecordingUpdated 339 notifyScheduledRecordingStatusChanged(scheduledRecording); 340 } else { 341 Log.w(TAG, "Error updating " + scheduledRecording); 342 } 343 } 344 } 345 }.executeOnDbThread(scheduledRecording); 346 } 347 348 private final class AsyncRecordedProgramsQueryTask 349 extends AsyncDbTask.AsyncQueryListTask<RecordedProgram> { 350 public AsyncRecordedProgramsQueryTask(ContentResolver contentResolver) { 351 super(contentResolver, TvContract.RecordedPrograms.CONTENT_URI, 352 RecordedProgram.PROJECTION, null, null, null); 353 } 354 355 @Override 356 protected RecordedProgram fromCursor(Cursor c) { 357 return RecordedProgram.fromCursor(c); 358 } 359 360 @Override 361 protected void onCancelled(List<RecordedProgram> scheduledRecordings) { 362 mPendingTasks.remove(this); 363 } 364 365 @Override 366 protected void onPostExecute(List<RecordedProgram> result) { 367 mPendingTasks.remove(this); 368 mRecordedProgramLoadFinished = true; 369 if (result != null) { 370 for (RecordedProgram r : result) { 371 mRecordedPrograms.put(r.getId(), r); 372 } 373 } 374 } 375 } 376 377 private final class AsyncRecordedProgramQueryTask 378 extends AsyncDbTask.AsyncQueryItemTask<RecordedProgram> { 379 380 private final Uri mUri; 381 382 public AsyncRecordedProgramQueryTask(ContentResolver contentResolver, Uri uri) { 383 super(contentResolver, uri, RecordedProgram.PROJECTION, null, null, null); 384 mUri = uri; 385 } 386 387 @Override 388 protected RecordedProgram fromCursor(Cursor c) { 389 return RecordedProgram.fromCursor(c); 390 } 391 392 @Override 393 protected void onCancelled(RecordedProgram recordedProgram) { 394 mPendingTasks.remove(this); 395 } 396 397 @Override 398 protected void onPostExecute(RecordedProgram recordedProgram) { 399 mPendingTasks.remove(this); 400 onObservedChange(mUri, recordedProgram); 401 } 402 } 403 } 404