From 4991c5c05b98e426614de5d3f23a5e39d6384aa7 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sun, 31 Jul 2016 12:01:48 +0200 Subject: [PATCH] Implement DataHolder compatible with old public API --- .../android/gms/common/data/DataHolder.java | 455 ++++++++++++++++-- 1 file changed, 426 insertions(+), 29 deletions(-) diff --git a/play-services-api/src/main/java/com/google/android/gms/common/data/DataHolder.java b/play-services-api/src/main/java/com/google/android/gms/common/data/DataHolder.java index bd1efdcb..51a4a602 100644 --- a/play-services-api/src/main/java/com/google/android/gms/common/data/DataHolder.java +++ b/play-services-api/src/main/java/com/google/android/gms/common/data/DataHolder.java @@ -17,39 +17,57 @@ package com.google.android.gms.common.data; import android.annotation.SuppressLint; +import android.content.ContentValues; import android.database.AbstractWindowedCursor; +import android.database.CharArrayBuffer; import android.database.Cursor; import android.database.CursorWindow; +import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Parcel; +import org.microg.gms.common.PublicApi; import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; +import java.io.Closeable; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Class for accessing collections of data, organized into columns. This provides the backing * support for DataBuffer. Much like a cursor, the holder supports the notion of a current * position, and has methods for extracting various types of data from named columns. */ -public class DataHolder extends AutoSafeParcelable { +@PublicApi(until = "1") +public class DataHolder extends AutoSafeParcelable implements Closeable { @SafeParceled(1000) private int versionCode = 1; @SafeParceled(1) - public final String[] columns; + private final String[] columns; @SafeParceled(2) - public final CursorWindow[] windows; + private final CursorWindow[] windows; @SafeParceled(3) - public final int statusCode; + private final int statusCode; @SafeParceled(4) - public final Bundle metadata; + private final Bundle metadata; + + private boolean closed = false; + private Map columnIndizes; + + protected static final int FIELD_TYPE_NULL = 0; + protected static final int FIELD_TYPE_INTEGER = 1; + protected static final int FIELD_TYPE_FLOAT = 2; + protected static final int FIELD_TYPE_STRING = 3; + protected static final int FIELD_TYPE_BLOB = 4; private DataHolder() { columns = null; @@ -58,18 +76,81 @@ public class DataHolder extends AutoSafeParcelable { metadata = null; } + /** + * Creates a data holder with the specified data. + * + * @param columns The column names corresponding to the data in the given windows. + * @param windows The {@link CursorWindow} instances holding the data. + * @param statusCode The status code of this {@link DataHolder}. + * @param metadata The metadata associated with this {@link DataHolder} (may be null). + */ public DataHolder(String[] columns, CursorWindow[] windows, int statusCode, Bundle metadata) { this.columns = columns; this.windows = windows; this.statusCode = statusCode; this.metadata = metadata; + validateContents(); } - protected static final int FIELD_TYPE_BLOB = 4; - protected static final int FIELD_TYPE_FLOAT = 2; - protected static final int FIELD_TYPE_INTEGER = 1; - protected static final int FIELD_TYPE_NULL = 0; - protected static final int FIELD_TYPE_STRING = 3; + /** + * Creates a data holder wrapping the provided cursor, with provided status code and metadata. + * + * @param cursor The cursor containing the data. + * @param statusCode The status code of this {@link DataHolder}. + * @param metadata The metadata associated with this {@link DataHolder} (may be null). + */ + public DataHolder(AbstractWindowedCursor cursor, int statusCode, Bundle metadata) { + this(cursor.getColumnNames(), createCursorWindows(cursor), statusCode, metadata); + } + + /** + * Creates a data holder wrapping the provided cursor, with provided status code and metadata. + * + * @param cursor The cursor containing the data. + * @param statusCode The status code of this {@link DataHolder}. + * @param metadata The metadata associated with this {@link DataHolder} (may be null). + */ + public DataHolder(Cursor cursor, int statusCode, Bundle metadata) { + this(cursor.getColumnNames(), createCursorWindows(cursor), statusCode, metadata); + } + + /** + * Get a {@link DataHolder.Builder} to create a new {@link DataHolder} manually. + * + * @param columns The array of column names that the object supports. + * @param uniqueColumn The non-null column name that must contain unique values. New rows added to the builder with the same value in this column will replace any older rows. + * @return {@link DataHolder.Builder} object to work with. + */ + public static Builder builder(String[] columns, String uniqueColumn) { + return new Builder(columns, uniqueColumn); + } + + /** + * Get a {@link DataHolder.Builder} to create a new {@link DataHolder} manually. + * + * @param columns The array of column names that the object supports. + * @return {@link DataHolder.Builder} object to work with. + */ + public static Builder builder(String[] columns) { + return builder(columns, null); + } + + /** + * @param statusCode The status code of this {@link DataHolder}. + * @param metadata The metadata associated with this {@link DataHolder} (may be null). + * @return An empty {@link DataHolder} object with the given status and metadata. + */ + public static DataHolder empty(int statusCode, Bundle metadata) { + return new DataHolder(new String[0], new CursorWindow[0], statusCode, metadata); + } + + /** + * @param statusCode The status code of this {@link DataHolder}. + * @return An empty {@link DataHolder} object with the given status and null metadata. + */ + public static DataHolder empty(int statusCode) { + return empty(statusCode, null); + } @SuppressWarnings("deprecation") @SuppressLint("NewApi") @@ -98,46 +179,166 @@ public class DataHolder extends AutoSafeParcelable { throw new RuntimeException("Unsupported cursor on this platform!"); } - public static DataHolder fromCursor(Cursor cursor, int statusCode, Bundle metadata) { + /** + * Closes the data holder, releasing all of its resources and making it completely invalid. + */ + @Override + public void close() { + synchronized (this) { + if (!closed) { + closed = true; + for (CursorWindow window : windows) { + window.close(); + } + } + } + } + + /** + * Copies the String content in the given column at the provided position into a {@link CharArrayBuffer}. + * This will throw an {@link IllegalArgumentException} if the column does not exist, the + * position is invalid, or the data holder has been closed. + * + * @param column The column to retrieve. + * @param row The row to retrieve the data from. + * @param windowIndex Index of the cursor window to extract the data from. + * @param dataOut The {@link CharArrayBuffer} to copy into. + */ + public void copyToBuffer(String column, int row, int windowIndex, CharArrayBuffer dataOut) { + throw new RuntimeException("Not yet available"); + } + + @SuppressWarnings("deprecation") + private static CursorWindow[] createCursorWindows(Builder builder) { + if (builder.columns.length == 0) return new CursorWindow[0]; List windows = new ArrayList(); - CursorWindow cursorWindow = null; - int row = 0; + try { + CursorWindow current = null; + for (int rowIndex = 0; rowIndex < builder.rows.size(); rowIndex++) { + Map row = builder.rows.get(rowIndex); + if (current == null || !current.allocRow()) { + current = new CursorWindow(false); + current.setStartPosition(rowIndex); + current.setNumColumns(builder.columns.length); + windows.add(current); + if (!current.allocRow()) { + windows.remove(current); + return windows.toArray(new CursorWindow[windows.size()]); + } + } + for (int columnIndex = 0; columnIndex < builder.columns.length; columnIndex++) { + Object val = row.get(builder.columns[columnIndex]); + if (val == null) { + current.putNull(rowIndex, columnIndex); + } else if (val instanceof String) { + current.putString((String) val, rowIndex, columnIndex); + } else if (val instanceof Long) { + current.putLong((Long) val, rowIndex, columnIndex); + } else if (val instanceof Integer) { + current.putLong((Integer) val, rowIndex, columnIndex); + } else if (val instanceof Boolean) { + if ((Boolean) val) + current.putLong(1, rowIndex, columnIndex); + } else if (val instanceof byte[]) { + current.putBlob((byte[]) val, rowIndex, columnIndex); + } else if (val instanceof Double) { + current.putDouble((Double) val, rowIndex, columnIndex); + } else if (val instanceof Float) { + current.putDouble((Float) val, rowIndex, columnIndex); + } else { + throw new IllegalArgumentException("Unsupported object for column " + columnIndex + ": " + val); + } + } + } + } catch (RuntimeException e) { + for (CursorWindow window : windows) { + window.close(); + } + throw e; + } + return windows.toArray(new CursorWindow[windows.size()]); + } + + private static CursorWindow[] createCursorWindows(Cursor cursor) { + if (cursor.getColumnCount() == 0) return new CursorWindow[0]; + List windows = new ArrayList(); + CursorWindow current = null; + int rowIndex = 0; while (cursor.moveToNext()) { - if (cursorWindow == null || !cursorWindow.allocRow()) { - cursorWindow = new CursorWindow(false); - cursorWindow.setNumColumns(cursor.getColumnCount()); - windows.add(cursorWindow); - if (!cursorWindow.allocRow()) - throw new RuntimeException("Impossible to store Cursor in CursorWindows"); - row = 0; + if (current == null || !current.allocRow()) { + current = new CursorWindow(false); + current.setStartPosition(rowIndex); + current.setNumColumns(cursor.getColumnCount()); + windows.add(current); + if (!current.allocRow()) { + windows.remove(current); + return windows.toArray(new CursorWindow[windows.size()]); + } } for (int i = 0; i < cursor.getColumnCount(); i++) { switch (getCursorType(cursor, i)) { case FIELD_TYPE_NULL: - cursorWindow.putNull(row, i); + current.putNull(rowIndex, i); break; case FIELD_TYPE_BLOB: - cursorWindow.putBlob(cursor.getBlob(i), row, i); + current.putBlob(cursor.getBlob(i), rowIndex, i); break; case FIELD_TYPE_FLOAT: - cursorWindow.putDouble(cursor.getDouble(i), row, i); + current.putDouble(cursor.getDouble(i), rowIndex, i); break; case FIELD_TYPE_INTEGER: - cursorWindow.putLong(cursor.getLong(i), row, i); + current.putLong(cursor.getLong(i), rowIndex, i); break; case FIELD_TYPE_STRING: - cursorWindow.putString(cursor.getString(i), row, i); + current.putString(cursor.getString(i), rowIndex, i); break; } } - row++; + rowIndex++; } - DataHolder dataHolder = new DataHolder(cursor.getColumnNames(), windows.toArray(new CursorWindow[windows.size()]), statusCode, metadata); cursor.close(); - return dataHolder; + return windows.toArray(new CursorWindow[windows.size()]); } + @PublicApi(exclude = true) + @Deprecated + public static DataHolder fromCursor(Cursor cursor, int statusCode, Bundle metadata) { + return new DataHolder(cursor, statusCode, metadata); + } + /** + * Retrieves the boolean value for a given column at the provided position. + * This will throw an {@link IllegalArgumentException} if the column does not exist, the + * position is invalid, or the data holder has been closed. + * + * @param column The column to retrieve. + * @param row The row to retrieve the data from. + * @param windowIndex Index of the cursor window to extract the data from. + * @return The boolean value in that column. + */ + public boolean getBoolean(String column, int row, int windowIndex) { + return windows[windowIndex].getLong(row, columnIndizes.get(column)) == 1; + } + + /** + * Retrieves the byte array value for a given column at the provided position. + * This will throw an {@link IllegalArgumentException} if the column does not exist, the + * position is invalid, or the data holder has been closed. + * + * @param column The column to retrieve. + * @param row The row to retrieve the data from. + * @param windowIndex Index of the cursor window to extract the data from. + * @return The byte array value in that column. + */ + public byte[] getByteArray(String column, int row, int windowIndex) { + return windows[windowIndex].getBlob(row, columnIndizes.get(column)); + } + + /** + * Gets the number of rows in the data holder. + * + * @return the number of rows in the data holder. + */ public int getCount() { int c = 0; if (windows != null) { @@ -148,6 +349,88 @@ public class DataHolder extends AutoSafeParcelable { return c; } + /** + * Retrieves the integer value for a given column at the provided position. + * This will throw an {@link IllegalArgumentException} if the column does not exist, the + * position is invalid, or the data holder has been closed. + * + * @param column The column to retrieve. + * @param row The row to retrieve the data from. + * @param windowIndex Index of the cursor window to extract the data from. + * @return The integer value in that column. + */ + public int getInteger(String column, int row, int windowIndex) { + return windows[windowIndex].getInt(row, columnIndizes.get(column)); + } + + /** + * Retrieves the long value for a given column at the provided position. + * This will throw an {@link IllegalArgumentException} if the column does not exist, the + * position is invalid, or the data holder has been closed. + * + * @param column The column to retrieve. + * @param row The row to retrieve the data from. + * @param windowIndex Index of the cursor window to extract the data from. + * @return The long value in that column. + */ + public long getLong(String column, int row, int windowIndex) { + return windows[windowIndex].getLong(row, columnIndizes.get(column)); + } + + public int getStatusCode() { + return statusCode; + } + + /** + * Retrieves the string value for a given column at the provided position. + * This will throw an {@link IllegalArgumentException} if the column does not exist, the + * position is invalid, or the data holder has been closed. + * + * @param column The column to retrieve. + * @param row The row to retrieve the data from. + * @param windowIndex Index of the cursor window to extract the data from. + * @return The string value in that column. + */ + public String getString(String column, int row, int windowIndex) { + return windows[windowIndex].getString(row, columnIndizes.get(column)); + } + + /** + * Returns whether the given column at the provided position contains null. + * This will throw an {@link IllegalArgumentException} if the column does not exist, the + * position is invalid, or the data holder has been closed. + * + * @param column The column to retrieve. + * @param row The row to retrieve the data from. + * @param windowIndex Index of the cursor window to extract the data from. + * @return Whether the column value is null at this position. + */ + public boolean isNull(String column, int row, int windowIndex) { + return windows[windowIndex].isNull(row, columnIndizes.get(column)); + } + + public boolean isClosed() { + synchronized (this) { + return closed; + } + } + + /** + * Retrieves the column data at the provided position as a URI if possible, checking for null values. + * This will throw an {@link IllegalArgumentException} if the column does not exist, the + * position is invalid, or the data holder has been closed. + * + * @param column The column to retrieve. + * @param row The row to retrieve the data from. + * @param windowIndex Index of the cursor window to extract the data from. + * @return The column data as a URI, or null if not present. + */ + public Uri parseUri(String column, int row, int windowIndex) { + String string = getString(column, row, windowIndex); + if (string != null) return Uri.parse(string); + return null; + } + @Override public String toString() { return "DataHolder{" + @@ -158,5 +441,119 @@ public class DataHolder extends AutoSafeParcelable { '}'; } - public static final Creator CREATOR = new AutoCreator(DataHolder.class); + public void validateContents() { + columnIndizes = new HashMap(); + for (int i = 0; i < columns.length; i++) { + columnIndizes.put(columns[i], i); + } + } + + /** + * Helper class to build {@link DataHolder} instances containing arbitrary data. + *

+ * Note that the constructor is private; use DataHolder.builder() to create instances of this class. + */ + public static class Builder { + private final String[] columns; + private final ArrayList> rows; + private final String uniqueColumn; + private final Map uniqueIndizes; + + private Builder(String[] columns, String uniqueColumn) { + this.columns = columns; + this.rows = new ArrayList>(); + this.uniqueColumn = uniqueColumn; + this.uniqueIndizes = new HashMap(); + } + + /** + * Instantiate an {@link DataHolder} from this {@link DataHolder.Builder} with the given status code and metadata. + * + * @param statusCode The status code of this {@link DataHolder}. + * @param metadata The metadata associated with this {@link DataHolder} (may be null). + * @return {@link DataHolder} representation of this object. + */ + public DataHolder build(int statusCode, Bundle metadata) { + return new DataHolder(columns, createCursorWindows(this), statusCode, metadata); + } + + /** + * Instantiate an {@link DataHolder} from this {@link DataHolder.Builder} with the given status code and null metadata. + * + * @param statusCode The status code of this {@link DataHolder}. + * @return {@link DataHolder} representation of this object. + */ + public DataHolder build(int statusCode) { + return build(statusCode, null); + } + + /** + * @return The number of rows that the resulting DataHolder will contain. + */ + public int getCount() { + return rows.size(); + } + + /** + * Sort the rows in this builder based on the standard data type comparisons for the value in the provided column. + * Calling this multiple times with the same column will not change the sort order of the builder. + * Note that any data which is added after this call will not be sorted. + * + * @param sortColumn The column to sort the rows in this builder by. + * @return {@link DataHolder.Builder} to continue construction. + */ + public Builder sort(String sortColumn) { + throw new RuntimeException("Not yet implemented"); + } + + /** + * Add a new row of data to the {@link DataHolder} this {@link DataHolder.Builder} will create. Note that the data must contain an entry for all columns + *

+ * Currently the only supported value types that are supported are String, Long, and Boolean (Integer is also accepted and will be stored as a Long). + * + * @param values {@link ContentValues} containing row data. + * @return {@link DataHolder.Builder} to continue construction. + */ + public Builder withRow(ContentValues values) { + HashMap row = new HashMap(); + for (Map.Entry entry : values.valueSet()) { + row.put(entry.getKey(), entry.getValue()); + } + return withRow(row); + } + + /** + * Add a new row of data to the {@link DataHolder} this {@link DataHolder.Builder} will create. Note that the data must contain an entry for all columns + *

+ * Currently the only supported value types that are supported are String, Long, and Boolean (Integer is also accepted and will be stored as a Long). + * + * @param row Map containing row data. + * @return {@link DataHolder.Builder} to continue construction. + */ + public Builder withRow(HashMap row) { + if (uniqueColumn != null) { + Object val = row.get(uniqueColumn); + if (val != null) { + Integer old = uniqueIndizes.get(val); + if (old != null) { + rows.set(old, row); + return this; + } else { + uniqueIndizes.put(val, rows.size()); + } + } + } + rows.add(row); + return this; + } + } + + public static final Creator CREATOR = new AutoCreator(DataHolder.class) { + @Override + public DataHolder createFromParcel(Parcel parcel) { + DataHolder res = super.createFromParcel(parcel); + res.validateContents(); + return res; + } + }; }