• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
MountedStorageStatus(boolean mounted, File mountedDir, long capacity)86         private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) {
87             mStorageMounted = mounted;
88             mStorageMountedDir = mountedDir;
89             mStorageMountedCapacity = capacity;
90         }
91 
isValidForDvr()92         private boolean isValidForDvr() {
93             return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES;
94         }
95 
96         @Override
equals(Object other)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          */
onStorageMountChanged(boolean storageMounted)116         void onStorageMountChanged(boolean storageMounted);
117     }
118 
119     private final class StorageStatusBroadcastReceiver extends BroadcastReceiver {
120         @Override
onReceive(Context context, Intent intent)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      */
RecordingStorageStatusManager(final Context context)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      */
addListener(OnStorageMountChangedListener listener)165     public void addListener(OnStorageMountChangedListener listener) {
166         mOnStorageMountChangedListeners.add(listener);
167     }
168 
169     /** Removes the current listener. */
removeListener(OnStorageMountChangedListener listener)170     public void removeListener(OnStorageMountChangedListener listener) {
171         mOnStorageMountChangedListeners.remove(listener);
172     }
173 
174     /** Returns true if a storage is mounted. */
isStorageMounted()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
getRecordingRootDataDirectory()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
getDvrStorageStatus()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             Log.w(TAG, "Error getting Dvr Storage Status.", e);
221             SoftPreconditions.checkState(false);
222             return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
223         }
224         return STORAGE_STATUS_OK;
225     }
226 
227     /**
228      * Returns whether the storage has sufficient storage.
229      *
230      * @return {@code true} when there is sufficient storage, {@code false} otherwise
231      */
isStorageSufficient()232     public boolean isStorageSufficient() {
233         return getDvrStorageStatus() == STORAGE_STATUS_OK;
234     }
235 
236     /** APPs that want to clean up DB for recordings should override this method to do the job. */
cleanUpDbIfNeeded()237     protected void cleanUpDbIfNeeded() {}
238 
getStorageStatusInternal()239     private MountedStorageStatus getStorageStatusInternal() {
240         boolean storageMounted =
241                 Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
242         File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null;
243         storageMounted = storageMounted && storageMountedDir != null;
244         long storageMountedCapacity = 0L;
245         if (storageMounted) {
246             try {
247                 StatFs statFs = new StatFs(storageMountedDir.toString());
248                 storageMountedCapacity = statFs.getTotalBytes();
249             } catch (IllegalArgumentException e) {
250                 Log.w(TAG, "Storage mount status was changed.", e);
251                 storageMounted = false;
252                 storageMountedDir = null;
253             }
254         }
255         return new MountedStorageStatus(storageMounted, storageMountedDir, storageMountedCapacity);
256     }
257 }
258