Home | History | Annotate | Download | only in dvr
      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