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