Home | History | Annotate | Download | only in recording
      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.common.recording;
     18 
     19 import android.content.BroadcastReceiver;
     20 import android.content.ContentResolver;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.IntentFilter;
     24 import android.os.Environment;
     25 import android.os.Looper;
     26 import android.os.StatFs;
     27 import android.support.annotation.AnyThread;
     28 import android.support.annotation.IntDef;
     29 import android.support.annotation.WorkerThread;
     30 import android.util.Log;
     31 import com.android.tv.common.SoftPreconditions;
     32 import com.android.tv.common.feature.CommonFeatures;
     33 import java.io.File;
     34 import java.io.IOException;
     35 import java.lang.annotation.Retention;
     36 import java.lang.annotation.RetentionPolicy;
     37 import java.util.Objects;
     38 import java.util.Set;
     39 import java.util.concurrent.CopyOnWriteArraySet;
     40 
     41 /** Signals DVR storage status change such as plugging/unplugging. */
     42 public class RecordingStorageStatusManager {
     43     private static final String TAG = "RecordingStorageStatusManager";
     44     private static final boolean DEBUG = false;
     45 
     46     /** Minimum storage size to support DVR */
     47     public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB
     48 
     49     private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES =
     50             10 * 1024 * 1024 * 1024L; // 10GB
     51     private static final String RECORDING_DATA_SUB_PATH = "/recording";
     52 
     53     /** Storage status constants. */
     54     @IntDef({
     55         STORAGE_STATUS_OK,
     56         STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL,
     57         STORAGE_STATUS_FREE_SPACE_INSUFFICIENT,
     58         STORAGE_STATUS_MISSING
     59     })
     60     @Retention(RetentionPolicy.SOURCE)
     61     public @interface StorageStatus {}
     62 
     63     /** Current storage is OK to record a program. */
     64     public static final int STORAGE_STATUS_OK = 0;
     65 
     66     /** Current storage's total capacity is smaller than DVR requirement. */
     67     public static final int STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL = 1;
     68 
     69     /** Current storage's free space is insufficient to record programs. */
     70     public static final int STORAGE_STATUS_FREE_SPACE_INSUFFICIENT = 2;
     71 
     72     /** Current storage is missing. */
     73     public static final int STORAGE_STATUS_MISSING = 3;
     74 
     75     private final Context mContext;
     76     private final Set<OnStorageMountChangedListener> mOnStorageMountChangedListeners =
     77             new CopyOnWriteArraySet<>();
     78     private MountedStorageStatus mMountedStorageStatus;
     79     private boolean mStorageValid;
     80 
     81     private class MountedStorageStatus {
     82         private final boolean mStorageMounted;
     83         private final File mStorageMountedDir;
     84         private final long mStorageMountedCapacity;
     85 
     86         private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) {
     87             mStorageMounted = mounted;
     88             mStorageMountedDir = mountedDir;
     89             mStorageMountedCapacity = capacity;
     90         }
     91 
     92         private boolean isValidForDvr() {
     93             return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES;
     94         }
     95 
     96         @Override
     97         public boolean equals(Object other) {
     98             if (!(other instanceof MountedStorageStatus)) {
     99                 return false;
    100             }
    101             MountedStorageStatus status = (MountedStorageStatus) other;
    102             return mStorageMounted == status.mStorageMounted
    103                     && Objects.equals(mStorageMountedDir, status.mStorageMountedDir)
    104                     && mStorageMountedCapacity == status.mStorageMountedCapacity;
    105         }
    106     }
    107 
    108     public interface OnStorageMountChangedListener {
    109 
    110         /**
    111          * Listener for DVR storage status change.
    112          *
    113          * @param storageMounted {@code true} when DVR possible storage is mounted, {@code false}
    114          *     otherwise.
    115          */
    116         void onStorageMountChanged(boolean storageMounted);
    117     }
    118 
    119     private final class StorageStatusBroadcastReceiver extends BroadcastReceiver {
    120         @Override
    121         public void onReceive(Context context, Intent intent) {
    122             MountedStorageStatus result = getStorageStatusInternal();
    123             if (mMountedStorageStatus.equals(result)) {
    124                 return;
    125             }
    126             mMountedStorageStatus = result;
    127             if (result.mStorageMounted) {
    128                 cleanUpDbIfNeeded();
    129             }
    130             boolean valid = result.isValidForDvr();
    131             if (valid == mStorageValid) {
    132                 return;
    133             }
    134             mStorageValid = valid;
    135             for (OnStorageMountChangedListener l : mOnStorageMountChangedListeners) {
    136                 l.onStorageMountChanged(valid);
    137             }
    138         }
    139     }
    140 
    141     /**
    142      * Creates RecordingStorageStatusManager.
    143      *
    144      * @param context {@link Context}
    145      */
    146     public RecordingStorageStatusManager(final Context context) {
    147         mContext = context;
    148         mMountedStorageStatus = getStorageStatusInternal();
    149         mStorageValid = mMountedStorageStatus.isValidForDvr();
    150         IntentFilter filter = new IntentFilter();
    151         filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
    152         filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
    153         filter.addAction(Intent.ACTION_MEDIA_EJECT);
    154         filter.addAction(Intent.ACTION_MEDIA_REMOVED);
    155         filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL);
    156         filter.addDataScheme(ContentResolver.SCHEME_FILE);
    157         mContext.registerReceiver(new StorageStatusBroadcastReceiver(), filter);
    158     }
    159 
    160     /**
    161      * Adds the listener for receiving storage status change.
    162      *
    163      * @param listener
    164      */
    165     public void addListener(OnStorageMountChangedListener listener) {
    166         mOnStorageMountChangedListeners.add(listener);
    167     }
    168 
    169     /** Removes the current listener. */
    170     public void removeListener(OnStorageMountChangedListener listener) {
    171         mOnStorageMountChangedListeners.remove(listener);
    172     }
    173 
    174     /** Returns true if a storage is mounted. */
    175     public boolean isStorageMounted() {
    176         return mMountedStorageStatus.mStorageMounted;
    177     }
    178 
    179     /** Returns the path to DVR recording data directory. This can take for a while sometimes. */
    180     @WorkerThread
    181     public File getRecordingRootDataDirectory() {
    182         SoftPreconditions.checkState(Looper.myLooper() != Looper.getMainLooper());
    183         if (mMountedStorageStatus.mStorageMountedDir == null) {
    184             return null;
    185         }
    186         File root = mContext.getExternalFilesDir(null);
    187         String rootPath;
    188         try {
    189             rootPath = root != null ? root.getCanonicalPath() : null;
    190         } catch (IOException | SecurityException e) {
    191             return null;
    192         }
    193         return rootPath == null ? null : new File(rootPath + RECORDING_DATA_SUB_PATH);
    194     }
    195 
    196     /**
    197      * Returns the current storage status for DVR recordings.
    198      *
    199      * @return {@link StorageStatus}
    200      */
    201     @AnyThread
    202     public @StorageStatus int getDvrStorageStatus() {
    203         MountedStorageStatus status = mMountedStorageStatus;
    204         if (status.mStorageMountedDir == null) {
    205             return STORAGE_STATUS_MISSING;
    206         }
    207         if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(mContext)) {
    208             return STORAGE_STATUS_OK;
    209         }
    210         if (status.mStorageMountedCapacity < MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
    211             return STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL;
    212         }
    213         try {
    214             StatFs statFs = new StatFs(status.mStorageMountedDir.toString());
    215             if (statFs.getAvailableBytes() < MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
    216                 return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
    217             }
    218         } catch (IllegalArgumentException e) {
    219             // In rare cases, storage status change was not notified yet.
    220             SoftPreconditions.checkState(false);
    221             return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
    222         }
    223         return STORAGE_STATUS_OK;
    224     }
    225 
    226     /**
    227      * Returns whether the storage has sufficient storage.
    228      *
    229      * @return {@code true} when there is sufficient storage, {@code false} otherwise
    230      */
    231     public boolean isStorageSufficient() {
    232         return getDvrStorageStatus() == STORAGE_STATUS_OK;
    233     }
    234 
    235     /** APPs that want to clean up DB for recordings should override this method to do the job. */
    236     protected void cleanUpDbIfNeeded() {}
    237 
    238     private MountedStorageStatus getStorageStatusInternal() {
    239         boolean storageMounted =
    240                 Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    241         File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null;
    242         storageMounted = storageMounted && storageMountedDir != null;
    243         long storageMountedCapacity = 0L;
    244         if (storageMounted) {
    245             try {
    246                 StatFs statFs = new StatFs(storageMountedDir.toString());
    247                 storageMountedCapacity = statFs.getTotalBytes();
    248             } catch (IllegalArgumentException e) {
    249                 Log.e(TAG, "Storage mount status was changed.");
    250                 storageMounted = false;
    251                 storageMountedDir = null;
    252             }
    253         }
    254         return new MountedStorageStatus(storageMounted, storageMountedDir, storageMountedCapacity);
    255     }
    256 }
    257