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