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.net.Uri;
29 import android.os.AsyncTask;
30 import android.os.Environment;
31 import android.os.Looper;
32 import android.os.RemoteException;
33 import android.os.StatFs;
34 import android.support.annotation.AnyThread;
35 import android.support.annotation.IntDef;
36 import android.support.annotation.WorkerThread;
37 import android.util.Log;
38 
39 import com.android.tv.common.SoftPreconditions;
40 import com.android.tv.common.feature.CommonFeatures;
41 import com.android.tv.util.Utils;
42 
43 import java.io.File;
44 import java.io.IOException;
45 import java.lang.annotation.Retention;
46 import java.lang.annotation.RetentionPolicy;
47 import java.util.ArrayList;
48 import java.util.List;
49 import java.util.Objects;
50 import java.util.Set;
51 import java.util.concurrent.CopyOnWriteArraySet;
52 
53 /**
54  * Signals DVR storage status change such as plugging/unplugging.
55  */
56 public class DvrStorageStatusManager {
57     private static final String TAG = "DvrStorageStatusManager";
58     private static final boolean DEBUG = false;
59 
60     /**
61      * Minimum storage size to support DVR
62      */
63     public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB
64     private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES
65             = 10 * 1024 * 1024 * 1024L; // 10GB
66     private static final String RECORDING_DATA_SUB_PATH = "/recording";
67 
68     private static final String[] PROJECTION = {
69             TvContract.RecordedPrograms._ID,
70             TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME,
71             TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI
72     };
73     private final static int BATCH_OPERATION_COUNT = 100;
74 
75     @IntDef({STORAGE_STATUS_OK, STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL,
76             STORAGE_STATUS_FREE_SPACE_INSUFFICIENT, STORAGE_STATUS_MISSING})
77     @Retention(RetentionPolicy.SOURCE)
78     public @interface StorageStatus {
79     }
80 
81     /**
82      * Current storage is OK to record a program.
83      */
84     public static final int STORAGE_STATUS_OK = 0;
85 
86     /**
87      * Current storage's total capacity is smaller than DVR requirement.
88      */
89     public static final int STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL = 1;
90 
91     /**
92      * Current storage's free space is insufficient to record programs.
93      */
94     public static final int STORAGE_STATUS_FREE_SPACE_INSUFFICIENT = 2;
95 
96     /**
97      * Current storage is missing.
98      */
99     public static final int STORAGE_STATUS_MISSING = 3;
100 
101     private final Context mContext;
102     private final Set<OnStorageMountChangedListener> mOnStorageMountChangedListeners =
103             new CopyOnWriteArraySet<>();
104     private final boolean mRunningInMainProcess;
105     private MountedStorageStatus mMountedStorageStatus;
106     private boolean mStorageValid;
107     private CleanUpDbTask mCleanUpDbTask;
108 
109     private class MountedStorageStatus {
110         private final boolean mStorageMounted;
111         private final File mStorageMountedDir;
112         private final long mStorageMountedCapacity;
113 
MountedStorageStatus(boolean mounted, File mountedDir, long capacity)114         private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) {
115             mStorageMounted = mounted;
116             mStorageMountedDir = mountedDir;
117             mStorageMountedCapacity = capacity;
118         }
119 
isValidForDvr()120         private boolean isValidForDvr() {
121             return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES;
122         }
123 
124         @Override
equals(Object other)125         public boolean equals(Object other) {
126             if (!(other instanceof MountedStorageStatus)) {
127                 return false;
128             }
129             MountedStorageStatus status = (MountedStorageStatus) other;
130             return mStorageMounted == status.mStorageMounted
131                     && Objects.equals(mStorageMountedDir, status.mStorageMountedDir)
132                     && mStorageMountedCapacity == status.mStorageMountedCapacity;
133         }
134     }
135 
136     public interface OnStorageMountChangedListener {
137 
138         /**
139          * Listener for DVR storage status change.
140          *
141          * @param storageMounted {@code true} when DVR possible storage is mounted,
142          *                       {@code false} otherwise.
143          */
onStorageMountChanged(boolean storageMounted)144         void onStorageMountChanged(boolean storageMounted);
145     }
146 
147     private final class StorageStatusBroadcastReceiver extends BroadcastReceiver {
148         @Override
onReceive(Context context, Intent intent)149         public void onReceive(Context context, Intent intent) {
150             MountedStorageStatus result = getStorageStatusInternal();
151             if (mMountedStorageStatus.equals(result)) {
152                 return;
153             }
154             mMountedStorageStatus = result;
155             if (result.mStorageMounted && mRunningInMainProcess) {
156                 // Cleans up DB in LC process.
157                 // Tuner process is not always on.
158                 if (mCleanUpDbTask != null) {
159                     mCleanUpDbTask.cancel(true);
160                 }
161                 mCleanUpDbTask = new CleanUpDbTask();
162                 mCleanUpDbTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
163             }
164             boolean valid = result.isValidForDvr();
165             if (valid == mStorageValid) {
166                 return;
167             }
168             mStorageValid = valid;
169             for (OnStorageMountChangedListener l : mOnStorageMountChangedListeners) {
170                 l.onStorageMountChanged(valid);
171             }
172         }
173     }
174 
175     /**
176      * Creates DvrStorageStatusManager.
177      *
178      * @param context {@link Context}
179      */
DvrStorageStatusManager(final Context context, boolean runningInMainProcess)180     public DvrStorageStatusManager(final Context context, boolean runningInMainProcess) {
181         mContext = context;
182         mRunningInMainProcess = runningInMainProcess;
183         mMountedStorageStatus = getStorageStatusInternal();
184         mStorageValid = mMountedStorageStatus.isValidForDvr();
185         IntentFilter filter = new IntentFilter();
186         filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
187         filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
188         filter.addAction(Intent.ACTION_MEDIA_EJECT);
189         filter.addAction(Intent.ACTION_MEDIA_REMOVED);
190         filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL);
191         filter.addDataScheme(ContentResolver.SCHEME_FILE);
192         mContext.registerReceiver(new StorageStatusBroadcastReceiver(), filter);
193     }
194 
195     /**
196      * Adds the listener for receiving storage status change.
197      *
198      * @param listener
199      */
addListener(OnStorageMountChangedListener listener)200     public void addListener(OnStorageMountChangedListener listener) {
201         mOnStorageMountChangedListeners.add(listener);
202     }
203 
204     /**
205      * Removes the current listener.
206      */
removeListener(OnStorageMountChangedListener listener)207     public void removeListener(OnStorageMountChangedListener listener) {
208         mOnStorageMountChangedListeners.remove(listener);
209     }
210 
211     /**
212      * Returns true if a storage is mounted.
213      */
isStorageMounted()214     public boolean isStorageMounted() {
215         return mMountedStorageStatus.mStorageMounted;
216     }
217 
218     /**
219      * Returns the path to DVR recording data directory.
220      * This can take for a while sometimes.
221      */
222     @WorkerThread
getRecordingRootDataDirectory()223     public File getRecordingRootDataDirectory() {
224         SoftPreconditions.checkState(Looper.myLooper() != Looper.getMainLooper());
225         if (mMountedStorageStatus.mStorageMountedDir == null) {
226             return null;
227         }
228         File root = mContext.getExternalFilesDir(null);
229         String rootPath;
230         try {
231             rootPath = root != null ? root.getCanonicalPath() : null;
232         } catch (IOException | SecurityException e) {
233             return null;
234         }
235         return rootPath == null ? null : new File(rootPath + RECORDING_DATA_SUB_PATH);
236     }
237 
238     /**
239      * Returns the current storage status for DVR recordings.
240      *
241      * @return {@link StorageStatus}
242      */
243     @AnyThread
getDvrStorageStatus()244     public @StorageStatus int getDvrStorageStatus() {
245         MountedStorageStatus status = mMountedStorageStatus;
246         if (status.mStorageMountedDir == null) {
247             return STORAGE_STATUS_MISSING;
248         }
249         if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(mContext)) {
250             return STORAGE_STATUS_OK;
251         }
252         if (status.mStorageMountedCapacity < MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
253             return STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL;
254         }
255         try {
256             StatFs statFs = new StatFs(status.mStorageMountedDir.toString());
257             if (statFs.getAvailableBytes() < MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
258                 return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
259             }
260         } catch (IllegalArgumentException e) {
261             // In rare cases, storage status change was not notified yet.
262             SoftPreconditions.checkState(false);
263             return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
264         }
265         return STORAGE_STATUS_OK;
266     }
267 
268     /**
269      * Returns whether the storage has sufficient storage.
270      *
271      * @return {@code true} when there is sufficient storage, {@code false} otherwise
272      */
isStorageSufficient()273     public boolean isStorageSufficient() {
274         return getDvrStorageStatus() == STORAGE_STATUS_OK;
275     }
276 
getStorageStatusInternal()277     private MountedStorageStatus getStorageStatusInternal() {
278         boolean storageMounted =
279                 Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
280         File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null;
281         storageMounted = storageMounted && storageMountedDir != null;
282         long storageMountedCapacity = 0L;
283         if (storageMounted) {
284             try {
285                 StatFs statFs = new StatFs(storageMountedDir.toString());
286                 storageMountedCapacity = statFs.getTotalBytes();
287             } catch (IllegalArgumentException e) {
288                 Log.e(TAG, "Storage mount status was changed.");
289                 storageMounted = false;
290                 storageMountedDir = null;
291             }
292         }
293         return new MountedStorageStatus(
294                 storageMounted, storageMountedDir, storageMountedCapacity);
295     }
296 
297     private class CleanUpDbTask extends AsyncTask<Void, Void, Void> {
298         private final ContentResolver mContentResolver;
299 
CleanUpDbTask()300         private CleanUpDbTask() {
301             mContentResolver = mContext.getContentResolver();
302         }
303 
304         @Override
doInBackground(Void... params)305         protected Void doInBackground(Void... params) {
306             @DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus();
307             if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
308                 return null;
309             }
310             List<ContentProviderOperation> ops = getDeleteOps(storageStatus
311                     == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL);
312             if (ops == null || ops.isEmpty()) {
313                 return null;
314             }
315             Log.i(TAG, "New device storage mounted. # of recordings to be forgotten : "
316                     + ops.size());
317             for (int i = 0 ; i < ops.size() && !isCancelled() ; i += BATCH_OPERATION_COUNT) {
318                 int toIndex = (i + BATCH_OPERATION_COUNT) > ops.size()
319                         ? ops.size() : (i + BATCH_OPERATION_COUNT);
320                 ArrayList<ContentProviderOperation> batchOps =
321                         new ArrayList<>(ops.subList(i, toIndex));
322                 try {
323                     mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, batchOps);
324                 } catch (RemoteException | OperationApplicationException e) {
325                     Log.e(TAG, "Failed to clean up  RecordedPrograms.", e);
326                 }
327             }
328             return null;
329         }
330 
331         @Override
onPostExecute(Void result)332         protected void onPostExecute(Void result) {
333             if (mCleanUpDbTask == this) {
334                 mCleanUpDbTask = null;
335             }
336         }
337 
getDeleteOps(boolean deleteAll)338         private List<ContentProviderOperation> getDeleteOps(boolean deleteAll) {
339             List<ContentProviderOperation> ops = new ArrayList<>();
340 
341             try (Cursor c = mContentResolver.query(
342                     TvContract.RecordedPrograms.CONTENT_URI, PROJECTION, null, null, null)) {
343                 if (c == null) {
344                     return null;
345                 }
346                 while (c.moveToNext()) {
347                     @DvrStorageStatusManager.StorageStatus int storageStatus =
348                             getDvrStorageStatus();
349                     if (isCancelled()
350                             || storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
351                         ops.clear();
352                         break;
353                     }
354                     String id = c.getString(0);
355                     String packageName = c.getString(1);
356                     String dataUriString = c.getString(2);
357                     if (dataUriString == null) {
358                         continue;
359                     }
360                     Uri dataUri = Uri.parse(dataUriString);
361                     if (!Utils.isInBundledPackageSet(packageName)
362                             || dataUri == null || dataUri.getPath() == null
363                             || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) {
364                         continue;
365                     }
366                     File recordedProgramDir = new File(dataUri.getPath());
367                     if (deleteAll || !recordedProgramDir.exists()) {
368                         ops.add(ContentProviderOperation.newDelete(
369                                 TvContract.buildRecordedProgramUri(Long.parseLong(id))).build());
370                     }
371                 }
372                 return ops;
373             }
374         }
375     }
376 }
377