1 /*
2  * Copyright (C) 2021 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.server.apphibernation;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.WorkerThread;
22 import android.text.format.DateUtils;
23 import android.util.AtomicFile;
24 import android.util.Slog;
25 import android.util.proto.ProtoInputStream;
26 import android.util.proto.ProtoOutputStream;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 
30 import java.io.File;
31 import java.io.FileInputStream;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.concurrent.ScheduledExecutorService;
37 import java.util.concurrent.ScheduledFuture;
38 import java.util.concurrent.TimeUnit;
39 
40 /**
41  * Disk store utility class for hibernation states.
42  *
43  * @param <T> the type of hibernation state data
44  */
45 class HibernationStateDiskStore<T> {
46     private static final String TAG = "HibernationStateDiskStore";
47 
48     // Time to wait before actually writing. Saves extra writes if data changes come in batches.
49     private static final long DISK_WRITE_DELAY = 1L * DateUtils.MINUTE_IN_MILLIS;
50     private static final String STATES_FILE_NAME = "states";
51 
52     private final File mHibernationFile;
53     private final ScheduledExecutorService mExecutorService;
54     private final ProtoReadWriter<List<T>> mProtoReadWriter;
55     private List<T> mScheduledStatesToWrite = new ArrayList<>();
56     private ScheduledFuture<?> mFuture;
57 
58     /**
59      * Initialize a disk store for hibernation states in the given directory.
60      *
61      * @param hibernationDir directory to write/read states file
62      * @param readWriter writer/reader of states proto
63      * @param executorService scheduled executor for writing data
64      */
HibernationStateDiskStore(@onNull File hibernationDir, @NonNull ProtoReadWriter<List<T>> readWriter, @NonNull ScheduledExecutorService executorService)65     HibernationStateDiskStore(@NonNull File hibernationDir,
66             @NonNull ProtoReadWriter<List<T>> readWriter,
67             @NonNull ScheduledExecutorService executorService) {
68         this(hibernationDir, readWriter, executorService, STATES_FILE_NAME);
69     }
70 
71     @VisibleForTesting
HibernationStateDiskStore(@onNull File hibernationDir, @NonNull ProtoReadWriter<List<T>> readWriter, @NonNull ScheduledExecutorService executorService, @NonNull String fileName)72     HibernationStateDiskStore(@NonNull File hibernationDir,
73             @NonNull ProtoReadWriter<List<T>> readWriter,
74             @NonNull ScheduledExecutorService executorService,
75             @NonNull String fileName) {
76         mHibernationFile = new File(hibernationDir, fileName);
77         mExecutorService = executorService;
78         mProtoReadWriter = readWriter;
79     }
80 
81     /**
82      * Schedule a full write of all the hibernation states to the file on disk. Does not run
83      * immediately and subsequent writes override previous ones.
84      *
85      * @param hibernationStates list of hibernation states to write to disk
86      */
scheduleWriteHibernationStates(@onNull List<T> hibernationStates)87     void scheduleWriteHibernationStates(@NonNull List<T> hibernationStates) {
88         synchronized (this) {
89             mScheduledStatesToWrite = hibernationStates;
90             if (mExecutorService.isShutdown()) {
91                 Slog.e(TAG, "Scheduled executor service is shut down.");
92                 return;
93             }
94 
95             // Already have write scheduled
96             if (mFuture != null) {
97                 Slog.i(TAG, "Write already scheduled. Skipping schedule.");
98                 return;
99             }
100 
101             mFuture = mExecutorService.schedule(this::writeHibernationStates, DISK_WRITE_DELAY,
102                     TimeUnit.MILLISECONDS);
103         }
104     }
105 
106     /**
107      * Read hibernation states from disk.
108      *
109      * @return the parsed list of hibernation states, null if file does not exist
110      */
111     @Nullable
112     @WorkerThread
readHibernationStates()113     List<T> readHibernationStates() {
114         synchronized (this) {
115             if (!mHibernationFile.exists()) {
116                 Slog.i(TAG, "No hibernation file on disk for file " + mHibernationFile.getPath());
117                 return null;
118             }
119             AtomicFile atomicFile = new AtomicFile(mHibernationFile);
120 
121             try {
122                 FileInputStream inputStream = atomicFile.openRead();
123                 ProtoInputStream protoInputStream = new ProtoInputStream(inputStream);
124                 return mProtoReadWriter.readFromProto(protoInputStream);
125             } catch (IOException e) {
126                 Slog.e(TAG, "Failed to read states protobuf.", e);
127                 return null;
128             }
129         }
130     }
131 
132     @WorkerThread
writeHibernationStates()133     private void writeHibernationStates() {
134         synchronized (this) {
135             writeStateProto(mScheduledStatesToWrite);
136             mScheduledStatesToWrite.clear();
137             mFuture = null;
138         }
139     }
140 
141     @WorkerThread
writeStateProto(List<T> states)142     private void writeStateProto(List<T> states) {
143         AtomicFile atomicFile = new AtomicFile(mHibernationFile);
144 
145         FileOutputStream fileOutputStream;
146         try {
147             fileOutputStream = atomicFile.startWrite();
148         } catch (IOException e) {
149             Slog.e(TAG, "Failed to start write to states protobuf.", e);
150             return;
151         }
152 
153         try {
154             ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
155             mProtoReadWriter.writeToProto(protoOutputStream, states);
156             protoOutputStream.flush();
157             atomicFile.finishWrite(fileOutputStream);
158         } catch (Exception e) {
159             Slog.e(TAG, "Failed to finish write to states protobuf.", e);
160             atomicFile.failWrite(fileOutputStream);
161         }
162     }
163 }
164