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