package org.wikipedia.database.contract; import android.net.Uri; import android.support.annotation.NonNull; import android.support.v4.util.ArraySet; import org.wikipedia.database.DbUtil; import org.wikipedia.database.column.CodeEnumColumn; import org.wikipedia.database.column.CsvColumn; import org.wikipedia.database.column.IdColumn; import org.wikipedia.database.column.IntColumn; import org.wikipedia.database.column.LongColumn; import org.wikipedia.database.column.NamespaceColumn; import org.wikipedia.database.column.StrColumn; import org.wikipedia.database.http.HttpColumns; import org.wikipedia.database.http.HttpStatus; import org.wikipedia.readinglist.page.ReadingListPageRow; import org.wikipedia.readinglist.page.database.disk.DiskColumns; import org.wikipedia.readinglist.page.database.disk.DiskStatus; import java.util.Collection; import java.util.Set; @SuppressWarnings("checkstyle:interfaceistype") public final class ReadingListPageContract { public static final String TABLE_PAGE = "readinglistpage"; public static final String TABLE_HTTP = "readinglistpagehttp"; public static final String TABLE_DISK = "readinglistpagedisk"; private static final String PATH = "readinglist"; public interface PageCol { IdColumn ID = new IdColumn(TABLE_PAGE); StrColumn KEY = new StrColumn(TABLE_PAGE, "key", "text not null unique"); CsvColumn<Set<String>> LIST_KEYS = new CsvColumn<Set<String>>(TABLE_PAGE, "listKeys", "text not null") { @NonNull @Override protected Set<String> val(@NonNull Collection<String> strs) { return new ArraySet<>(strs); } @NonNull @Override protected Collection<String> put(@NonNull Set<String> row) { return row; } }; StrColumn SITE = new StrColumn(TABLE_PAGE, "site", "text not null"); // TODO: should null (autoselect system language) be allowed? It might be more meaningful to // force clients to store the nonnull active system language instead. This // implementation matches that used by other tables. A change made here should be made // elsewhere too. StrColumn LANG = new StrColumn(TABLE_PAGE, "lang", "text"); NamespaceColumn NAMESPACE = new NamespaceColumn(TABLE_PAGE, "namespace"); StrColumn TITLE = new StrColumn(TABLE_PAGE, "title", "text not null"); // TODO: use page ID not title. Page IDs are a concise and canonical way to uniquely refer // to a page but there are many places where we do not preserve the ID. //IntColumn PAGE_ID = new IntColumn(TABLE_PAGE, "pageId", "integer not null"); IntColumn DISK_PAGE_REV = new IntColumn(TABLE_PAGE, "diskPageRev", "integer"); LongColumn MTIME = new LongColumn(TABLE_PAGE, "mtime", "integer not null"); LongColumn ATIME = new LongColumn(TABLE_PAGE, "atime", "integer not null"); StrColumn THUMBNAIL_URL = new StrColumn(TABLE_PAGE, "thumbnailUrl", "text"); StrColumn DESCRIPTION = new StrColumn(TABLE_PAGE, "description", "text"); // The cumulative size in bytes for an offline page and all page resources downloaded by // SavedPageSyncService. Null or 0 if DiskStatus.ONLINE, not yet downloaded, or not yet // downloaded since these columns were added. Outdated if the saved page cache size is later // exceeded and resources are evicted. Written to by SavedPageSyncService. // Android appears to present the user with logical size in app settings so it is the // preferred metric to display and physical size will likely never be used. Since quantities // are aggregated across files, neither can be derived from the other. // wc -c /data/data/org.wikipedia.dev/files/okhttp-cache/*.[0-9]|tail -n1 // stat -c %s /data/data/org.wikipedia.dev/files/okhttp-cache/*.[0-9] LongColumn PHYSICAL_SIZE = new LongColumn(TABLE_PAGE, "physicalSize", "integer"); // du -c /data/data/org.wikipedia.dev/files/okhttp-cache/*.[0-9]|tail -n1 // Block size: stat -c %B /data/data/org.wikipedia.dev/files/okhttp-cache LongColumn LOGICAL_SIZE = new LongColumn(TABLE_PAGE, "logicalSize", "integer"); // Example: // 1 Download the Obama article. // - Physical size recorded by us is 5 729 692 bytes. // - Logical size recorded by us is 6 754 304 bytes (6 596 kibibytes). // 2 Terminate the app and check the sizes (note: journal size is never included): // - Physical: wc -c /data/data/org.wikipedia.dev/files/okhttp-cache/*.[0-9]|tail -n1 => 5 729 692 bytes. // - Logical: du -c /data/data/org.wikipedia.dev/files/okhttp-cache/*.[0-9]|tail -n1 => 6 596 kibibytes. // - The size of "data" is about 6 868 kibibytes (6.7070313 mebibytes): // - Calculate the size of all data: du -c /data/data/org.wikipedia.dev|tail -n1 => 13 736 kibibytes. // - Subtract the size of the cache: du -c /data/data/org.wikipedia.dev/cache|tail -n1 => 6 868 kibibytes. // 3 Open settings: data size is 6.71 mebibytes. // 4 Dump the database records: sqlite3 /data/data/org.wikipedia.dev/databases/wikipedia.db '.dump readinglistpage' String[] SELECTION = DbUtil.qualifiedNames(KEY); String[] ALL = DbUtil.qualifiedNames(ID, KEY, LIST_KEYS, SITE, LANG, NAMESPACE, TITLE, DISK_PAGE_REV, MTIME, ATIME, THUMBNAIL_URL, DESCRIPTION, PHYSICAL_SIZE, LOGICAL_SIZE); String[] CONTENT = DbUtil.qualifiedNames(KEY, LIST_KEYS, SITE, LANG, NAMESPACE, TITLE, DISK_PAGE_REV, MTIME, ATIME, THUMBNAIL_URL, DESCRIPTION, PHYSICAL_SIZE, LOGICAL_SIZE); } public static final HttpColumns<ReadingListPageRow> HTTP_COLS = new HttpColumns<>(TABLE_HTTP); public interface HttpCol { IdColumn ID = HTTP_COLS.id(); StrColumn KEY = HTTP_COLS.key(); CodeEnumColumn<HttpStatus> STATUS = HTTP_COLS.status(); LongColumn TIMESTAMP = HTTP_COLS.timestamp(); LongColumn TRANSACTION_ID = HTTP_COLS.transactionId(); String[] SELECTION = HTTP_COLS.selection(); String[] CONTENT = HTTP_COLS.content(); } public static final DiskColumns<ReadingListPageRow> DISK_COLS = new DiskColumns<>(TABLE_DISK); public static class DiskCol { public static final IdColumn ID = DISK_COLS.id(); public static final StrColumn KEY = DISK_COLS.key(); public static final CodeEnumColumn<DiskStatus> STATUS = DISK_COLS.status(); public static final LongColumn TIMESTAMP = DISK_COLS.timestamp(); public static final LongColumn TRANSACTION_ID = DISK_COLS.transactionId(); public static final StrColumn FILENAME = new StrColumn(TABLE_DISK, "filename", "text"); public static final String[] SELECTION = DISK_COLS.selection(); public static final String[] CONTENT; static { CONTENT = new String[DISK_COLS.content().length + 1]; System.arraycopy(DISK_COLS.content(), 0, CONTENT, 0, DISK_COLS.content().length); CONTENT[DISK_COLS.content().length] = FILENAME.qualifiedName(); } } public interface Page extends PageCol { String TABLES = TABLE_PAGE; String PATH = ReadingListPageContract.PATH + "/page"; Uri URI = Uri.withAppendedPath(AppContentProviderContract.AUTHORITY_BASE, PATH); String[] PROJECTION = null; String ORDER_MRU = ":atimeCol desc".replaceAll(":atimeCol", ATIME.qualifiedName()); String ORDER_ALPHABETICAL = ":titleCol asc".replaceAll(":titleCol", TITLE.qualifiedName()); } public interface Http extends HttpCol { String TABLES = TABLE_HTTP; // HACK: Http has no real dependency on Option. However, HttpWithOption is a composite of // Option and Http and observers expect to be notified when _either_ change. Making // this path hierarchical allows HttpWithOption to also be hierarchical but needlessly // notifies Http clients when Option changes. More here: // - http://chalup.github.io/blog/2014/09/14/contentprovider-series-uris/ // - https://gist.github.com/chalup/4201307da02b9cfe4f40 String PATH = Page.PATH + "/http"; Uri URI = Uri.withAppendedPath(AppContentProviderContract.AUTHORITY_BASE, PATH); String[] PROJECTION = null; } public static final class Disk extends DiskCol { public static final String TABLES = TABLE_DISK; // HACK: Http has no real dependency on Option. However, HttpWithOption is a composite of // Option and Http and observers expect to be notified when _either_ change. Making // this path hierarchical allows HttpWithOption to also be hierarchical but needlessly // notifies Http clients when Option changes. More here: // - http://chalup.github.io/blog/2014/09/14/contentprovider-series-uris/ // - https://gist.github.com/chalup/4201307da02b9cfe4f40 public static final String PATH = Page.PATH + "/disk"; public static final Uri URI = Uri.withAppendedPath(AppContentProviderContract.AUTHORITY_BASE, PATH); public static final String[] PROJECTION = null; private Disk() { } } public static final class HttpWithPage implements Page { public static final String TABLES = ":httpTbl left join :tbl on (:tbl.keyCol = :httpTbl.keyCol)" .replaceAll(":tbl.keyCol", KEY.qualifiedName()) .replaceAll(":httpTbl.keyCol", HttpCol.KEY.qualifiedName()) .replaceAll(":httpTbl", TABLE_HTTP) .replaceAll(":tbl", TABLE_PAGE); public static final String PATH = Http.PATH + "/with_http"; public static final Uri URI = Uri.withAppendedPath(AppContentProviderContract.AUTHORITY_BASE, PATH); public static final StrColumn HTTP_KEY = HttpCol.KEY; public static final CodeEnumColumn<HttpStatus> HTTP_STATUS = HttpCol.STATUS; public static final LongColumn HTTP_TIMESTAMP = HttpCol.TIMESTAMP; public static final LongColumn HTTP_TRANSACTION_ID = HttpCol.TRANSACTION_ID; public static final String[] PROJECTION; static { PROJECTION = new String[ALL.length + HttpCol.CONTENT.length]; System.arraycopy(ALL, 0, PROJECTION, 0, ALL.length); System.arraycopy(HttpCol.CONTENT, 0, PROJECTION, ALL.length, HttpCol.CONTENT.length); } private HttpWithPage() { } } public static final class DiskWithPage implements Page { public static final String TABLES = ":diskTbl left join :tbl on (:tbl.keyCol = :diskTbl.keyCol)" .replaceAll(":tbl.keyCol", KEY.qualifiedName()) .replaceAll(":diskTbl.keyCol", DiskCol.KEY.qualifiedName()) .replaceAll(":diskTbl", TABLE_DISK) .replaceAll(":tbl", TABLE_PAGE); public static final String PATH = Disk.PATH + "/with_disk"; public static final Uri URI = Uri.withAppendedPath(AppContentProviderContract.AUTHORITY_BASE, PATH); public static final StrColumn DISK_KEY = DiskCol.KEY; public static final CodeEnumColumn<DiskStatus> DISK_STATUS = DiskCol.STATUS; public static final LongColumn DISK_TIMESTAMP = DiskCol.TIMESTAMP; public static final LongColumn DISK_TRANSACTION_ID = DiskCol.TRANSACTION_ID; public static final StrColumn DISK_FILENAME = DiskCol.FILENAME; public static final String[] PROJECTION; static { PROJECTION = new String[ALL.length + DiskCol.CONTENT.length]; System.arraycopy(ALL, 0, PROJECTION, 0, ALL.length); System.arraycopy(DiskCol.CONTENT, 0, PROJECTION, ALL.length, DiskCol.CONTENT.length); } private DiskWithPage() { } } public static final class PageWithDisk implements Page { public static final String TABLES = ":tbl join :diskTbl on (:tbl.keyCol = :diskTbl.keyCol)" .replaceAll(":tbl.keyCol", KEY.qualifiedName()) .replaceAll(":diskTbl.keyCol", DiskCol.KEY.qualifiedName()) .replaceAll(":diskTbl", TABLE_DISK) .replaceAll(":tbl", TABLE_PAGE); public static final String PATH = Disk.PATH + "/with_page"; public static final Uri URI = Uri.withAppendedPath(AppContentProviderContract.AUTHORITY_BASE, PATH); public static final StrColumn DISK_KEY = DiskCol.KEY; public static final CodeEnumColumn<DiskStatus> DISK_STATUS = DiskCol.STATUS; public static final LongColumn DISK_TIMESTAMP = DiskCol.TIMESTAMP; public static final LongColumn DISK_TRANSACTION_ID = DiskCol.TRANSACTION_ID; public static final StrColumn DISK_FILENAME = DiskCol.FILENAME; public static final String[] PROJECTION; static { PROJECTION = new String[ALL.length + DiskCol.CONTENT.length]; System.arraycopy(ALL, 0, PROJECTION, 0, ALL.length); System.arraycopy(DiskCol.CONTENT, 0, PROJECTION, ALL.length, DiskCol.CONTENT.length); } private PageWithDisk() { } } private ReadingListPageContract() { } }