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.documentsui.archives;
18 
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.net.Uri;
22 import android.util.Log;
23 
24 import androidx.annotation.GuardedBy;
25 
26 import java.io.IOException;
27 import java.util.concurrent.ExecutorService;
28 import java.util.concurrent.Executors;
29 
30 import org.apache.commons.compress.archivers.ArchiveException;
31 import org.apache.commons.compress.compressors.CompressorException;
32 
33 /**
34  * Loads an instance of Archive lazily.
35  */
36 public class Loader {
37     private static final String TAG = "Loader";
38 
39     public static final int STATUS_OPENING = 0;
40     public static final int STATUS_OPENED = 1;
41     public static final int STATUS_FAILED = 2;
42     public static final int STATUS_CLOSING = 3;
43     public static final int STATUS_CLOSED = 4;
44 
45     private final Context mContext;
46     private final Uri mArchiveUri;
47     private final int mAccessMode;
48     private final Uri mNotificationUri;
49     private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
50     private final Object mLock = new Object();
51     @GuardedBy("mLock")
52     private int mStatus = STATUS_OPENING;
53     @GuardedBy("mLock")
54     private int mRefCount = 0;
55     private Archive mArchive = null;
56 
Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri)57     Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri) {
58         this.mContext = context;
59         this.mArchiveUri = archiveUri;
60         this.mAccessMode = accessMode;
61         this.mNotificationUri = notificationUri;
62 
63         // Start loading the archive immediately in the background.
64         mExecutor.submit(this::get);
65     }
66 
get()67     synchronized Archive get() {
68         synchronized (mLock) {
69             if (mStatus == STATUS_OPENED) {
70                 return mArchive;
71             }
72         }
73 
74         synchronized (mLock) {
75             if (mStatus != STATUS_OPENING) {
76                 throw new IllegalStateException(
77                         "Trying to perform an operation on an archive which is invalidated.");
78             }
79         }
80 
81         try {
82             if (ReadableArchive.supportsAccessMode(mAccessMode)) {
83                 final ContentResolver contentResolver = mContext.getContentResolver();
84                 final String archiveMimeType = contentResolver.getType(mArchiveUri);
85                 mArchive = ReadableArchive.createForParcelFileDescriptor(
86                         mContext,
87                         contentResolver.openFileDescriptor(
88                                 mArchiveUri, "r", null /* signal */),
89                         mArchiveUri, archiveMimeType, mAccessMode, mNotificationUri);
90             } else if (WriteableArchive.supportsAccessMode(mAccessMode)) {
91                 mArchive = WriteableArchive.createForParcelFileDescriptor(
92                         mContext,
93                         mContext.getContentResolver().openFileDescriptor(
94                                 mArchiveUri, "w", null /* signal */),
95                         mArchiveUri, mAccessMode, mNotificationUri);
96             } else {
97                 throw new IllegalStateException("Access mode not supported.");
98             }
99             synchronized (mLock) {
100                 if (mRefCount == 0) {
101                     mArchive.close();
102                     mStatus = STATUS_CLOSED;
103                 } else {
104                     mStatus = STATUS_OPENED;
105                 }
106             }
107         } catch (IOException | RuntimeException | ArchiveException | CompressorException e) {
108             Log.e(TAG, "Failed to open the archive.", e);
109             synchronized (mLock) {
110                 mStatus = STATUS_FAILED;
111             }
112             throw new IllegalStateException("Failed to open the archive.", e);
113         } finally {
114             synchronized (mLock) {
115                 // Only notify when there might be someone listening.
116                 if (mRefCount > 0) {
117                     // Notify observers that the root directory is loaded (or failed)
118                     // so clients reload it.
119                     mContext.getContentResolver().notifyChange(
120                             ArchivesProvider.buildUriForArchive(mArchiveUri, mAccessMode),
121                             null /* observer */, false /* syncToNetwork */);
122                 }
123             }
124         }
125 
126         return mArchive;
127     }
128 
getStatus()129     int getStatus() {
130         synchronized (mLock) {
131             return mStatus;
132         }
133     }
134 
acquire()135     void acquire() {
136         synchronized (mLock) {
137             mRefCount++;
138         }
139     }
140 
release()141     void release() {
142         synchronized (mLock) {
143             mRefCount--;
144             if (mRefCount == 0) {
145                 assert(mStatus == STATUS_OPENING
146                         || mStatus == STATUS_OPENED
147                         || mStatus == STATUS_FAILED);
148 
149                 switch (mStatus) {
150                     case STATUS_OPENED:
151                         try {
152                             mArchive.close();
153                             mStatus = STATUS_CLOSED;
154                         } catch (IOException e) {
155                             Log.e(TAG, "Failed to close the archive on release.", e);
156                         }
157                         break;
158                     case STATUS_FAILED:
159                         mStatus = STATUS_CLOSED;
160                         break;
161                     case STATUS_OPENING:
162                         mStatus = STATUS_CLOSING;
163                         // ::get() will close the archive once opened.
164                         break;
165                 }
166             }
167         }
168     }
169 }
170