WifiConfigStore.java revision b88000ed5302860e71e32646695daf661f56d927
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
17package com.android.server.wifi;
18
19import android.app.AlarmManager;
20import android.content.Context;
21import android.os.Environment;
22import android.os.FileUtils;
23import android.os.Handler;
24import android.os.Looper;
25import android.util.Log;
26
27import com.android.internal.annotations.VisibleForTesting;
28import com.android.internal.os.AtomicFile;
29
30import org.xmlpull.v1.XmlPullParserException;
31
32import java.io.File;
33import java.io.FileNotFoundException;
34import java.io.FileOutputStream;
35import java.io.IOException;
36
37/**
38 * This class provides the API's to save/load/modify network configurations from a persistent
39 * store. Uses keystore for certificate/key management operations.
40 * NOTE: This class should only be used from WifiConfigManager and is not thread-safe!
41 */
42public class WifiConfigStore {
43    /**
44     * Alarm tag to use for starting alarms for buffering file writes.
45     */
46    @VisibleForTesting
47    public static final String BUFFERED_WRITE_ALARM_TAG = "WriteBufferAlarm";
48    /**
49     * Log tag.
50     */
51    private static final String TAG = "WifiConfigStore";
52    /**
53     * Config store file name for both shared & user specific stores.
54     */
55    private static final String STORE_FILE_NAME = "WifiConfigStore.xml";
56    /**
57     * Directory to store the config store files in.
58     */
59    private static final String STORE_DIRECTORY_NAME = "wifi";
60    /**
61     * Time interval for buffering file writes for non-forced writes
62     */
63    private static final int BUFFERED_WRITE_ALARM_INTERVAL_MS = 10 * 1000;
64    /**
65     * Handler instance to post alarm timeouts to
66     */
67    private final Handler mEventHandler;
68    /**
69     * Alarm manager instance to start buffer timeout alarms.
70     */
71    private final AlarmManager mAlarmManager;
72    /**
73     * Clock instance to retrieve timestamps for alarms.
74     */
75    private final Clock mClock;
76    /**
77     * Shared config store file instance.
78     */
79    private StoreFile mSharedStore;
80    /**
81     * User specific store file instance.
82     */
83    private StoreFile mUserStore;
84    /**
85     * Verbose logging flag.
86     */
87    private boolean mVerboseLoggingEnabled = false;
88    /**
89     * Flag to indicate if there is a buffered write pending.
90     */
91    private boolean mBufferedWritePending = false;
92    /**
93     * Alarm listener for flushing out any buffered writes.
94     */
95    private final AlarmManager.OnAlarmListener mBufferedWriteListener =
96            new AlarmManager.OnAlarmListener() {
97                public void onAlarm() {
98                    try {
99                        writeBufferedData();
100                    } catch (IOException e) {
101                        Log.wtf(TAG, "Buffered write failed");
102                    }
103
104                }
105            };
106
107    /**
108     * Create a new instance of WifiConfigStore.
109     * Note: The store file instances have been made inputs to this class to ease unit-testing.
110     *
111     * @param context     context to use for retrieving the alarm manager.
112     * @param looper      looper instance to post alarm timeouts to.
113     * @param clock       clock instance to retrieve timestamps for alarms.
114     * @param sharedStore StoreFile instance pointing to the shared store file. This should
115     *                    be retrieved using {@link #createSharedFile()} method.
116     * @param userStore   StoreFile instance pointing to the user specific store file. This should
117     *                    be retrieved using {@link #createUserFile(int)} method.
118     */
119    public WifiConfigStore(Context context, Looper looper, Clock clock,
120            StoreFile sharedStore, StoreFile userStore) {
121
122        mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
123        mEventHandler = new Handler(looper);
124        mClock = clock;
125
126        // Initialize the store files.
127        mSharedStore = sharedStore;
128        mUserStore = userStore;
129    }
130
131    /**
132     * Helper method to create a store file instance for either the shared store or user store.
133     * Note: The method creates the store directory if not already present. This may be needed for
134     * user store files.
135     *
136     * @param storeBaseDir Base directory under which the store file is to be stored. The store file
137     *                     will be at <storeBaseDir>/wifi/WifiConfigStore.xml.
138     * @return new instance of the store file.
139     */
140    private static StoreFile createFile(File storeBaseDir) {
141        File storeDir = new File(storeBaseDir, STORE_DIRECTORY_NAME);
142        if (!storeDir.exists()) {
143            if (!storeDir.mkdir()) {
144                Log.w(TAG, "Could not create store directory " + storeDir);
145            }
146        }
147        return new StoreFile(new File(storeDir, STORE_FILE_NAME));
148    }
149
150    /**
151     * Create a new instance of the shared store file.
152     *
153     * @return new instance of the store file or null if the directory cannot be created.
154     */
155    public static StoreFile createSharedFile() {
156        return createFile(Environment.getDataMiscDirectory());
157    }
158
159    /**
160     * Create a new instance of the user specific store file.
161     * The user store file is inside the user's encrypted data directory.
162     *
163     * @param userId userId corresponding to the currently logged-in user.
164     * @return new instance of the store file or null if the directory cannot be created.
165     */
166    public static StoreFile createUserFile(int userId) {
167        return createFile(Environment.getDataMiscCeDirectory(userId));
168    }
169
170    /**
171     * Enable verbose logging.
172     */
173    public void enableVerboseLogging(boolean verbose) {
174        mVerboseLoggingEnabled = verbose;
175    }
176
177    /**
178     * API to check if any of the store files are present on the device. This can be used
179     * to detect if the device needs to perform data migration from legacy stores.
180     *
181     * @return true if any of the store file is present, false otherwise.
182     */
183    public boolean areStoresPresent() {
184        return (mSharedStore.exists() || mUserStore.exists());
185    }
186
187    /**
188     * API to write the provided store data to config stores.
189     * The method writes the user specific configurations to user specific config store and the
190     * shared configurations to shared config store.
191     *
192     * @param forceSync boolean to force write the config stores now. if false, the writes are
193     *                  buffered and written after the configured interval.
194     * @param storeData The entire data to be stored across all the config store files.
195     */
196    public void write(boolean forceSync, WifiConfigStoreData storeData)
197            throws XmlPullParserException, IOException {
198        // Serialize the provided data and send it to the respective stores. The actual write will
199        // be performed later depending on the |forceSync| flag .
200        byte[] sharedDataBytes = storeData.createSharedRawData();
201        byte[] userDataBytes = storeData.createUserRawData();
202
203        mSharedStore.storeRawDataToWrite(sharedDataBytes);
204        mUserStore.storeRawDataToWrite(userDataBytes);
205
206        // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides any
207        // pending buffer writes.
208        if (forceSync) {
209            writeBufferedData();
210        } else {
211            startBufferedWriteAlarm();
212        }
213    }
214
215    /**
216     * Helper method to start a buffered write alarm if one doesn't already exist.
217     */
218    private void startBufferedWriteAlarm() {
219        if (!mBufferedWritePending) {
220            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
221                    mClock.getElapsedSinceBootMillis() + BUFFERED_WRITE_ALARM_INTERVAL_MS,
222                    BUFFERED_WRITE_ALARM_TAG, mBufferedWriteListener, mEventHandler);
223            mBufferedWritePending = true;
224        }
225    }
226
227    /**
228     * Helper method to stop a buffered write alarm if one exists.
229     */
230    private void stopBufferedWriteAlarm() {
231        if (mBufferedWritePending) {
232            mAlarmManager.cancel(mBufferedWriteListener);
233            mBufferedWritePending = false;
234        }
235    }
236
237    /**
238     * Helper method to actually perform the writes to the file. This flushes out any write data
239     * being buffered in the respective stores and cancels any pending buffer write alarms.
240     */
241    private void writeBufferedData() throws IOException {
242        stopBufferedWriteAlarm();
243
244        long writeStartTime = mClock.getElapsedSinceBootMillis();
245        mSharedStore.writeBufferedRawData();
246        mUserStore.writeBufferedRawData();
247        long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime;
248
249        Log.d(TAG, "Writing to stores completed in " + writeTime + " ms.");
250    }
251
252    /**
253     * API to read the store data from the config stores.
254     * The method reads the user specific configurations from user specific config store and the
255     * shared configurations from the shared config store.
256     *
257     * @return storeData The entire data retrieved across all the config store files.
258     */
259    public WifiConfigStoreData read() throws XmlPullParserException, IOException {
260        long readStartTime = mClock.getElapsedSinceBootMillis();
261        byte[] sharedDataBytes = mSharedStore.readRawData();
262        byte[] userDataBytes = mUserStore.readRawData();
263        long readTime = mClock.getElapsedSinceBootMillis() - readStartTime;
264
265        Log.d(TAG, "Reading from stores completed in " + readTime + " ms.");
266
267        return WifiConfigStoreData.parseRawData(sharedDataBytes, userDataBytes);
268    }
269
270    /**
271     * Handle a user switch. This changes the user specific store.
272     *
273     * @param userStore StoreFile instance pointing to the user specific store file. This should
274     *                  be retrieved using {@link #createUserFile(int)} method.
275     */
276    public void switchUserStore(StoreFile userStore) {
277        // Stop any pending buffered writes, if any.
278        stopBufferedWriteAlarm();
279        mUserStore = userStore;
280    }
281
282    /**
283     * Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read
284     * raw data from the persistent file. This class provides helper methods to read/write the
285     * entire file into a byte array.
286     * This helps to separate out the processing/parsing from the actual file writing.
287     */
288    public static class StoreFile {
289        /**
290         * File permissions to lock down the file.
291         */
292        private static final int FILE_MODE = 0600;
293        /**
294         * The store file to be written to.
295         */
296        private final AtomicFile mAtomicFile;
297        /**
298         * This is an intermediate buffer to store the data to be written.
299         */
300        private byte[] mWriteData;
301        /**
302         * Store the file name for setting the file permissions/logging purposes.
303         */
304        private String mFileName;
305
306        public StoreFile(File file) {
307            mAtomicFile = new AtomicFile(file);
308            mFileName = mAtomicFile.getBaseFile().getAbsolutePath();
309        }
310
311        /**
312         * Returns whether the store file already exists on disk or not.
313         *
314         * @return true if it exists, false otherwise.
315         */
316        public boolean exists() {
317            return mAtomicFile.exists();
318        }
319
320        /**
321         * Read the entire raw data from the store file and return in a byte array.
322         *
323         * @return raw data read from the file or null if the file is not found.
324         * @throws IOException if an error occurs. The input stream is always closed by the method
325         * even when an exception is encountered.
326         */
327        public byte[] readRawData() throws IOException {
328            try {
329                return mAtomicFile.readFully();
330            } catch (FileNotFoundException e) {
331                return null;
332            }
333        }
334
335        /**
336         * Store the provided byte array to be written when {@link #writeBufferedRawData()} method
337         * is invoked.
338         * This intermediate step is needed to help in buffering file writes.
339         *
340         * @param data raw data to be written to the file.
341         */
342        public void storeRawDataToWrite(byte[] data) {
343            mWriteData = data;
344        }
345
346        /**
347         * Write the stored raw data to the store file.
348         * After the write to file, the mWriteData member is reset.
349         * @throws IOException if an error occurs. The output stream is always closed by the method
350         * even when an exception is encountered.
351         */
352        public void writeBufferedRawData() throws IOException {
353            if (mWriteData == null) {
354                Log.w(TAG, "No data stored for writing to file: " + mFileName);
355                return;
356            }
357            // Write the data to the atomic file.
358            FileOutputStream out = null;
359            try {
360                out = mAtomicFile.startWrite();
361                FileUtils.setPermissions(mFileName, FILE_MODE, -1, -1);
362                out.write(mWriteData);
363                mAtomicFile.finishWrite(out);
364            } catch (IOException e) {
365                if (out != null) {
366                    mAtomicFile.failWrite(out);
367                }
368                throw e;
369            }
370            // Reset the pending write data after write.
371            mWriteData = null;
372        }
373    }
374}
375