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.providers.tv;
18
19import android.app.IntentService;
20import android.content.Intent;
21import android.database.Cursor;
22import android.media.tv.TvContract.Programs;
23import android.media.tv.TvContract.WatchedPrograms;
24import android.text.format.DateUtils;
25import android.util.Log;
26
27import com.android.internal.annotations.VisibleForTesting;
28
29import java.util.concurrent.TimeUnit;
30
31/**
32 * A service that cleans up EPG data.
33 */
34public class EpgDataCleanupService extends IntentService {
35    private static final boolean DEBUG = true;
36    private static final String TAG = "EpgDataCleanupService";
37
38    static final String ACTION_CLEAN_UP_EPG_DATA =
39            "com.android.providers.tv.intent.CLEAN_UP_EPG_DATA";
40
41    public EpgDataCleanupService() {
42        super("EpgDataCleanupService");
43    }
44
45    @Override
46    protected void onHandleIntent(Intent intent) {
47        if (DEBUG) {
48            Log.d(TAG, "Received intent: " + intent);
49        }
50        final String action = intent.getAction();
51        if (!ACTION_CLEAN_UP_EPG_DATA.equals(action)) {
52            return;
53        }
54
55        long nowMillis = System.currentTimeMillis();
56
57        int maxProgramAgeInDays = getResources().getInteger(R.integer.max_program_age_in_days);
58        if (maxProgramAgeInDays > 0) {
59            clearOldPrograms(nowMillis - TimeUnit.DAYS.toMillis(maxProgramAgeInDays));
60        }
61
62        int maxWatchedProgramAgeInDays =
63                getResources().getInteger(R.integer.max_watched_program_age_in_days);
64        if (maxWatchedProgramAgeInDays > 0) {
65            clearOldWatchHistory(nowMillis - TimeUnit.DAYS.toMillis(maxWatchedProgramAgeInDays));
66        }
67
68        int maxWatchedProgramEntryCount =
69                getResources().getInteger(R.integer.max_watched_program_entry_count);
70        if (maxWatchedProgramEntryCount > 0) {
71            clearOverflowWatchHistory(maxWatchedProgramEntryCount);
72        }
73    }
74
75    /**
76     * Clear program info that ended before {@code maxEndTimeMillis}.
77     */
78    @VisibleForTesting
79    void clearOldPrograms(long maxEndTimeMillis) {
80        int deleteCount = getContentResolver().delete(
81                Programs.CONTENT_URI,
82                Programs.COLUMN_END_TIME_UTC_MILLIS + "<?",
83                new String[] { String.valueOf(maxEndTimeMillis) });
84        if (DEBUG && deleteCount > 0) {
85            Log.d(TAG, "Deleted " + deleteCount + " programs"
86                  + " (reason: ended before "
87                  + DateUtils.getRelativeTimeSpanString(this, maxEndTimeMillis) + ")");
88        }
89    }
90
91    /**
92     * Clear watch history whose watch started before {@code maxStartTimeMillis}.
93     * In theory, history entry for currently watching program can be deleted
94     * (e.g., have been watching since before {@code maxStartTimeMillis}).
95     */
96    @VisibleForTesting
97    void clearOldWatchHistory(long maxStartTimeMillis) {
98        int deleteCount = getContentResolver().delete(
99                WatchedPrograms.CONTENT_URI,
100                WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + "<?",
101                new String[] { String.valueOf(maxStartTimeMillis) });
102        if (DEBUG && deleteCount > 0) {
103            Log.d(TAG, "Deleted " + deleteCount + " watched programs"
104                  + " (reason: started before "
105                  + DateUtils.getRelativeTimeSpanString(this, maxStartTimeMillis) + ")");
106        }
107    }
108
109    /**
110     * Clear watch history except last {@code maxEntryCount} entries.
111     * "Last" here is based on watch start time, and so, in theory, history entry for program
112     * that user was watching until recent reboot can be deleted earlier than other entries
113     * which ended before.
114     */
115    @VisibleForTesting
116    void clearOverflowWatchHistory(int maxEntryCount) {
117        Cursor cursor = getContentResolver().query(
118                WatchedPrograms.CONTENT_URI,
119                new String[] { WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS }, null, null,
120                WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
121        if (cursor == null) {
122            Log.e(TAG, "Failed to query watched program");
123            return;
124        }
125        int totalCount;
126        long maxStartTimeMillis;
127        try {
128            totalCount = cursor.getCount();
129            int overflowCount = totalCount - maxEntryCount;
130            if (overflowCount <= 0) {
131                return;
132            }
133            if (!cursor.moveToPosition(overflowCount - 1)) {
134                Log.e(TAG, "Failed to query watched program");
135                return;
136            }
137            maxStartTimeMillis = cursor.getLong(0);
138        } finally {
139            cursor.close();
140        }
141
142        int deleteCount = getContentResolver().delete(
143                WatchedPrograms.CONTENT_URI,
144                WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + "<?",
145                new String[] { String.valueOf(maxStartTimeMillis + 1) });
146        if (DEBUG && deleteCount > 0) {
147            Log.d(TAG, "Deleted " + deleteCount + " of " + totalCount + " watched programs"
148                  + " (reason: entry count > " + maxEntryCount + ")");
149        }
150    }
151}
152