Cursor.getType() for API Level <11
Asked Answered
G

4

24

I'm querying the CallLog content provider and need to detect the column types.

In Honeycomb and newer (API Level 11+) you can get a columns preferred data type by calling the method Cursor.getType(int columnIndex) which returns one of the following types:

  • FIELD_TYPE_NULL (0)
  • FIELD_TYPE_INTEGER (1)
  • FIELD_TYPE_FLOAT (2)
  • FIELD_TYPE_STRING (3)
  • FIELD_TYPE_BLOB (4)

How can I accomplish this on pre-Honeycomb <11 devices?

I've tried the following:

for ( int i = 0; i < cursor.getColumnCount(); i++ ) {    

    int columnType = -1;
    try {
        cursor.getInt( i );
        columnType = Cursor.FIELD_TYPE_INTEGER;

    } catch ( Exception ignore ) {

        try {
            cursor.getString( i );
            columnType = Cursor.FIELD_TYPE_STRING;

        } catch ( Exception ignore1 ) {

            try {
                cursor.getFloat( i );
                columnType = Cursor.FIELD_TYPE_FLOAT;

            } catch ( Exception ignore2 ) {

                try {                                             
                  cursor.getBlob( i );
                  columnType = Cursor.FIELD_TYPE_BLOB;

                } catch ( Exception ignore3 ) {

                     columnType = Cursor.FIELD_TYPE_NULL;
                }
           }
       }
   }

}

However, no exception is thrown. The data is always casted in the first type you are checking for, in this case getInt(). That means, I get the correct values if the column type is Integer but a 0 for all other types.

Why am I not looking in the documentation to check what type is stored? The columns differ depending on the device manufacturer and not all of them are documented, see this question: How to handle manufacturer-dependent differences in ContentProviders?

Any ideas?

Goodbye answered 25/7, 2012 at 20:39 Comment(6)
Can you do SELECTs against SQLite's metadata tables?Silassilastic
I'm working with the official call log content provider. AFAIK you cannot rawquery, pragma or request metadata of these tables. It seems like you are bound to the ContentResolver.query() method...Goodbye
Have you tried this hack? #6146376Point
I think that hack cannot be applied to my problem since I'm not dealing with raw SQLite databases, but with content providers. Or am I missing something?Goodbye
don't you actually know what your columns contain ?Manlike
As I've stated at the end of my question, the columns differ from device to device (undocumented). That's why I'm interested in the internal structure of the content provider, the database schema (column types) if you like.Goodbye
E
10

You can use this code when cursor is positioned in a valid row:

CursorWrapper cw = (CursorWrapper)cursor;

Class<?> cursorWrapper = CursorWrapper.class;
Field mCursor = cursorWrapper.getDeclaredField("mCursor");
mCursor.setAccessible(true);
AbstractWindowedCursor abstractWindowedCursor = (AbstractWindowedCursor)mCursor.get(cw);
CursorWindow cursorWindow = abstractWindowedCursor.getWindow();
int pos = abstractWindowedCursor.getPosition();
for ( int i = 0; i < cursor.getColumnCount(); i++ ) {
    String type = null;
    if (cursorWindow.isNull(pos, i)) {
        type = "Cursor.FIELD_TYPE_NULL";
    } else if (cursorWindow.isLong(pos, i)) {
        type = "Cursor.FIELD_TYPE_INTEGER";
    } else if (cursorWindow.isFloat(pos, i)) {
        type = "Cursor.FIELD_TYPE_FLOAT";
    } else if (cursorWindow.isString(pos, i)) {
        type = "Cursor.FIELD_TYPE_STRING";
    } else if (cursorWindow.isBlob(pos, i)) {
        type = "Cursor.FIELD_TYPE_BLOB";
    }
}

Note that Cursor.FIELD_TYPE_* constant values are defined starting from HONEYCOMB.

Eakin answered 24/9, 2013 at 0:45 Comment(5)
Thanks @Juan, I've accepted this as the best answer. However, I'd like to stress once again that this solution really only works when the cursor is positioned in a valid row. That is because SQLite is dynamically-typed, which means that the datatype of a value is associated with the value itself, and not with its container.Goodbye
I just realized that Cursor.getType() in API Level 11 behaves just exactly the same way: No data, no column type. So this answer is totally valid.Goodbye
I'm struggling with this. For some reason I'm getting a ClassCastException on this line CursorWrapper cw = (CursorWrapper)c;Barbershop
@DiscoS2 Please note that Cursor is an interface, so this solution will work when the cursor object is an instance of CursorWrapper class or a subclass of it. If your situation is different than the one in this post (CallLog content provider query), maybe it's a better idea to create another post explaining it.Schlesien
@JuanSánchez In my case I'm getting the exception because Cursor is an instance of net.sqlcipher.CrossProcessCursorWrapper which from the name would appear to be a subclass of CursorWrapper, no? (defined as public class CrossProcessCursorWrapper extends CursorWrapper implements CrossProcessCursor)Vergos
A
12

Expanding on Juan's answer, here is my replacement for the API 11 method Cursor.getType(int i) - for a cursor retuned by an SQL query

public class DbCompat {

    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;

    static int getType(Cursor cursor, int i) throws Exception {
        SQLiteCursor sqLiteCursor = (SQLiteCursor) cursor;
        CursorWindow cursorWindow = sqLiteCursor.getWindow();
        int pos = cursor.getPosition();
        int type = -1;
        if (cursorWindow.isNull(pos, i)) {
            type = FIELD_TYPE_NULL;
        } else if (cursorWindow.isLong(pos, i)) {
            type = FIELD_TYPE_INTEGER;
        } else if (cursorWindow.isFloat(pos, i)) {
            type = FIELD_TYPE_FLOAT;
        } else if (cursorWindow.isString(pos, i)) {
            type = FIELD_TYPE_STRING;
        } else if (cursorWindow.isBlob(pos, i)) {
            type = FIELD_TYPE_BLOB;
        }

        return type;
    }
}

gist: https://gist.github.com/kassim/c340cbfc5243db3a4826

Agreement answered 19/12, 2013 at 15:31 Comment(2)
Why getType method throws Exception?Beatification
This one is perfect for SQLite on Android, when executing a raw query, e.g. like Cursor cursor = msadb.rawQuery(sql, ...)Foolery
E
10

You can use this code when cursor is positioned in a valid row:

CursorWrapper cw = (CursorWrapper)cursor;

Class<?> cursorWrapper = CursorWrapper.class;
Field mCursor = cursorWrapper.getDeclaredField("mCursor");
mCursor.setAccessible(true);
AbstractWindowedCursor abstractWindowedCursor = (AbstractWindowedCursor)mCursor.get(cw);
CursorWindow cursorWindow = abstractWindowedCursor.getWindow();
int pos = abstractWindowedCursor.getPosition();
for ( int i = 0; i < cursor.getColumnCount(); i++ ) {
    String type = null;
    if (cursorWindow.isNull(pos, i)) {
        type = "Cursor.FIELD_TYPE_NULL";
    } else if (cursorWindow.isLong(pos, i)) {
        type = "Cursor.FIELD_TYPE_INTEGER";
    } else if (cursorWindow.isFloat(pos, i)) {
        type = "Cursor.FIELD_TYPE_FLOAT";
    } else if (cursorWindow.isString(pos, i)) {
        type = "Cursor.FIELD_TYPE_STRING";
    } else if (cursorWindow.isBlob(pos, i)) {
        type = "Cursor.FIELD_TYPE_BLOB";
    }
}

Note that Cursor.FIELD_TYPE_* constant values are defined starting from HONEYCOMB.

Eakin answered 24/9, 2013 at 0:45 Comment(5)
Thanks @Juan, I've accepted this as the best answer. However, I'd like to stress once again that this solution really only works when the cursor is positioned in a valid row. That is because SQLite is dynamically-typed, which means that the datatype of a value is associated with the value itself, and not with its container.Goodbye
I just realized that Cursor.getType() in API Level 11 behaves just exactly the same way: No data, no column type. So this answer is totally valid.Goodbye
I'm struggling with this. For some reason I'm getting a ClassCastException on this line CursorWrapper cw = (CursorWrapper)c;Barbershop
@DiscoS2 Please note that Cursor is an interface, so this solution will work when the cursor object is an instance of CursorWrapper class or a subclass of it. If your situation is different than the one in this post (CallLog content provider query), maybe it's a better idea to create another post explaining it.Schlesien
@JuanSánchez In my case I'm getting the exception because Cursor is an instance of net.sqlcipher.CrossProcessCursorWrapper which from the name would appear to be a subclass of CursorWrapper, no? (defined as public class CrossProcessCursorWrapper extends CursorWrapper implements CrossProcessCursor)Vergos
M
0

There is something that may work : http://developer.android.com/reference/android/database/DatabaseUtils.html cursorRowToContentValues

will copy the row in a ContentValues object. Then, you can call ContentValues.get(), which give you an object. you can then look at the Class of this object.

edit

According to the source code of DatabaseUtils, the object are either blobs or Strings.

edit 2

However, if your cursor is a WindowedCursor, it has methods to know the object types. (isBlob, isString, isLong...)

Manlike answered 23/8, 2012 at 14:34 Comment(3)
I just double-checked the source code and the method returns always STRING if the cursor is not an instance of AbstractWindowedCursor. So I tried to cast the content provider cursor to AbstractWindowedCursor, but I get an ClassCastException. So it's not possible?Goodbye
doesn't seem so. Still, chances are you are supposed to know what your data are. CallLog contentprovider is documented and I would assume it can give you what you need to know to fetch dataManlike
If I knew the column types, I would not ask this question. a) Not all content providers are documented (e.g. SMS). b) Look at my linked question (http://stackoverflow.com/questions/11504654/how-to-handle-manufacturer-dependent-differences-in-contentproviders). The columns differ from device to device and there is no common subset.Goodbye
P
0

I faced the same problem in the past. I tackled it with a pretty nice solution. Match this with your needs. In my case I have an amount of different objects which are all synced to a server in the cloud. They all have common properties, so that they all inherit from a common BaseObject. This object has a method that takes a cursor as a parameter and returns a new object of the same type, so that each object that inherits from it, overrides this method with the extended properties of it.

*Note that the inheritance of objects is not necessary for this approach. It's just a smarter way of doing it. As long as you have the same method in all the objects you need to take form DB this will work as you'll be able to see in the end.

Let me illustrate that:

Our baseObject.

public class BaseObject{

    protected int number;
    protected String text;

    public <T extends BaseObject> T setObject(Cursor c) {
        number = c.getInt(cur.getColumnIndexOrThrow(COLUMN_NAME_FOR_NUMBER));
        text = c.getString(cur.getColumnIndexOrThrow(COLUMN_NAME_FOR_TEXT));

        return (T) this;
    }
}

A new object that inherits from the first.

public class Contact extends BaseObject{

    private String name;

    @Override
    public <T extends BaseObject> T setObject(Cursor c) {

        super.setObject(c);

        name = c.getString(cur.getColumnIndexOrThrow(COLUMN_NAME_FOR_NAME));

        return (T) this;
    }
}

Finally in your database it's as easy as to ask for the data you want by calling a generic method "getAllObjects" and passing the class type you want to query along with the other parameters of the query:

public synchronized <T extends BaseObject> ArrayList<T> getObjectsForClass(final Class<T> classType,
        String selection, String[] selectionArgs, String sort, String limit) {

    ArrayList<T> objects = null;

    if (db == null || !db.isOpen()) {
        db = getWritableDatabase();
    }

    objects = new ArrayList<T>();

    Cursor c = null;
    T object;
    try {
        object = classType.newInstance();

        String table = object.getTable();

        StringBuilder tableSb = new StringBuilder();
        tableSb.append(table).append(" INNER JOIN ").append(Constants.DB_BASE_OBJECT_TABLE)
                .append(" ON ").append(table).append(".").append(BaseObject.DB_OBJECT_ID_KEY).append(" = ")
                .append(Constants.DB_BASE_OBJECT_TABLE).append(".")
                .append(BaseObject.DB_ID_KEY);

        c = db.query(tableSb.toString(), null, selection, selectionArgs, null, null, sort, limit);

        if (c.getCount() > 0) {
            c.moveToFirst();
            while (!c.isAfterLast()) {

                object = classType.newInstance();
                object.setObject(c);
                objects.add(object);

                c.moveToNext();
            }
        }

    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    c.close();

    return objects;
}

And there you go. One generic method to get any object from the your database and to successfully turn it into an object or an array of objects in runtime.

Notes:

  • Every object should have a method getTable() in order to be able to query the right table
  • In this approach there's also a OODB connection as you may see. You can use the same approach without that, by just querying all items (SELECT * FROM...)

Hope it helps. Answer back with issues or doubts.

Paprika answered 27/8, 2012 at 13:2 Comment(2)
Thanks for your elaborate answer, but it doesn't tackle my problem at all. Your code helps to query a DB with a single generic function and returns an object of the type being specified. I want to find out the data scheme, that is the column names and types of a content provider... plz correct me if I'm missing the link.Goodbye
Of course, but if you pass the object class as a parameter you'll already know the type of the returned columns, so that you won't need to check the type of every of them. I went for this approach since I faced the same problem and wanted to make my app compatible since 8.Paprika

© 2022 - 2024 — McMap. All rights reserved.