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