1 /*
2  * Copyright (C) 2023 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.car.carlauncher.datastore;
18 
19 import android.os.FileObserver;
20 import android.util.Log;
21 
22 import androidx.annotation.Nullable;
23 
24 import com.google.protobuf.MessageLite;
25 
26 import java.io.File;
27 import java.io.FileInputStream;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.OutputStream;
32 import java.util.concurrent.ExecutorService;
33 import java.util.concurrent.Executors;
34 
35 /**
36  * Class level abstraction representing a proto file holding app data.
37  *
38  * Only a single controller should hold reference to this class. All methods that perform read or
39  * write operations must be thread safe and idempotent.
40  *
41  * @param <T> the proto object type that this data file is holding
42  */
43 public abstract class ProtoDataSource<T extends MessageLite> {
44     private final File mFile;
45     private static final String TAG = "ProtoDataSource";
46     private FileInputStream mInputStream;
47     private FileOutputStream mOutputStream;
48     private FileDeletionObserver mFileDeletionObserver;
49     private FileObserver mFileObserver;
50 
ProtoDataSource(File dataFileDirectory, String dataFileName)51     public ProtoDataSource(File dataFileDirectory, String dataFileName) {
52         mFile = new File(dataFileDirectory, dataFileName);
53     }
54 
55     /**
56      * @return true if the file exists on disk, and false otherwise.
57      */
exists()58     public boolean exists() {
59         return mFile.exists();
60     }
61 
62     /**
63      * Used by subclasses to access the mFile object.
64      */
getDataFile()65     protected File getDataFile() {
66         return mFile;
67     }
68 
69     /**
70      * Writes the {@link MessageLite} subclass T to the file represented by this object in the
71      * background thread.
72      */
writeToFileInBackgroundThread(T data)73     public void writeToFileInBackgroundThread(T data) {
74         ExecutorService executorService = Executors.newSingleThreadExecutor();
75         executorService.execute(() -> {
76             writeToFile(data);
77             executorService.shutdown();
78         });
79     }
80 
81     /**
82      * Writes the {@link MessageLite} subclass T to the file represented by this object.
83      */
writeToFile(T data)84     public boolean writeToFile(T data) {
85         boolean success = true;
86         boolean dataFileAlreadyExisted = getDataFile().exists();
87         try {
88             if (mOutputStream == null) {
89                 mOutputStream = new FileOutputStream(getDataFile(), false);
90             }
91             writeDelimitedTo(data, mOutputStream);
92         } catch (IOException e) {
93             Log.e(TAG, "Launcher item list not written to file successfully.");
94             success = false;
95         } finally {
96             try {
97                 if (mOutputStream != null) {
98                     mOutputStream.flush();
99                     mOutputStream.getFD().sync();
100                     mOutputStream.close();
101                     mOutputStream = null;
102                 }
103             } catch (IOException e) {
104                 Log.e(TAG, "Unable to close output stream. ");
105             }
106         }
107         // If writing to the file was successful and this file is newly created, attach deletion
108         // observer.
109         if (success && !dataFileAlreadyExisted) {
110             // Stop watching for deletions on any previously monitored file.
111             detachFileDeletionObserver();
112             mFileObserver = new FileObserver(getDataFile()) {
113                 @Override
114                 public void onEvent(int event, @Nullable String path) {
115                     if (DELETE_SELF == event) {
116                         Log.i(TAG, "DELETE_SELF event triggered");
117                         mFileDeletionObserver.onDeleted();
118                         mFileObserver.stopWatching();
119                     }
120                 }
121             };
122             mFileObserver.startWatching();
123         }
124         return success;
125     }
126 
127     /**
128      * Reads the {@link MessageLite} subclass T from the file represented by this object.
129      */
130     @Nullable
readFromFile()131     public T readFromFile() {
132         if (!exists()) {
133             Log.e(TAG, "File does not exist. Cannot read from file.");
134             return null;
135         }
136         T result = null;
137         try {
138             if (mInputStream == null) {
139                 mInputStream = new FileInputStream(getDataFile());
140             }
141             result = parseDelimitedFrom(mInputStream);
142         } catch (IOException e) {
143             Log.e(TAG, "Read from input stream not successfully");
144         } finally {
145             if (mInputStream != null) {
146                 try {
147                     mInputStream.close();
148                     mInputStream = null;
149                 } catch (IOException e) {
150                     Log.e(TAG, "Unable to close input stream");
151                 }
152             }
153         }
154         return result;
155     }
156 
157     /**
158      * @return True if delete file was successful, false otherwise
159      */
deleteFile()160     public boolean deleteFile() {
161         boolean success = false;
162         try {
163             if (mFile.exists()) {
164                 success = mFile.delete();
165             }
166         } catch (SecurityException ex) {
167             Log.e(TAG, "deleteFile - " + ex);
168         }
169         return success;
170     }
171 
172     /**
173      * Attaches a {@link FileDeletionObserver} that will be notified when the
174      * associated proto file is deleted.
175      *
176      * <p>Calling this method replaces any previously attached observer.
177      *
178      * @param observer The {@link FileDeletionObserver} to attach, or {@code null}
179      *        to remove any existing observer.
180      */
attachFileDeletionObserver(FileDeletionObserver observer)181     public void attachFileDeletionObserver(FileDeletionObserver observer) {
182         mFileDeletionObserver = observer;
183     }
184 
185     /**
186      * Detaches the currently attached {@link FileDeletionObserver}, if any.
187      *
188      * <p>This stops the observer from receiving further notifications about file
189      * deletion events.
190      */
detachFileDeletionObserver()191     public void detachFileDeletionObserver() {
192         if (mFileObserver != null) {
193             mFileObserver.stopWatching();
194         }
195     }
196 
197     /**
198      * This method will be called by {@link ProtoDataSource#readFromFile}.
199      *
200      * Implementation is left to subclass since {@link MessageLite.parseDelimitedFrom(InputStream)}
201      * requires a defined class at compile time. Subclasses should implement this method by directly
202      * calling YourMessageType.parseDelimitedFrom(inputStream) here.
203      *
204      * @param inputStream the input stream to be which the data source should read from.
205      * @return the object T written to this file.
206      * @throws IOException an IOException for when reading from proto fails.
207      */
208     @Nullable
parseDelimitedFrom(InputStream inputStream)209     protected abstract T parseDelimitedFrom(InputStream inputStream) throws IOException;
210 
211     /**
212      * This method will be called by
213      * {@link ProtoDataSource#writeToFileInBackgroundThread(MessageLite)}.
214      *
215      * Implementation is left to subclass since {@link MessageLite#writeDelimitedTo(OutputStream)}
216      * requires a defined class at compile time. Subclasses should implement this method by directly
217      * calling T.writeDelimitedTo(outputStream) here.
218      *
219      * @param outputData the output data T to be written to the file.
220      * @param outputStream the output stream which the data should be written to.
221      * @throws IOException an IO Exception for when writing to proto fails.
222      */
writeDelimitedTo(T outputData, OutputStream outputStream)223     protected abstract void writeDelimitedTo(T outputData, OutputStream outputStream)
224             throws IOException;
225 
226     /**
227      * An interface for observing the deletion of a file.
228      *
229      * <p>Classes that implement this interface can be attached to a
230      * {@code ProtoDataSource} (or a similar class managing file monitoring)
231      * to receive notifications when the associated file is deleted.
232      *
233      * @see ProtoDataSource#attachFileDeletionObserver(FileDeletionObserver)
234      * @see ProtoDataSource#detachFileDeletionObserver()
235      */
236     public interface FileDeletionObserver {
237         /**
238          * Called when the observed file is deleted.
239          */
onDeleted()240         void onDeleted();
241     }
242 }
243