WifiConfigStore.java revision 1a2b2242a2f30e0ad6dfa1d43265a15059db2a8a
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", e);
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     */
117    public WifiConfigStore(Context context, Looper looper, Clock clock,
118            StoreFile sharedStore) {
119
120        mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
121        mEventHandler = new Handler(looper);
122        mClock = clock;
123
124        // Initialize the store files.
125        mSharedStore = sharedStore;
126        // The user store is initialized to null, this will be set when the user unlocks and
127        // CE storage is accessible via |switchUserStoreAndRead|.
128        mUserStore = null;
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 != null && 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        mSharedStore.storeRawDataToWrite(sharedDataBytes);
202        if (mUserStore != null) {
203            byte[] userDataBytes = storeData.createUserRawData();
204            mUserStore.storeRawDataToWrite(userDataBytes);
205        }
206
207        // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides any
208        // pending buffer writes.
209        if (forceSync) {
210            writeBufferedData();
211        } else {
212            startBufferedWriteAlarm();
213        }
214    }
215
216    /**
217     * Helper method to start a buffered write alarm if one doesn't already exist.
218     */
219    private void startBufferedWriteAlarm() {
220        if (!mBufferedWritePending) {
221            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
222                    mClock.getElapsedSinceBootMillis() + BUFFERED_WRITE_ALARM_INTERVAL_MS,
223                    BUFFERED_WRITE_ALARM_TAG, mBufferedWriteListener, mEventHandler);
224            mBufferedWritePending = true;
225        }
226    }
227
228    /**
229     * Helper method to stop a buffered write alarm if one exists.
230     */
231    private void stopBufferedWriteAlarm() {
232        if (mBufferedWritePending) {
233            mAlarmManager.cancel(mBufferedWriteListener);
234            mBufferedWritePending = false;
235        }
236    }
237
238    /**
239     * Helper method to actually perform the writes to the file. This flushes out any write data
240     * being buffered in the respective stores and cancels any pending buffer write alarms.
241     */
242    private void writeBufferedData() throws IOException {
243        stopBufferedWriteAlarm();
244
245        long writeStartTime = mClock.getElapsedSinceBootMillis();
246        mSharedStore.writeBufferedRawData();
247        if (mUserStore != null) {
248            mUserStore.writeBufferedRawData();
249        }
250        long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime;
251
252        Log.d(TAG, "Writing to stores completed in " + writeTime + " ms.");
253    }
254
255    /**
256     * API to read the store data from the config stores.
257     * The method reads the user specific configurations from user specific config store and the
258     * shared configurations from the shared config store.
259     *
260     * @return storeData The entire data retrieved across all the config store files.
261     */
262    public WifiConfigStoreData read() throws XmlPullParserException, IOException {
263        long readStartTime = mClock.getElapsedSinceBootMillis();
264        byte[] sharedDataBytes = mSharedStore.readRawData();
265        byte[] userDataBytes = null;
266        if (mUserStore != null) {
267            userDataBytes = mUserStore.readRawData();
268        }
269        long readTime = mClock.getElapsedSinceBootMillis() - readStartTime;
270        Log.d(TAG, "Reading from stores completed in " + readTime + " ms.");
271
272        return WifiConfigStoreData.parseRawData(sharedDataBytes, userDataBytes);
273    }
274
275    /**
276     * Handles a user switch. This method changes the user specific store file and reads from the
277     * new user's store file.
278     *
279     * @param userStore StoreFile instance pointing to the user specific store file. This should
280     *                  be retrieved using {@link #createUserFile(int)} method.
281     */
282    public WifiConfigStoreData switchUserStoreAndRead(StoreFile userStore)
283            throws XmlPullParserException, IOException {
284        // Stop any pending buffered writes, if any.
285        stopBufferedWriteAlarm();
286        mUserStore = userStore;
287
288        // Now read from the user store file.
289        long readStartTime = mClock.getElapsedSinceBootMillis();
290        byte[] userDataBytes = mUserStore.readRawData();
291        long readTime = mClock.getElapsedSinceBootMillis() - readStartTime;
292        Log.d(TAG, "Reading from user store completed in " + readTime + " ms.");
293
294        return WifiConfigStoreData.parseRawData(null, userDataBytes);
295    }
296
297    /**
298     * Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read
299     * raw data from the persistent file. This class provides helper methods to read/write the
300     * entire file into a byte array.
301     * This helps to separate out the processing/parsing from the actual file writing.
302     */
303    public static class StoreFile {
304        /**
305         * File permissions to lock down the file.
306         */
307        private static final int FILE_MODE = 0600;
308        /**
309         * The store file to be written to.
310         */
311        private final AtomicFile mAtomicFile;
312        /**
313         * This is an intermediate buffer to store the data to be written.
314         */
315        private byte[] mWriteData;
316        /**
317         * Store the file name for setting the file permissions/logging purposes.
318         */
319        private String mFileName;
320
321        public StoreFile(File file) {
322            mAtomicFile = new AtomicFile(file);
323            mFileName = mAtomicFile.getBaseFile().getAbsolutePath();
324        }
325
326        /**
327         * Returns whether the store file already exists on disk or not.
328         *
329         * @return true if it exists, false otherwise.
330         */
331        public boolean exists() {
332            return mAtomicFile.exists();
333        }
334
335        /**
336         * Read the entire raw data from the store file and return in a byte array.
337         *
338         * @return raw data read from the file or null if the file is not found.
339         * @throws IOException if an error occurs. The input stream is always closed by the method
340         * even when an exception is encountered.
341         */
342        public byte[] readRawData() throws IOException {
343            try {
344                return mAtomicFile.readFully();
345            } catch (FileNotFoundException e) {
346                return null;
347            }
348        }
349
350        /**
351         * Store the provided byte array to be written when {@link #writeBufferedRawData()} method
352         * is invoked.
353         * This intermediate step is needed to help in buffering file writes.
354         *
355         * @param data raw data to be written to the file.
356         */
357        public void storeRawDataToWrite(byte[] data) {
358            mWriteData = data;
359        }
360
361        /**
362         * Write the stored raw data to the store file.
363         * After the write to file, the mWriteData member is reset.
364         * @throws IOException if an error occurs. The output stream is always closed by the method
365         * even when an exception is encountered.
366         */
367        public void writeBufferedRawData() throws IOException {
368            if (mWriteData == null) {
369                Log.w(TAG, "No data stored for writing to file: " + mFileName);
370                return;
371            }
372            // Write the data to the atomic file.
373            FileOutputStream out = null;
374            try {
375                out = mAtomicFile.startWrite();
376                FileUtils.setPermissions(mFileName, FILE_MODE, -1, -1);
377                out.write(mWriteData);
378                mAtomicFile.finishWrite(out);
379            } catch (IOException e) {
380                if (out != null) {
381                    mAtomicFile.failWrite(out);
382                }
383                throw e;
384            }
385            // Reset the pending write data after write.
386            mWriteData = null;
387        }
388    }
389}
390