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