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.dvr; 18 19 import android.content.BroadcastReceiver; 20 import android.content.ContentProviderOperation; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.OperationApplicationException; 26 import android.database.Cursor; 27 import android.media.tv.TvContract; 28 import android.media.tv.TvInputInfo; 29 import android.net.Uri; 30 import android.os.AsyncTask; 31 import android.os.Environment; 32 import android.os.Looper; 33 import android.os.RemoteException; 34 import android.os.StatFs; 35 import android.support.annotation.AnyThread; 36 import android.support.annotation.IntDef; 37 import android.support.annotation.WorkerThread; 38 import android.util.Log; 39 40 import com.android.tv.TvApplication; 41 import com.android.tv.common.SoftPreconditions; 42 import com.android.tv.common.feature.CommonFeatures; 43 import com.android.tv.tuner.tvinput.TunerTvInputService; 44 import com.android.tv.util.TvInputManagerHelper; 45 import com.android.tv.util.Utils; 46 47 import java.io.File; 48 import java.io.IOException; 49 import java.lang.annotation.Retention; 50 import java.lang.annotation.RetentionPolicy; 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.Objects; 54 import java.util.Set; 55 import java.util.concurrent.CopyOnWriteArraySet; 56 57 /** 58 * Signals DVR storage status change such as plugging/unplugging. 59 */ 60 public class DvrStorageStatusManager { 61 private static final String TAG = "DvrStorageStatusManager"; 62 private static final boolean DEBUG = false; 63 64 /** 65 * Minimum storage size to support DVR 66 */ 67 public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB 68 private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES 69 = 10 * 1024 * 1024 * 1024L; // 10GB 70 private static final String RECORDING_DATA_SUB_PATH = "/recording"; 71 72 private static final String[] PROJECTION = { 73 TvContract.RecordedPrograms._ID, 74 TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME, 75 TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI 76 }; 77 private final static int BATCH_OPERATION_COUNT = 100; 78 79 @IntDef({STORAGE_STATUS_OK, STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL, 80 STORAGE_STATUS_FREE_SPACE_INSUFFICIENT, STORAGE_STATUS_MISSING}) 81 @Retention(RetentionPolicy.SOURCE) 82 public @interface StorageStatus { 83 } 84 85 /** 86 * Current storage is OK to record a program. 87 */ 88 public static final int STORAGE_STATUS_OK = 0; 89 90 /** 91 * Current storage's total capacity is smaller than DVR requirement. 92 */ 93 public static final int STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL = 1; 94 95 /** 96 * Current storage's free space is insufficient to record programs. 97 */ 98 public static final int STORAGE_STATUS_FREE_SPACE_INSUFFICIENT = 2; 99 100 /** 101 * Current storage is missing. 102 */ 103 public static final int STORAGE_STATUS_MISSING = 3; 104 105 private final Context mContext; 106 private final Set<OnStorageMountChangedListener> mOnStorageMountChangedListeners = 107 new CopyOnWriteArraySet<>(); 108 private final boolean mRunningInMainProcess; 109 private MountedStorageStatus mMountedStorageStatus; 110 private boolean mStorageValid; 111 private CleanUpDbTask mCleanUpDbTask; 112 113 private class MountedStorageStatus { 114 private final boolean mStorageMounted; 115 private final File mStorageMountedDir; 116 private final long mStorageMountedCapacity; 117 118 private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) { 119 mStorageMounted = mounted; 120 mStorageMountedDir = mountedDir; 121 mStorageMountedCapacity = capacity; 122 } 123 124 private boolean isValidForDvr() { 125 return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES; 126 } 127 128 @Override 129 public boolean equals(Object other) { 130 if (!(other instanceof MountedStorageStatus)) { 131 return false; 132 } 133 MountedStorageStatus status = (MountedStorageStatus) other; 134 return mStorageMounted == status.mStorageMounted 135 && Objects.equals(mStorageMountedDir, status.mStorageMountedDir) 136 && mStorageMountedCapacity == status.mStorageMountedCapacity; 137 } 138 } 139 140 public interface OnStorageMountChangedListener { 141 142 /** 143 * Listener for DVR storage status change. 144 * 145 * @param storageMounted {@code true} when DVR possible storage is mounted, 146 * {@code false} otherwise. 147 */ 148 void onStorageMountChanged(boolean storageMounted); 149 } 150 151 private final class StorageStatusBroadcastReceiver extends BroadcastReceiver { 152 @Override 153 public void onReceive(Context context, Intent intent) { 154 MountedStorageStatus result = getStorageStatusInternal(); 155 if (mMountedStorageStatus.equals(result)) { 156 return; 157 } 158 mMountedStorageStatus = result; 159 if (result.mStorageMounted && mRunningInMainProcess) { 160 // Cleans up DB in LC process. 161 // Tuner process is not always on. 162 if (mCleanUpDbTask != null) { 163 mCleanUpDbTask.cancel(true); 164 } 165 mCleanUpDbTask = new CleanUpDbTask(); 166 mCleanUpDbTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 167 } 168 boolean valid = result.isValidForDvr(); 169 if (valid == mStorageValid) { 170 return; 171 } 172 mStorageValid = valid; 173 for (OnStorageMountChangedListener l : mOnStorageMountChangedListeners) { 174 l.onStorageMountChanged(valid); 175 } 176 } 177 } 178 179 /** 180 * Creates DvrStorageStatusManager. 181 * 182 * @param context {@link Context} 183 */ 184 public DvrStorageStatusManager(final Context context, boolean runningInMainProcess) { 185 mContext = context; 186 mRunningInMainProcess = runningInMainProcess; 187 mMountedStorageStatus = getStorageStatusInternal(); 188 mStorageValid = mMountedStorageStatus.isValidForDvr(); 189 IntentFilter filter = new IntentFilter(); 190 filter.addAction(Intent.ACTION_MEDIA_MOUNTED); 191 filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 192 filter.addAction(Intent.ACTION_MEDIA_EJECT); 193 filter.addAction(Intent.ACTION_MEDIA_REMOVED); 194 filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); 195 filter.addDataScheme(ContentResolver.SCHEME_FILE); 196 mContext.registerReceiver(new StorageStatusBroadcastReceiver(), filter); 197 } 198 199 /** 200 * Adds the listener for receiving storage status change. 201 * 202 * @param listener 203 */ 204 public void addListener(OnStorageMountChangedListener listener) { 205 mOnStorageMountChangedListeners.add(listener); 206 } 207 208 /** 209 * Removes the current listener. 210 */ 211 public void removeListener(OnStorageMountChangedListener listener) { 212 mOnStorageMountChangedListeners.remove(listener); 213 } 214 215 /** 216 * Returns true if a storage is mounted. 217 */ 218 public boolean isStorageMounted() { 219 return mMountedStorageStatus.mStorageMounted; 220 } 221 222 /** 223 * Returns the path to DVR recording data directory. 224 * This can take for a while sometimes. 225 */ 226 @WorkerThread 227 public File getRecordingRootDataDirectory() { 228 SoftPreconditions.checkState(Looper.myLooper() != Looper.getMainLooper()); 229 if (mMountedStorageStatus.mStorageMountedDir == null) { 230 return null; 231 } 232 File root = mContext.getExternalFilesDir(null); 233 String rootPath; 234 try { 235 rootPath = root != null ? root.getCanonicalPath() : null; 236 } catch (IOException | SecurityException e) { 237 return null; 238 } 239 return rootPath == null ? null : new File(rootPath + RECORDING_DATA_SUB_PATH); 240 } 241 242 /** 243 * Returns the current storage status for DVR recordings. 244 * 245 * @return {@link StorageStatus} 246 */ 247 @AnyThread 248 public @StorageStatus int getDvrStorageStatus() { 249 MountedStorageStatus status = mMountedStorageStatus; 250 if (status.mStorageMountedDir == null) { 251 return STORAGE_STATUS_MISSING; 252 } 253 if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(mContext)) { 254 return STORAGE_STATUS_OK; 255 } 256 if (status.mStorageMountedCapacity < MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES) { 257 return STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL; 258 } 259 try { 260 StatFs statFs = new StatFs(status.mStorageMountedDir.toString()); 261 if (statFs.getAvailableBytes() < MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) { 262 return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; 263 } 264 } catch (IllegalArgumentException e) { 265 // In rare cases, storage status change was not notified yet. 266 SoftPreconditions.checkState(false); 267 return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; 268 } 269 return STORAGE_STATUS_OK; 270 } 271 272 /** 273 * Returns whether the storage has sufficient storage. 274 * 275 * @return {@code true} when there is sufficient storage, {@code false} otherwise 276 */ 277 public boolean isStorageSufficient() { 278 return getDvrStorageStatus() == STORAGE_STATUS_OK; 279 } 280 281 private MountedStorageStatus getStorageStatusInternal() { 282 boolean storageMounted = 283 Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); 284 File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null; 285 storageMounted = storageMounted && storageMountedDir != null; 286 long storageMountedCapacity = 0L; 287 if (storageMounted) { 288 try { 289 StatFs statFs = new StatFs(storageMountedDir.toString()); 290 storageMountedCapacity = statFs.getTotalBytes(); 291 } catch (IllegalArgumentException e) { 292 Log.e(TAG, "Storage mount status was changed."); 293 storageMounted = false; 294 storageMountedDir = null; 295 } 296 } 297 return new MountedStorageStatus( 298 storageMounted, storageMountedDir, storageMountedCapacity); 299 } 300 301 private class CleanUpDbTask extends AsyncTask<Void, Void, Boolean> { 302 private final ContentResolver mContentResolver; 303 304 private CleanUpDbTask() { 305 mContentResolver = mContext.getContentResolver(); 306 } 307 308 @Override 309 protected Boolean doInBackground(Void... params) { 310 @DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus(); 311 if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { 312 return null; 313 } 314 if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { 315 return true; 316 } 317 List<ContentProviderOperation> ops = getDeleteOps(); 318 if (ops == null || ops.isEmpty()) { 319 return null; 320 } 321 Log.i(TAG, "New device storage mounted. # of recordings to be forgotten : " 322 + ops.size()); 323 for (int i = 0 ; i < ops.size() && !isCancelled() ; i += BATCH_OPERATION_COUNT) { 324 int toIndex = (i + BATCH_OPERATION_COUNT) > ops.size() 325 ? ops.size() : (i + BATCH_OPERATION_COUNT); 326 ArrayList<ContentProviderOperation> batchOps = 327 new ArrayList<>(ops.subList(i, toIndex)); 328 try { 329 mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, batchOps); 330 } catch (RemoteException | OperationApplicationException e) { 331 Log.e(TAG, "Failed to clean up RecordedPrograms.", e); 332 } 333 } 334 return null; 335 } 336 337 @Override 338 protected void onPostExecute(Boolean forgetStorage) { 339 if (forgetStorage != null && forgetStorage == true) { 340 DvrManager dvrManager = TvApplication.getSingletons(mContext).getDvrManager(); 341 TvInputManagerHelper tvInputManagerHelper = 342 TvApplication.getSingletons(mContext).getTvInputManagerHelper(); 343 List<TvInputInfo> tvInputInfoList = 344 tvInputManagerHelper.getTvInputInfos(true, false); 345 if (tvInputInfoList == null || tvInputInfoList.isEmpty()) { 346 return; 347 } 348 for (TvInputInfo info : tvInputInfoList) { 349 if (Utils.isBundledInput(info.getId())) { 350 dvrManager.forgetStorage(info.getId()); 351 } 352 } 353 } 354 if (mCleanUpDbTask == this) { 355 mCleanUpDbTask = null; 356 } 357 } 358 359 private List<ContentProviderOperation> getDeleteOps() { 360 List<ContentProviderOperation> ops = new ArrayList<>(); 361 362 try (Cursor c = mContentResolver.query( 363 TvContract.RecordedPrograms.CONTENT_URI, PROJECTION, null, null, null)) { 364 if (c == null) { 365 return null; 366 } 367 while (c.moveToNext()) { 368 @DvrStorageStatusManager.StorageStatus int storageStatus = 369 getDvrStorageStatus(); 370 if (isCancelled() 371 || storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { 372 ops.clear(); 373 break; 374 } 375 String id = c.getString(0); 376 String packageName = c.getString(1); 377 String dataUriString = c.getString(2); 378 if (dataUriString == null) { 379 continue; 380 } 381 Uri dataUri = Uri.parse(dataUriString); 382 if (!Utils.isInBundledPackageSet(packageName) 383 || dataUri == null || dataUri.getPath() == null 384 || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) { 385 continue; 386 } 387 File recordedProgramDir = new File(dataUri.getPath()); 388 if (!recordedProgramDir.exists()) { 389 ops.add(ContentProviderOperation.newDelete( 390 TvContract.buildRecordedProgramUri(Long.parseLong(id))).build()); 391 } 392 } 393 return ops; 394 } 395 } 396 } 397 } 398