1 /*
2  * Copyright (C) 2016 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.tuner.tvinput;
18 
19 import android.app.job.JobParameters;
20 import android.app.job.JobService;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.media.tv.TvContract;
25 import android.net.Uri;
26 import android.os.AsyncTask;
27 import android.util.Log;
28 import com.android.tv.common.BaseApplication;
29 import com.android.tv.common.recording.RecordingStorageStatusManager;
30 import com.android.tv.common.util.CommonUtils;
31 import java.io.File;
32 import java.io.IOException;
33 import java.util.HashSet;
34 import java.util.Set;
35 import java.util.concurrent.TimeUnit;
36 
37 /**
38  * Creates {@link JobService} to clean up recorded program files which are not referenced from
39  * database.
40  */
41 public class TunerStorageCleanUpService extends JobService {
42     private static final String TAG = "TunerStorageCleanUpService";
43 
44     private CleanUpStorageTask mTask;
45 
46     @Override
onCreate()47     public void onCreate() {
48         if (getApplicationContext().getSystemService(Context.TV_INPUT_SERVICE) == null) {
49             Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
50             this.stopSelf();
51             return;
52         }
53         super.onCreate();
54         mTask = new CleanUpStorageTask(this, this);
55     }
56 
57     @Override
onStartJob(JobParameters params)58     public boolean onStartJob(JobParameters params) {
59         mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
60         return true;
61     }
62 
63     @Override
onStopJob(JobParameters params)64     public boolean onStopJob(JobParameters params) {
65         return false;
66     }
67 
68     /**
69      * Cleans up recorded program files which are not referenced from database. Cleaning up will be
70      * done periodically.
71      */
72     public static class CleanUpStorageTask extends AsyncTask<JobParameters, Void, JobParameters[]> {
73         private static final String[] mProjection = {
74             TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME,
75             TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI
76         };
77         private static final long ELAPSED_MILLIS_TO_DELETE = TimeUnit.DAYS.toMillis(1);
78 
79         private final Context mContext;
80         private final RecordingStorageStatusManager mDvrStorageStatusManager;
81         private final JobService mJobService;
82         private final ContentResolver mContentResolver;
83 
84         /**
85          * Creates a recurring storage cleaning task.
86          *
87          * @param context {@link Context}
88          * @param jobService {@link JobService}
89          */
CleanUpStorageTask(Context context, JobService jobService)90         public CleanUpStorageTask(Context context, JobService jobService) {
91             mContext = context;
92             mDvrStorageStatusManager =
93                     BaseApplication.getSingletons(context).getRecordingStorageStatusManager();
94             mJobService = jobService;
95             mContentResolver = mContext.getContentResolver();
96         }
97 
getRecordedProgramsDirs()98         private Set<String> getRecordedProgramsDirs() {
99             try (Cursor c =
100                     mContentResolver.query(
101                             TvContract.RecordedPrograms.CONTENT_URI,
102                             mProjection,
103                             null,
104                             null,
105                             null)) {
106                 if (c == null) {
107                     return null;
108                 }
109                 Set<String> recordedProgramDirs = new HashSet<>();
110                 while (c.moveToNext()) {
111                     String packageName = c.getString(0);
112                     String dataUriString = c.getString(1);
113                     if (dataUriString == null) {
114                         continue;
115                     }
116                     Uri dataUri = Uri.parse(dataUriString);
117                     if (!CommonUtils.isInBundledPackageSet(packageName)
118                             || dataUri == null
119                             || dataUri.getPath() == null
120                             || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) {
121                         continue;
122                     }
123                     File recordedProgramDir = new File(dataUri.getPath());
124                     try {
125                         recordedProgramDirs.add(recordedProgramDir.getCanonicalPath());
126                     } catch (IOException | SecurityException e) {
127                     }
128                 }
129                 return recordedProgramDirs;
130             }
131         }
132 
133         @Override
doInBackground(JobParameters... params)134         protected JobParameters[] doInBackground(JobParameters... params) {
135             if (mDvrStorageStatusManager.getDvrStorageStatus()
136                     == RecordingStorageStatusManager.STORAGE_STATUS_MISSING) {
137                 return params;
138             }
139             File dvrRecordingDir = mDvrStorageStatusManager.getRecordingRootDataDirectory();
140             if (dvrRecordingDir == null || !dvrRecordingDir.isDirectory()) {
141                 return params;
142             }
143             Set<String> recordedProgramDirs = getRecordedProgramsDirs();
144             if (recordedProgramDirs == null) {
145                 return params;
146             }
147             File[] files = dvrRecordingDir.listFiles();
148             if (files == null || files.length == 0) {
149                 return params;
150             }
151             for (File recordingDir : files) {
152                 try {
153                     if (!recordedProgramDirs.contains(recordingDir.getCanonicalPath())) {
154                         long lastModified = recordingDir.lastModified();
155                         long now = System.currentTimeMillis();
156                         if (lastModified != 0 && lastModified < now - ELAPSED_MILLIS_TO_DELETE) {
157                             // To prevent current recordings from being deleted,
158                             // deletes recordings which was not modified for long enough time.
159                             if (!CommonUtils.deleteDirOrFile(recordingDir)) {
160                                 Log.w(TAG, "Unable to delete recording data at " + recordingDir);
161                             }
162                         }
163                     }
164                 } catch (IOException | SecurityException e) {
165                     // would not happen
166                 }
167             }
168             return params;
169         }
170 
171         @Override
onPostExecute(JobParameters[] params)172         protected void onPostExecute(JobParameters[] params) {
173             for (JobParameters param : params) {
174                 mJobService.jobFinished(param, false);
175             }
176         }
177     }
178 }
179