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