OSDN Git Service

Switch PackageStatusStorage to XML-based storage
authorNeil Fuller <nfuller@google.com>
Wed, 17 May 2017 03:43:12 +0000 (04:43 +0100)
committerNeil Fuller <nfuller@google.com>
Mon, 26 Jun 2017 13:49:59 +0000 (14:49 +0100)
Based on feedback from a framework developer: an XML file can be used
instead of a Sqlite DB.

To run tests:
make -j30 FrameworksServicesTests
adb install -r -g
"out/target/product/angler/data/app/FrameworksServicesTests/FrameworksServicesTests.apk"
adb shell am instrument -e package com.android.server.timezone -w
com.android.frameworks.servicestests \
    "com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner"

Test: See above.
Bug: 31008728
Change-Id: I1e6614d26df0e37ccea4dff82867e0b6aba39ca6

services/core/java/com/android/server/timezone/PackageStatusStorage.java
services/core/java/com/android/server/timezone/PackageTracker.java
services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java
services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java

index 31f0e31..05e97c7 100644 (file)
 
 package com.android.server.timezone;
 
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
+import com.android.internal.util.FastXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import android.util.AtomicFile;
 import android.util.Slog;
+import android.util.Xml;
 
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
 
 import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_FAILURE;
 import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_SUCCESS;
 import static com.android.server.timezone.PackageStatus.CHECK_STARTED;
+import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
 /**
  * Storage logic for accessing/mutating the Android system's persistent state related to time zone
- * update checking. There is expected to be a single instance and all methods synchronized on
- * {@code this} for thread safety.
+ * update checking. There is expected to be a single instance. All non-private methods are thread
+ * safe.
  */
 final class PackageStatusStorage {
 
-    private static final String TAG = "timezone.PackageStatusStorage";
+    private static final String LOG_TAG = "timezone.PackageStatusStorage";
 
-    private static final String DATABASE_NAME = "timezonepackagestatus.db";
-    private static final int DATABASE_VERSION = 1;
-
-    /** The table name. It will have a single row with _id == {@link #SINGLETON_ID} */
-    private static final String TABLE = "status";
-    private static final String COLUMN_ID = "_id";
+    private static final String TAG_PACKAGE_STATUS = "PackageStatus";
 
     /**
-     * Column that stores a monotonically increasing lock ID, used to detect concurrent update
+     * Attribute that stores a monotonically increasing lock ID, used to detect concurrent update
      * issues without on-line locks. Incremented on every write.
      */
-    private static final String COLUMN_OPTIMISTIC_LOCK_ID = "optimistic_lock_id";
+    private static final String ATTRIBUTE_OPTIMISTIC_LOCK_ID = "optimisticLockId";
 
     /**
-     * Column that stores the current "check status" of the time zone update application packages.
+     * Attribute that stores the current "check status" of the time zone update application
+     * packages.
      */
-    private static final String COLUMN_CHECK_STATUS = "check_status";
+    private static final String ATTRIBUTE_CHECK_STATUS = "checkStatus";
 
     /**
-     * Column that stores the version of the time zone rules update application being checked / last
-     * checked.
+     * Attribute that stores the version of the time zone rules update application being checked
+     * / last checked.
      */
-    private static final String COLUMN_UPDATE_APP_VERSION = "update_app_package_version";
+    private static final String ATTRIBUTE_UPDATE_APP_VERSION = "updateAppPackageVersion";
 
     /**
-     * Column that stores the version of the time zone rules data application being checked / last
-     * checked.
+     * Attribute that stores the version of the time zone rules data application being checked
+     * / last checked.
      */
-    private static final String COLUMN_DATA_APP_VERSION = "data_app_package_version";
-
-    /**
-     * The ID of the one row.
-     */
-    private static final int SINGLETON_ID = 1;
+    private static final String ATTRIBUTE_DATA_APP_VERSION = "dataAppPackageVersion";
 
     private static final int UNKNOWN_PACKAGE_VERSION = -1;
 
-    private final DatabaseHelper mDatabaseHelper;
+    private final AtomicFile mPackageStatusFile;
 
-    PackageStatusStorage(Context context) {
-        mDatabaseHelper = new DatabaseHelper(context);
+    PackageStatusStorage(File storageDir) {
+        mPackageStatusFile = new AtomicFile(new File(storageDir, "packageStatus.xml"));
+        if (!mPackageStatusFile.getBaseFile().exists()) {
+            try {
+                insertInitialPackageStatus();
+            } catch (IOException e) {
+                throw new IllegalStateException(e);
+            }
+        }
     }
 
-    void deleteDatabaseForTests() {
-        SQLiteDatabase.deleteDatabase(mDatabaseHelper.getDatabaseFile());
+    void deleteFileForTests() {
+        synchronized(this) {
+            mPackageStatusFile.delete();
+        }
     }
 
     /**
@@ -93,48 +103,60 @@ final class PackageStatusStorage {
         synchronized (this) {
             try {
                 return getPackageStatusInternal();
-            } catch (IllegalArgumentException e) {
-                // This means that data exists in the table but it was bad.
-                Slog.e(TAG, "Package status invalid, resetting and retrying", e);
+            } catch (ParseException e) {
+                // This means that data exists in the file but it was bad.
+                Slog.e(LOG_TAG, "Package status invalid, resetting and retrying", e);
 
                 // Reset the storage so it is in a good state again.
-                mDatabaseHelper.recoverFromBadData();
-                return getPackageStatusInternal();
+                recoverFromBadData(e);
+                try {
+                    return getPackageStatusInternal();
+                } catch (ParseException e2) {
+                    throw new IllegalStateException("Recovery from bad file failed", e2);
+                }
             }
         }
     }
 
-    private PackageStatus getPackageStatusInternal() {
-        String[] columns = {
-                COLUMN_CHECK_STATUS, COLUMN_UPDATE_APP_VERSION, COLUMN_DATA_APP_VERSION
-        };
-        Cursor cursor = mDatabaseHelper.getReadableDatabase()
-                .query(TABLE, columns, COLUMN_ID + " = ?",
-                        new String[] { Integer.toString(SINGLETON_ID) },
-                        null /* groupBy */, null /* having */, null /* orderBy */);
-        if (cursor.getCount() != 1) {
-            Slog.e(TAG, "Unable to find package status from package status row. Rows returned: "
-                    + cursor.getCount());
-            return null;
+    private PackageStatus getPackageStatusInternal() throws ParseException {
+        try (FileInputStream fis = mPackageStatusFile.openRead()) {
+            XmlPullParser parser = parseToPackageStatusTag(fis);
+            Integer checkStatus = getNullableIntAttribute(parser, ATTRIBUTE_CHECK_STATUS);
+            if (checkStatus == null) {
+                return null;
+            }
+            int updateAppVersion = getIntAttribute(parser, ATTRIBUTE_UPDATE_APP_VERSION);
+            int dataAppVersion = getIntAttribute(parser, ATTRIBUTE_DATA_APP_VERSION);
+            return new PackageStatus(checkStatus,
+                    new PackageVersions(updateAppVersion, dataAppVersion));
+        } catch (IOException e) {
+            ParseException e2 = new ParseException("Error reading package status", 0);
+            e2.initCause(e);
+            throw e2;
         }
-        cursor.moveToFirst();
+    }
 
-        // Determine check status.
-        if (cursor.isNull(0)) {
-            // This is normal the first time getPackageStatus() is called, or after
-            // resetCheckState().
-            return null;
+    // Callers should be synchronized(this).
+    private int recoverFromBadData(Exception cause) {
+        mPackageStatusFile.delete();
+        try {
+            return insertInitialPackageStatus();
+        } catch (IOException e) {
+            IllegalStateException fatal = new IllegalStateException(e);
+            fatal.addSuppressed(cause);
+            throw fatal;
         }
-        int checkStatus = cursor.getInt(0);
+    }
 
-        // Determine package version.
-        if (cursor.isNull(1) || cursor.isNull(2)) {
-            Slog.e(TAG, "Package version information unexpectedly null");
-            return null;
-        }
-        PackageVersions packageVersions = new PackageVersions(cursor.getInt(1), cursor.getInt(2));
+    /** Insert the initial data, returning the optimistic lock ID */
+    private int insertInitialPackageStatus() throws IOException {
+        // Doesn't matter what it is, but we avoid the obvious starting value each time the data
+        // is reset to ensure that old tokens are unlikely to work.
+        final int initialOptimisticLockId = (int) System.currentTimeMillis();
 
-        return new PackageStatus(checkStatus, packageVersions);
+        writePackageStatusInternal(null /* status */, initialOptimisticLockId,
+                null /* packageVersions */);
+        return initialOptimisticLockId;
     }
 
     /**
@@ -147,23 +169,29 @@ final class PackageStatusStorage {
         }
 
         synchronized (this) {
-            Integer optimisticLockId = getCurrentOptimisticLockId();
-            if (optimisticLockId == null) {
-                Slog.w(TAG, "Unable to find optimistic lock ID from package status row");
+            int optimisticLockId;
+            try {
+                optimisticLockId = getCurrentOptimisticLockId();
+            } catch (ParseException e) {
+                Slog.w(LOG_TAG, "Unable to find optimistic lock ID from package status");
 
                 // Recover.
-                optimisticLockId = mDatabaseHelper.recoverFromBadData();
+                optimisticLockId = recoverFromBadData(e);
             }
 
             int newOptimisticLockId = optimisticLockId + 1;
-            boolean statusRowUpdated = writeStatusRow(
-                    optimisticLockId, newOptimisticLockId, CHECK_STARTED, currentInstalledVersions);
-            if (!statusRowUpdated) {
-                Slog.e(TAG, "Unable to update status to CHECK_STARTED in package status row."
-                        + " synchronization failure?");
-                return null;
+            try {
+                boolean statusUpdated = writePackageStatusWithOptimisticLockCheck(
+                        optimisticLockId, newOptimisticLockId, CHECK_STARTED,
+                        currentInstalledVersions);
+                if (!statusUpdated) {
+                    throw new IllegalStateException("Unable to update status to CHECK_STARTED."
+                            + " synchronization failure?");
+                }
+                return new CheckToken(newOptimisticLockId, currentInstalledVersions);
+            } catch (IOException e) {
+                throw new IllegalStateException(e);
             }
-            return new CheckToken(newOptimisticLockId, currentInstalledVersions);
         }
     }
 
@@ -172,19 +200,25 @@ final class PackageStatusStorage {
      */
     void resetCheckState() {
         synchronized(this) {
-            Integer optimisticLockId = getCurrentOptimisticLockId();
-            if (optimisticLockId == null) {
-                Slog.w(TAG, "resetCheckState: Unable to find optimistic lock ID from package"
-                        + " status row");
+            int optimisticLockId;
+            try {
+                optimisticLockId = getCurrentOptimisticLockId();
+            } catch (ParseException e) {
+                Slog.w(LOG_TAG, "resetCheckState: Unable to find optimistic lock ID from package"
+                        + " status");
                 // Attempt to recover the storage state.
-                optimisticLockId = mDatabaseHelper.recoverFromBadData();
+                optimisticLockId = recoverFromBadData(e);
             }
 
             int newOptimisticLockId = optimisticLockId + 1;
-            if (!writeStatusRow(optimisticLockId, newOptimisticLockId,
-                    null /* status */, null /* packageVersions */)) {
-                Slog.e(TAG, "resetCheckState: Unable to reset package status row,"
-                        + " newOptimisticLockId=" + newOptimisticLockId);
+            try {
+                if (!writePackageStatusWithOptimisticLockCheck(optimisticLockId,
+                        newOptimisticLockId, null /* status */, null /* packageVersions */)) {
+                    throw new IllegalStateException("resetCheckState: Unable to reset package"
+                            + " status, newOptimisticLockId=" + newOptimisticLockId);
+                }
+            } catch (IOException e) {
+                throw new IllegalStateException(e);
             }
         }
     }
@@ -199,138 +233,146 @@ final class PackageStatusStorage {
             int optimisticLockId = checkToken.mOptimisticLockId;
             int newOptimisticLockId = optimisticLockId + 1;
             int status = succeeded ? CHECK_COMPLETED_SUCCESS : CHECK_COMPLETED_FAILURE;
-            return writeStatusRow(optimisticLockId, newOptimisticLockId,
-                    status, checkToken.mPackageVersions);
+            try {
+                return writePackageStatusWithOptimisticLockCheck(optimisticLockId,
+                        newOptimisticLockId, status, checkToken.mPackageVersions);
+            } catch (IOException e) {
+                throw new IllegalStateException(e);
+            }
         }
     }
 
-    // Caller should be synchronized(this)
-    private Integer getCurrentOptimisticLockId() {
-        final String[] columns = { COLUMN_OPTIMISTIC_LOCK_ID };
-        final String querySelection = COLUMN_ID + " = ?";
-        final String[] querySelectionArgs = { Integer.toString(SINGLETON_ID) };
-
-        SQLiteDatabase database = mDatabaseHelper.getReadableDatabase();
-        try (Cursor cursor = database.query(TABLE, columns, querySelection, querySelectionArgs,
-                null /* groupBy */, null /* having */, null /* orderBy */)) {
-            if (cursor.getCount() != 1) {
-                Slog.w(TAG, cursor.getCount() + " rows returned, expected exactly one.");
-                return null;
-            }
-            cursor.moveToFirst();
-            return cursor.getInt(0);
+    // Caller should be synchronized(this).
+    private int getCurrentOptimisticLockId() throws ParseException {
+        try (FileInputStream fis = mPackageStatusFile.openRead()) {
+            XmlPullParser parser = parseToPackageStatusTag(fis);
+            return getIntAttribute(parser, ATTRIBUTE_OPTIMISTIC_LOCK_ID);
+        } catch (IOException e) {
+            ParseException e2 = new ParseException("Unable to read file", 0);
+            e2.initCause(e);
+            throw e2;
         }
     }
 
-    // Caller should be synchronized(this)
-    private boolean writeStatusRow(int optimisticLockId, int newOptimisticLockId, Integer status,
-            PackageVersions packageVersions) {
-        if ((status == null) != (packageVersions == null)) {
-            throw new IllegalArgumentException(
-                    "Provide both status and packageVersions, or neither.");
+    /** Returns a parser or throws ParseException, never returns null. */
+    private static XmlPullParser parseToPackageStatusTag(FileInputStream fis)
+            throws ParseException {
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(fis, StandardCharsets.UTF_8.name());
+            int type;
+            while ((type = parser.next()) != END_DOCUMENT) {
+                final String tag = parser.getName();
+                if (type == START_TAG && TAG_PACKAGE_STATUS.equals(tag)) {
+                    return parser;
+                }
+            }
+            throw new ParseException("Unable to find " + TAG_PACKAGE_STATUS + " tag", 0);
+        } catch (XmlPullParserException e) {
+            throw new IllegalStateException("Unable to configure parser", e);
+        } catch (IOException e) {
+            ParseException e2 = new ParseException("Error reading XML", 0);
+            e.initCause(e);
+            throw e2;
         }
+    }
 
-        SQLiteDatabase database = mDatabaseHelper.getWritableDatabase();
-        ContentValues values = new ContentValues();
-        values.put(COLUMN_OPTIMISTIC_LOCK_ID, newOptimisticLockId);
-        if (status == null) {
-            values.putNull(COLUMN_CHECK_STATUS);
-            values.put(COLUMN_UPDATE_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
-            values.put(COLUMN_DATA_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
-        } else {
-            values.put(COLUMN_CHECK_STATUS, status);
-            values.put(COLUMN_UPDATE_APP_VERSION, packageVersions.mUpdateAppVersion);
-            values.put(COLUMN_DATA_APP_VERSION, packageVersions.mDataAppVersion);
-        }
+    // Caller should be synchronized(this).
+    private boolean writePackageStatusWithOptimisticLockCheck(int optimisticLockId,
+            int newOptimisticLockId, Integer status, PackageVersions packageVersions)
+            throws IOException {
 
-        String updateSelection = COLUMN_ID + " = ? AND " + COLUMN_OPTIMISTIC_LOCK_ID + " = ?";
-        String[] updateSelectionArgs = {
-                Integer.toString(SINGLETON_ID), Integer.toString(optimisticLockId)
-        };
-        int count = database.update(TABLE, values, updateSelection, updateSelectionArgs);
-        if (count > 1) {
-            // This has to be because of corruption: there should only ever be one row.
-            Slog.w(TAG, "writeStatusRow: " + count + " rows updated, expected exactly one.");
-            // Reset the table.
-            mDatabaseHelper.recoverFromBadData();
+        int currentOptimisticLockId;
+        try {
+            currentOptimisticLockId = getCurrentOptimisticLockId();
+            if (currentOptimisticLockId != optimisticLockId) {
+                return false;
+            }
+        } catch (ParseException e) {
+            recoverFromBadData(e);
+            return false;
         }
 
-        // 1 is the success case. 0 rows updated means the row is missing or the optimistic lock ID
-        // was not as expected, this could be because of corruption but is most likely due to an
-        // optimistic lock failure. Callers can decide on a case-by-case basis.
-        return count == 1;
-    }
-
-    /** Only used during tests to force an empty table. */
-    void deleteRowForTests() {
-        mDatabaseHelper.getWritableDatabase().delete(TABLE, null, null);
+        writePackageStatusInternal(status, newOptimisticLockId, packageVersions);
+        return true;
     }
 
-    /** Only used during tests to force a known table state. */
-    public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) {
-        int optimisticLockId = getCurrentOptimisticLockId();
-        writeStatusRow(optimisticLockId, optimisticLockId, checkStatus, packageVersions);
-    }
-
-    static class DatabaseHelper extends SQLiteOpenHelper {
-
-        private final Context mContext;
-
-        public DatabaseHelper(Context context) {
-            super(context, DATABASE_NAME, null, DATABASE_VERSION);
-            mContext = context;
-        }
-
-        @Override
-        public void onCreate(SQLiteDatabase db) {
-            db.execSQL("CREATE TABLE " + TABLE + " (" +
-                    "_id INTEGER PRIMARY KEY," +
-                    COLUMN_OPTIMISTIC_LOCK_ID + " INTEGER NOT NULL," +
-                    COLUMN_CHECK_STATUS + " INTEGER," +
-                    COLUMN_UPDATE_APP_VERSION + " INTEGER NOT NULL," +
-                    COLUMN_DATA_APP_VERSION + " INTEGER NOT NULL" +
-                    ");");
-            insertInitialRowState(db);
+    // Caller should be synchronized(this).
+    private void writePackageStatusInternal(Integer status, int optimisticLockId,
+            PackageVersions packageVersions) throws IOException {
+        if ((status == null) != (packageVersions == null)) {
+            throw new IllegalArgumentException(
+                    "Provide both status and packageVersions, or neither.");
         }
 
-        @Override
-        public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
-            // no-op: nothing to upgrade
+        FileOutputStream fos = null;
+        try {
+            fos = mPackageStatusFile.startWrite();
+            XmlSerializer serializer = new FastXmlSerializer();
+            serializer.setOutput(fos, StandardCharsets.UTF_8.name());
+            serializer.startDocument(null /* encoding */, true /* standalone */);
+            final String namespace = null;
+            serializer.startTag(namespace, TAG_PACKAGE_STATUS);
+            String statusAttributeValue = status == null ? "" : Integer.toString(status);
+            serializer.attribute(namespace, ATTRIBUTE_CHECK_STATUS, statusAttributeValue);
+            serializer.attribute(namespace, ATTRIBUTE_OPTIMISTIC_LOCK_ID,
+                    Integer.toString(optimisticLockId));
+            int updateAppVersion = status == null
+                    ? UNKNOWN_PACKAGE_VERSION : packageVersions.mUpdateAppVersion;
+            serializer.attribute(namespace, ATTRIBUTE_UPDATE_APP_VERSION,
+                    Integer.toString(updateAppVersion));
+            int dataAppVersion = status == null
+                    ? UNKNOWN_PACKAGE_VERSION : packageVersions.mDataAppVersion;
+            serializer.attribute(namespace, ATTRIBUTE_DATA_APP_VERSION,
+                    Integer.toString(dataAppVersion));
+            serializer.endTag(namespace, TAG_PACKAGE_STATUS);
+            serializer.endDocument();
+            serializer.flush();
+            mPackageStatusFile.finishWrite(fos);
+        } catch (IOException e) {
+            if (fos != null) {
+                mPackageStatusFile.failWrite(fos);
+            }
+            throw e;
         }
 
-        /** Recover the initial data row state, returning the new current optimistic lock ID */
-        int recoverFromBadData() {
-            // Delete the table content.
-            SQLiteDatabase writableDatabase = getWritableDatabase();
-            writableDatabase.delete(TABLE, null /* whereClause */, null /* whereArgs */);
+    }
 
-            // Insert the initial content.
-            return insertInitialRowState(writableDatabase);
+    /** Only used during tests to force a known table state. */
+    public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) {
+        synchronized (this) {
+            try {
+                int optimisticLockId = getCurrentOptimisticLockId();
+                writePackageStatusWithOptimisticLockCheck(optimisticLockId, optimisticLockId,
+                        checkStatus, packageVersions);
+            } catch (IOException | ParseException e) {
+                throw new IllegalStateException(e);
+            }
         }
+    }
 
-        /** Insert the initial data row, returning the optimistic lock ID */
-        private static int insertInitialRowState(SQLiteDatabase db) {
-            // Doesn't matter what it is, but we avoid the obvious starting value each time the row
-            // is reset to ensure that old tokens are unlikely to work.
-           final int initialOptimisticLockId = (int) System.currentTimeMillis();
-
-            // Insert the one row.
-            ContentValues values = new ContentValues();
-            values.put(COLUMN_ID, SINGLETON_ID);
-            values.put(COLUMN_OPTIMISTIC_LOCK_ID, initialOptimisticLockId);
-            values.putNull(COLUMN_CHECK_STATUS);
-            values.put(COLUMN_UPDATE_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
-            values.put(COLUMN_DATA_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
-            long id = db.insert(TABLE, null, values);
-            if (id == -1) {
-                Slog.w(TAG, "insertInitialRow: could not insert initial row, id=" + id);
-                return -1;
+    private static Integer getNullableIntAttribute(XmlPullParser parser, String attributeName)
+            throws ParseException {
+        String attributeValue = parser.getAttributeValue(null, attributeName);
+        try {
+            if (attributeValue == null) {
+                throw new ParseException("Attribute " + attributeName + " missing", 0);
+            } else if (attributeValue.isEmpty()) {
+                return null;
             }
-            return initialOptimisticLockId;
+            return Integer.parseInt(attributeValue);
+        } catch (NumberFormatException e) {
+            throw new ParseException(
+                    "Bad integer for attributeName=" + attributeName + ": " + attributeValue, 0);
         }
+    }
 
-        File getDatabaseFile() {
-            return mContext.getDatabasePath(DATABASE_NAME);
+    private static int getIntAttribute(XmlPullParser parser, String attributeName)
+            throws ParseException {
+        Integer value = getNullableIntAttribute(parser, attributeName);
+        if (value == null) {
+            throw new ParseException("Missing attribute " + attributeName, 0);
         }
+        return value;
     }
 }
index 8abf7df..f9af2ea 100644 (file)
@@ -21,9 +21,12 @@ import com.android.internal.annotations.VisibleForTesting;
 import android.app.timezone.RulesUpdaterContract;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.os.Environment;
 import android.provider.TimeZoneRulesDataContract;
 import android.util.Slog;
 
+import java.io.File;
+
 /**
  * Monitors the installed applications associated with time zone updates. If the app packages are
  * updated it indicates there <em>might</em> be a time zone rules update to apply so a targeted
@@ -81,11 +84,17 @@ public class PackageTracker implements IntentHelper.Listener {
     /** Creates the {@link PackageTracker} for normal use. */
     static PackageTracker create(Context context) {
         PackageTrackerHelperImpl helperImpl = new PackageTrackerHelperImpl(context);
+        // TODO(nfuller): Switch to FileUtils.createDir() when available. http://b/31008728
+        File storageDir = new File(Environment.getDataSystemDirectory(), "timezone");
+        if (!storageDir.exists()) {
+            storageDir.mkdir();
+        }
+
         return new PackageTracker(
                 helperImpl /* clock */,
                 helperImpl /* configHelper */,
                 helperImpl /* packageManagerHelper */,
-                new PackageStatusStorage(context),
+                new PackageStatusStorage(storageDir),
                 new IntentHelperImpl(context));
     }
 
index e085270..dd56072 100644 (file)
@@ -21,10 +21,11 @@ import org.junit.Before;
 import org.junit.Test;
 
 import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 
+import java.io.File;
+
 import static junit.framework.Assert.assertTrue;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -40,15 +41,16 @@ public class PackageStatusStorageTest {
     @Before
     public void setUp() throws Exception {
         Context context = InstrumentationRegistry.getContext();
+        File dataDir = context.getFilesDir();
 
         // Using the instrumentation context means the database is created in a test app-specific
         // directory.
-        mPackageStatusStorage = new PackageStatusStorage(context);
+        mPackageStatusStorage = new PackageStatusStorage(dataDir);
     }
 
     @After
     public void tearDown() throws Exception {
-        mPackageStatusStorage.deleteDatabaseForTests();
+        mPackageStatusStorage.deleteFileForTests();
     }
 
     @Test
@@ -90,7 +92,7 @@ public class PackageStatusStorageTest {
     }
 
     @Test
-    public void generateCheckToken_missingRowBehavior() {
+    public void generateCheckToken_missingFileBehavior() {
         // Assert initial state.
         assertNull(mPackageStatusStorage.getPackageStatus());
 
@@ -100,15 +102,15 @@ public class PackageStatusStorageTest {
         // There should now be state.
         assertNotNull(mPackageStatusStorage.getPackageStatus());
 
-        // Corrupt the table by removing the one row.
-        mPackageStatusStorage.deleteRowForTests();
+        // Corrupt the data by removing the file.
+        mPackageStatusStorage.deleteFileForTests();
 
         // Check that generateCheckToken recovers.
         assertNotNull(mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS));
     }
 
     @Test
-    public void getPackageStatus_missingRowBehavior() {
+    public void getPackageStatus_missingFileBehavior() {
         // Assert initial state.
         assertNull(mPackageStatusStorage.getPackageStatus());
 
@@ -118,14 +120,14 @@ public class PackageStatusStorageTest {
         // There should now be a state.
         assertNotNull(mPackageStatusStorage.getPackageStatus());
 
-        // Corrupt the table by removing the one row.
-        mPackageStatusStorage.deleteRowForTests();
+        // Corrupt the data by removing the file.
+        mPackageStatusStorage.deleteFileForTests();
 
         assertNull(mPackageStatusStorage.getPackageStatus());
     }
 
     @Test
-    public void markChecked_missingRowBehavior() {
+    public void markChecked_missingFileBehavior() {
         // Assert initial state.
         CheckToken token1 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS);
         assertNotNull(token1);
@@ -133,10 +135,10 @@ public class PackageStatusStorageTest {
         // There should now be a state.
         assertNotNull(mPackageStatusStorage.getPackageStatus());
 
-        // Corrupt the table by removing the one row.
-        mPackageStatusStorage.deleteRowForTests();
+        // Corrupt the data by removing the file.
+        mPackageStatusStorage.deleteFileForTests();
 
-        // The missing row should mean token1 is now considered invalid, so we should get a false.
+        // The missing file should mean token1 is now considered invalid, so we should get a false.
         assertFalse(mPackageStatusStorage.markChecked(token1, true /* succeeded */));
 
         // The storage should have recovered and we should be able to carry on like before.
index 45b0af3..4c7680b 100644 (file)
@@ -71,7 +71,7 @@ public class PackageTrackerTest {
 
         // Using the instrumentation context means the database is created in a test app-specific
         // directory. We can use the real thing for this test.
-        mPackageStatusStorage = new PackageStatusStorage(context);
+        mPackageStatusStorage = new PackageStatusStorage(context.getFilesDir());
 
         // For other interactions with the Android framework we create a fake object.
         mFakeIntentHelper = new FakeIntentHelper();
@@ -88,7 +88,7 @@ public class PackageTrackerTest {
     @After
     public void tearDown() throws Exception {
         if (mPackageStatusStorage != null) {
-            mPackageStatusStorage.deleteDatabaseForTests();
+            mPackageStatusStorage.deleteFileForTests();
         }
     }