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.util.Log;
20 
21 import androidx.annotation.Nullable;
22 
23 import com.google.protobuf.MessageLite;
24 
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.OutputStream;
31 import java.util.concurrent.ExecutorService;
32 import java.util.concurrent.Executors;
33 
34 /**
35  * Class level abstraction representing a proto file holding app data.
36  *
37  * Only a single controller should hold reference to this class. All methods that perform read or
38  * write operations must be thread safe and idempotent.
39  *
40  * @param <T> the proto object type that this data file is holding
41  */
42 public abstract class ProtoDataSource<T extends MessageLite> {
43     private final File mFile;
44     private static final String TAG = "ProtoDataSource";
45     private FileInputStream mInputStream;
46     private FileOutputStream mOutputStream;
47 
ProtoDataSource(File dataFileDirectory, String dataFileName)48     public ProtoDataSource(File dataFileDirectory, String dataFileName) {
49         mFile = new File(dataFileDirectory, dataFileName);
50     }
51 
52     /**
53      * @return true if the file exists on disk, and false otherwise.
54      */
exists()55     public boolean exists() {
56         return mFile.exists();
57     }
58 
59     /**
60      * Used by subclasses to access the mFile object.
61      */
getDataFile()62     protected File getDataFile() {
63         return mFile;
64     }
65 
66     /**
67      * Writes the {@link MessageLite} subclass T to the file represented by this object in the
68      * background thread.
69      */
writeToFileInBackgroundThread(T data)70     public void writeToFileInBackgroundThread(T data) {
71         ExecutorService executorService = Executors.newSingleThreadExecutor();
72         executorService.execute(() -> {
73             writeToFile(data);
74             executorService.shutdown();
75         });
76     }
77 
78     /**
79      * Writes the {@link MessageLite} subclass T to the file represented by this object.
80      */
writeToFile(T data)81     public boolean writeToFile(T data) {
82         boolean success = true;
83         try {
84             if (mOutputStream == null) {
85                 mOutputStream = new FileOutputStream(getDataFile(), false);
86             }
87             writeDelimitedTo(data, mOutputStream);
88         } catch (IOException e) {
89             Log.e(TAG, "Launcher item list not written to file successfully.");
90             success = false;
91         } finally {
92             try {
93                 if (mOutputStream != null) {
94                     mOutputStream.flush();
95                     mOutputStream.getFD().sync();
96                     mOutputStream.close();
97                     mOutputStream = null;
98                 }
99             } catch (IOException e) {
100                 Log.e(TAG, "Unable to close output stream. ");
101             }
102         }
103         return success;
104     }
105 
106     /**
107      * Reads the {@link MessageLite} subclass T from the file represented by this object.
108      */
109     @Nullable
readFromFile()110     public T readFromFile() {
111         if (!exists()) {
112             Log.e(TAG, "File does not exist. Cannot read from file.");
113             return null;
114         }
115         T result = null;
116         try {
117             if (mInputStream == null) {
118                 mInputStream = new FileInputStream(getDataFile());
119             }
120             result = parseDelimitedFrom(mInputStream);
121         } catch (IOException e) {
122             Log.e(TAG, "Read from input stream not successfully");
123         } finally {
124             if (mInputStream != null) {
125                 try {
126                     mInputStream.close();
127                     mInputStream = null;
128                 } catch (IOException e) {
129                     Log.e(TAG, "Unable to close input stream");
130                 }
131             }
132         }
133         return result;
134     }
135 
136     /**
137      * @return True if delete file was successful, false otherwise
138      */
deleteFile()139     public boolean deleteFile() {
140         boolean success = false;
141         try {
142             if (mFile.exists()) {
143                 success = mFile.delete();
144             }
145         } catch (SecurityException ex) {
146             Log.e(TAG, "deleteFile - " + ex);
147         }
148         return success;
149     }
150 
151     /**
152      * This method will be called by {@link ProtoDataSource#readFromFile}.
153      *
154      * Implementation is left to subclass since {@link MessageLite.parseDelimitedFrom(InputStream)}
155      * requires a defined class at compile time. Subclasses should implement this method by directly
156      * calling YourMessageType.parseDelimitedFrom(inputStream) here.
157      *
158      * @param inputStream the input stream to be which the data source should read from.
159      * @return the object T written to this file.
160      * @throws IOException an IOException for when reading from proto fails.
161      */
162     @Nullable
parseDelimitedFrom(InputStream inputStream)163     protected abstract T parseDelimitedFrom(InputStream inputStream) throws IOException;
164 
165     /**
166      * This method will be called by
167      * {@link ProtoDataSource#writeToFileInBackgroundThread(MessageLite)}.
168      *
169      * Implementation is left to subclass since {@link MessageLite#writeDelimitedTo(OutputStream)}
170      * requires a defined class at compile time. Subclasses should implement this method by directly
171      * calling T.writeDelimitedTo(outputStream) here.
172      *
173      * @param outputData the output data T to be written to the file.
174      * @param outputStream the output stream which the data should be written to.
175      * @throws IOException an IO Exception for when writing to proto fails.
176      */
writeDelimitedTo(T outputData, OutputStream outputStream)177     protected abstract void writeDelimitedTo(T outputData, OutputStream outputStream)
178             throws IOException;
179 }
180