package biz.bokhorst.xprivacy;
import java.io.FileNotFoundException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import android.annotation.SuppressLint;
import android.content.SyncAdapterType;
import android.content.SyncInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
public class XContentResolver extends XHook {
private Methods mMethod;
private boolean mClient;
private String mClassName;
private XContentResolver(Methods method, String restrictionName, String className) {
super(restrictionName, method.name().replace("Srv_", ""), method.name());
mMethod = method;
if (className == null)
mClassName = "com.android.server.content.ContentService";
else
mClassName = className;
}
private XContentResolver(Methods method, String restrictionName, boolean client) {
super(restrictionName, method.name(), null);
mMethod = method;
mClient = client;
mClassName = null;
}
public String getClassName() {
if (mClassName == null)
return (mClient ? "android.content.ContentProviderClient" : "android.content.ContentResolver");
else
return mClassName;
}
// @formatter:off
// public static SyncInfo getCurrentSync()
// static List<SyncInfo> getCurrentSyncs()
// static SyncAdapterType[] getSyncAdapterTypes()
// final AssetFileDescriptor openAssetFileDescriptor(Uri uri, String mode)
// final AssetFileDescriptor openAssetFileDescriptor(Uri uri, String mode, CancellationSignal cancellationSignal)
// final ParcelFileDescriptor openFileDescriptor(Uri uri, String mode, CancellationSignal cancellationSignal)
// final ParcelFileDescriptor openFileDescriptor(Uri uri, String mode)
// final InputStream openInputStream(Uri uri)
// final OutputStream openOutputStream(Uri uri)
// final OutputStream openOutputStream(Uri uri, String mode)
// final AssetFileDescriptor openTypedAssetFileDescriptor(Uri uri, String mimeType, Bundle opts, CancellationSignal cancellationSignal)
// final AssetFileDescriptor openTypedAssetFileDescriptor(Uri uri, String mimeType, Bundle opts)
// AssetFileDescriptor openAssetFile(Uri url, String mode, CancellationSignal signal)
// AssetFileDescriptor openAssetFile(Uri url, String mode)
// ParcelFileDescriptor openFile(Uri url, String mode)
// ParcelFileDescriptor openFile(Uri url, String mode, CancellationSignal signal)
// public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder)
// public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal)
// https://developers.google.com/gmail/android/
// http://developer.android.com/reference/android/content/ContentResolver.html
// http://developer.android.com/reference/android/content/ContentProviderClient.html
// http://developer.android.com/reference/android/provider/Contacts.People.html
// http://developer.android.com/reference/android/provider/ContactsContract.Contacts.html
// http://developer.android.com/reference/android/provider/ContactsContract.Data.html
// http://developer.android.com/reference/android/provider/ContactsContract.PhoneLookup.html
// http://developer.android.com/reference/android/provider/ContactsContract.Profile.html
// http://developer.android.com/reference/android/provider/ContactsContract.RawContacts.html
// frameworks/base/core/java/android/content/ContentResolver.java
// public List<SyncInfo> getCurrentSyncs()
// public void registerContentObserver(android.net.Uri uri, boolean notifyForDescendants, android.database.IContentObserver observer, int userHandle)
// public void unregisterContentObserver(android.database.IContentObserver observer)
// http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.2.2_r1/android/content/ContentService.java
// public List<android.content.SyncInfo> getCurrentSyncsAsUser(int userId)
// http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.0.0_r1/android/content/IContentService.java
// public Bundle call(String method, String request, Bundle args)
// http://developer.android.com/reference/android/provider/Settings.html
// http://developer.android.com/reference/android/provider/Settings.Global.html
// http://developer.android.com/reference/android/provider/Settings.Secure.html
// http://developer.android.com/reference/android/provider/Settings.System.html
// http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.2_r1/com/android/providers/settings/SettingsProvider.java
// @formatter:on
// @formatter:off
private enum Methods {
getCurrentSync, getCurrentSyncs, getSyncAdapterTypes,
openAssetFile, openFile, openAssetFileDescriptor, openFileDescriptor, openInputStream, openOutputStream, openTypedAssetFileDescriptor,
query, Srv_call, Srv_query,
Srv_getCurrentSyncs, Srv_getCurrentSyncsAsUser
};
// @formatter:on
// @formatter:off
public static List<String> cProviderClassName = Arrays.asList(new String[] {
"com.android.providers.downloads.DownloadProvider",
"com.android.providers.calendar.CalendarProvider2",
"com.android.providers.contacts.CallLogProvider",
"com.android.providers.contacts.ContactsProvider2",
"com.google.android.gm.provider.PublicContentProvider",
"com.google.android.gsf.gservices.GservicesProvider",
"com.android.providers.telephony.MmsProvider",
"com.android.providers.telephony.MmsSmsProvider",
"com.android.providers.telephony.SmsProvider",
"com.android.providers.telephony.TelephonyProvider",
"com.android.providers.userdictionary.UserDictionaryProvider",
"com.android.providers.settings.SettingsProvider",
});
// @formatter:on
public static List<XHook> getPackageInstances(String packageName, ClassLoader loader) {
if (packageName.startsWith("com.android.browser.provider"))
try {
Class.forName("com.android.browser.provider.BrowserProviderProxy", false, loader);
return getInstances("com.android.browser.provider.BrowserProviderProxy");
} catch (ClassNotFoundException ignored) {
try {
Class.forName("com.android.browser.provider.BrowserProvider2", false, loader);
return getInstances("com.android.browser.provider.BrowserProvider2");
} catch (ClassNotFoundException ignored2) {
Util.log(null, Log.ERROR, "Browser provider not found, package=" + packageName);
return new ArrayList<XHook>();
}
}
else if (packageName.startsWith("com.android.email.provider"))
try {
Class.forName("com.android.email.provider.EmailProvider", false, loader);
return getInstances("com.android.email.provider.EmailProvider");
} catch (ClassNotFoundException ignored) {
Util.log(null, Log.WARN, "E-mail provider not found, package=" + packageName);
return new ArrayList<XHook>();
}
else if (packageName.startsWith("com.google.android.gm.provider"))
try {
Class.forName("com.google.android.gm.provider.PublicContentProvider", false, loader);
return getInstances("com.google.android.gm.provider.PublicContentProvider");
} catch (ClassNotFoundException ignored) {
Util.log(null, Log.WARN, "G-mail provider not found, package=" + packageName);
return new ArrayList<XHook>();
}
else {
List<XHook> listHook = new ArrayList<XHook>();
for (String className : cProviderClassName)
if (className.startsWith(packageName))
listHook.addAll(getInstances(className));
return listHook;
}
}
private static List<XHook> getInstances(String className) {
List<XHook> listHook = new ArrayList<XHook>();
if ("com.android.providers.settings.SettingsProvider".equals(className))
listHook.add(new XContentResolver(Methods.Srv_call, null, className));
else
listHook.add(new XContentResolver(Methods.Srv_query, null, className));
return listHook;
}
public static List<XHook> getInstances(boolean server) {
List<XHook> listHook = new ArrayList<XHook>();
if (server) {
listHook.add(new XContentResolver(Methods.Srv_query, null, "com.android.internal.telephony.IccProvider"));
listHook.add(new XContentResolver(Methods.Srv_getCurrentSyncs, PrivacyManager.cAccounts, null));
listHook.add(new XContentResolver(Methods.Srv_getCurrentSyncsAsUser, PrivacyManager.cAccounts, null));
} else {
listHook.add(new XContentResolver(Methods.getCurrentSync, PrivacyManager.cAccounts, false));
listHook.add(new XContentResolver(Methods.getCurrentSyncs, PrivacyManager.cAccounts, false));
listHook.add(new XContentResolver(Methods.getSyncAdapterTypes, PrivacyManager.cAccounts, false));
listHook.add(new XContentResolver(Methods.openAssetFileDescriptor, PrivacyManager.cStorage, false));
listHook.add(new XContentResolver(Methods.openFileDescriptor, PrivacyManager.cStorage, false));
listHook.add(new XContentResolver(Methods.openInputStream, PrivacyManager.cStorage, false));
listHook.add(new XContentResolver(Methods.openOutputStream, PrivacyManager.cStorage, false));
listHook.add(new XContentResolver(Methods.openTypedAssetFileDescriptor, PrivacyManager.cStorage, false));
listHook.add(new XContentResolver(Methods.openAssetFile, PrivacyManager.cStorage, true));
listHook.add(new XContentResolver(Methods.openFile, PrivacyManager.cStorage, true));
listHook.add(new XContentResolver(Methods.openTypedAssetFileDescriptor, PrivacyManager.cStorage, true));
listHook.add(new XContentResolver(Methods.query, null, false));
listHook.add(new XContentResolver(Methods.query, null, true));
}
return listHook;
}
@Override
protected void before(XParam param) throws Throwable {
switch (mMethod) {
case getCurrentSync:
case getCurrentSyncs:
case getSyncAdapterTypes:
case openAssetFile:
case openFile:
case openAssetFileDescriptor:
case openFileDescriptor:
case openInputStream:
case openOutputStream:
case openTypedAssetFileDescriptor:
// Do nothing
break;
case Srv_call:
break;
case query:
case Srv_query:
handleUriBefore(param);
break;
case Srv_getCurrentSyncs:
case Srv_getCurrentSyncsAsUser:
// Do nothing
break;
}
}
@Override
protected void after(XParam param) throws Throwable {
switch (mMethod) {
case getCurrentSync:
if (isRestricted(param))
param.setResult(null);
break;
case getCurrentSyncs:
if (isRestricted(param))
param.setResult(new ArrayList<SyncInfo>());
break;
case getSyncAdapterTypes:
if (isRestricted(param))
param.setResult(new SyncAdapterType[0]);
break;
case openAssetFileDescriptor:
case openFileDescriptor:
case openInputStream:
case openOutputStream:
case openTypedAssetFileDescriptor:
case openAssetFile:
case openFile:
if (param.args.length > 0 && param.args[0] instanceof Uri) {
String uri = ((Uri) param.args[0]).toString();
if (isRestrictedExtra(param, uri))
param.setThrowable(new FileNotFoundException("XPrivacy"));
}
break;
case Srv_call:
handleCallAfter(param);
break;
case query:
case Srv_query:
handleUriAfter(param);
break;
case Srv_getCurrentSyncs:
case Srv_getCurrentSyncsAsUser:
if (param.getResult() != null)
if (isRestricted(param)) {
int uid = Binder.getCallingUid();
@SuppressWarnings("unchecked")
List<SyncInfo> listSync = (List<SyncInfo>) param.getResult();
List<SyncInfo> listFiltered = new ArrayList<SyncInfo>();
for (SyncInfo sync : listSync)
if (XAccountManager.isAccountAllowed(sync.account, uid))
listFiltered.add(sync);
param.setResult(listFiltered);
}
break;
}
}
@SuppressLint("DefaultLocale")
private void handleUriBefore(XParam param) throws Throwable {
// Check URI
if (param.args.length > 1 && param.args[0] instanceof Uri) {
String uri = ((Uri) param.args[0]).toString().toLowerCase();
String[] projection = (param.args[1] instanceof String[] ? (String[]) param.args[1] : null);
if (uri.startsWith("content://com.android.contacts/contacts/name_phone_or_email")) {
// Do nothing
} else if (uri.startsWith("content://com.android.contacts/")
&& !uri.equals("content://com.android.contacts/")) {
String[] components = uri.replace("content://com.android.", "").split("/");
String methodName = components[0] + "/" + components[1].split("\\?")[0];
if (methodName.equals("contacts/contacts") || methodName.equals("contacts/data")
|| methodName.equals("contacts/phone_lookup") || methodName.equals("contacts/raw_contacts"))
if (isRestrictedExtra(param, PrivacyManager.cContacts, methodName, uri)) {
// Get ID from URL if any
int urlid = -1;
if ((methodName.equals("contacts/contacts") || methodName.equals("contacts/phone_lookup"))
&& components.length > 2 && TextUtils.isDigitsOnly(components[2]))
urlid = Integer.parseInt(components[2]);
// Modify projection
boolean added = false;
if (projection != null && urlid < 0) {
List<String> listProjection = new ArrayList<String>();
listProjection.addAll(Arrays.asList(projection));
String cid = getIdForUri(uri);
if (cid != null && !listProjection.contains(cid)) {
added = true;
listProjection.add(cid);
}
param.args[1] = listProjection.toArray(new String[0]);
}
if (added)
param.setObjectExtra("column_added", added);
}
}
}
}
@SuppressLint("DefaultLocale")
private void handleUriAfter(XParam param) throws Throwable {
// Check URI
if (param.args.length > 1 && param.args[0] instanceof Uri && param.getResult() != null) {
String uri = ((Uri) param.args[0]).toString().toLowerCase();
String[] projection = (param.args[1] instanceof String[] ? (String[]) param.args[1] : null);
String selection = (param.args[2] instanceof String ? (String) param.args[2] : null);
Cursor cursor = (Cursor) param.getResult();
if (uri.startsWith("content://applications")) {
// Applications provider: allow selected applications
if (isRestrictedExtra(param, PrivacyManager.cSystem, "ApplicationsProvider", uri)) {
MatrixCursor result = new MatrixCursor(cursor.getColumnNames());
while (cursor.moveToNext()) {
int colPackage = cursor.getColumnIndex("package");
String packageName = (colPackage < 0 ? null : cursor.getString(colPackage));
if (packageName != null && XPackageManager.isPackageAllowed(0, packageName))
copyColumns(cursor, result);
}
result.respond(cursor.getExtras());
param.setResult(result);
cursor.close();
}
} else if (uri.startsWith("content://com.google.android.gsf.gservices")) {
// Google services provider: block only android_id
if (param.args.length > 3 && param.args[3] != null) {
List<String> listSelection = Arrays.asList((String[]) param.args[3]);
if (listSelection.contains("android_id"))
if (isRestrictedExtra(param, PrivacyManager.cIdentification, "GservicesProvider", uri)) {
int ikey = cursor.getColumnIndex("key");
int ivalue = cursor.getColumnIndex("value");
if (ikey == 0 && ivalue == 1 && cursor.getColumnCount() == 2) {
MatrixCursor result = new MatrixCursor(cursor.getColumnNames());
while (cursor.moveToNext()) {
if ("android_id".equals(cursor.getString(ikey)) && cursor.getString(ivalue) != null)
result.addRow(new Object[] { "android_id",
PrivacyManager.getDefacedProp(Binder.getCallingUid(), "GSF_ID") });
else
copyColumns(cursor, result);
}
result.respond(cursor.getExtras());
param.setResult(result);
cursor.close();
} else
Util.log(this, Log.ERROR,
"Unexpected result uri=" + uri + " columns=" + cursor.getColumnNames());
}
}
} else if (uri.startsWith("content://com.android.contacts/contacts/name_phone_or_email")) {
// Do nothing
} else if (uri.startsWith("content://com.android.contacts/")
&& !uri.equals("content://com.android.contacts/")) {
// Contacts provider: allow selected contacts
String[] components = uri.replace("content://com.android.", "").split("/");
String methodName = components[0] + "/" + components[1].split("\\?")[0];
if (methodName.equals("contacts/contacts") || methodName.equals("contacts/data")
|| methodName.equals("contacts/phone_lookup") || methodName.equals("contacts/raw_contacts")) {
if (isRestrictedExtra(param, PrivacyManager.cContacts, methodName, uri)) {
// Get ID from URL if any
int urlid = -1;
if ((methodName.equals("contacts/contacts") || methodName.equals("contacts/phone_lookup"))
&& components.length > 2 && TextUtils.isDigitsOnly(components[2]))
urlid = Integer.parseInt(components[2]);
// Modify column names back
Object column_added = param.getObjectExtra("column_added");
boolean added = (column_added == null ? false : (Boolean) param.getObjectExtra("column_added"));
List<String> listColumn = new ArrayList<String>();
listColumn.addAll(Arrays.asList(cursor.getColumnNames()));
if (added)
listColumn.remove(listColumn.size() - 1);
// Get blacklist setting
int uid = Binder.getCallingUid();
boolean blacklist = PrivacyManager
.getSettingBool(-uid, PrivacyManager.cSettingBlacklist, false);
MatrixCursor result = new MatrixCursor(listColumn.toArray(new String[0]));
// Filter rows
String cid = getIdForUri(uri);
int iid = (cid == null ? -1 : cursor.getColumnIndex(cid));
if (iid >= 0 || urlid >= 0)
while (cursor.moveToNext()) {
// Check if allowed
long id = (urlid >= 0 ? urlid : cursor.getLong(iid));
boolean allowed = PrivacyManager.getSettingBool(-uid, Meta.cTypeContact,
Long.toString(id), false);
if (blacklist)
allowed = !allowed;
if (allowed)
copyColumns(cursor, result, listColumn.size());
}
else
Util.log(this, Log.WARN, "ID missing URI=" + uri + " added=" + added + "/" + cid
+ " columns=" + TextUtils.join(",", cursor.getColumnNames()) + " projection="
+ (projection == null ? "null" : TextUtils.join(",", projection)) + " selection="
+ selection);
result.respond(cursor.getExtras());
param.setResult(result);
cursor.close();
}
} else {
methodName = null;
if (uri.startsWith("content://com.android.contacts/profile"))
methodName = "contacts/profile";
else
methodName = "ContactsProvider2"; // fall-back
if (methodName != null)
if (isRestrictedExtra(param, PrivacyManager.cContacts, methodName, uri)) {
// Return empty cursor
MatrixCursor result = new MatrixCursor(cursor.getColumnNames());
result.respond(cursor.getExtras());
param.setResult(result);
cursor.close();
}
}
} else {
// Other uri restrictions
String restrictionName = null;
String methodName = null;
if (uri.startsWith("content://browser")) {
restrictionName = PrivacyManager.cBrowser;
methodName = "BrowserProvider2";
}
else if (uri.startsWith("content://com.android.calendar")) {
restrictionName = PrivacyManager.cCalendar;
methodName = "CalendarProvider2";
}
else if (uri.startsWith("content://call_log")) {
restrictionName = PrivacyManager.cCalling;
methodName = "CallLogProvider";
}
else if (uri.startsWith("content://contacts/people")) {
restrictionName = PrivacyManager.cContacts;
methodName = "contacts/people";
}
else if (uri.startsWith("content://downloads")) {
restrictionName = PrivacyManager.cBrowser;
methodName = "Downloads";
}
else if (uri.startsWith("content://com.android.email.provider")) {
restrictionName = PrivacyManager.cEMail;
methodName = "EMailProvider";
}
else if (uri.startsWith("content://com.google.android.gm")) {
restrictionName = PrivacyManager.cEMail;
methodName = "GMailProvider";
}
else if (uri.startsWith("content://icc")) {
restrictionName = PrivacyManager.cContacts;
methodName = "IccProvider";
}
else if (uri.startsWith("content://mms")) {
restrictionName = PrivacyManager.cMessages;
methodName = "MmsProvider";
}
else if (uri.startsWith("content://mms-sms")) {
restrictionName = PrivacyManager.cMessages;
methodName = "MmsSmsProvider";
}
else if (uri.startsWith("content://sms")) {
restrictionName = PrivacyManager.cMessages;
methodName = "SmsProvider";
}
else if (uri.startsWith("content://telephony")) {
restrictionName = PrivacyManager.cPhone;
methodName = "TelephonyProvider";
}
else if (uri.startsWith("content://user_dictionary")) {
restrictionName = PrivacyManager.cDictionary;
methodName = "UserDictionary";
}
else if (uri.startsWith("content://com.android.voicemail")) {
restrictionName = PrivacyManager.cMessages;
methodName = "VoicemailContentProvider";
}
// Check if know / restricted
if (restrictionName != null && methodName != null) {
if (isRestrictedExtra(param, restrictionName, methodName, uri)) {
// Return empty cursor
MatrixCursor result = new MatrixCursor(cursor.getColumnNames());
result.respond(cursor.getExtras());
param.setResult(result);
cursor.close();
}
}
}
}
}
private void handleCallAfter(XParam param) throws Throwable {
if (param.args.length > 1 && param.args[0] instanceof String && param.args[1] instanceof String) {
String method = (String) param.args[0];
String request = (String) param.args[1];
if ("GET_secure".equals(method)) {
if (Settings.Secure.ANDROID_ID.equals(request)) {
if (!hasEmptyValue(param.getResult()))
if (isRestricted(param, PrivacyManager.cIdentification, "Srv_Android_ID")) {
int uid = Binder.getCallingUid();
String value = (String) PrivacyManager.getDefacedProp(uid, "ANDROID_ID");
Bundle bundle = new Bundle(1);
bundle.putString("value", value);
param.setResult(bundle);
}
}
} else if ("GET_system".equals(method)) {
// Do nothing
} else if ("GET_global".equals(method)) {
if ("default_dns_server".equals(request)) {
if (!hasEmptyValue(param.getResult()))
if (isRestricted(param, PrivacyManager.cNetwork, "Srv_Default_DNS")) {
int uid = Binder.getCallingUid();
InetAddress value = (InetAddress) PrivacyManager.getDefacedProp(uid, "InetAddress");
Bundle bundle = new Bundle(1);
bundle.putString("value", value.getHostAddress());
param.setResult(bundle);
}
} else if ("wifi_country_code".equals(request)) {
if (!hasEmptyValue(param.getResult()))
if (isRestricted(param, PrivacyManager.cNetwork, "Srv_WiFi_Country")) {
int uid = Binder.getCallingUid();
String value = (String) PrivacyManager.getDefacedProp(uid, "CountryIso");
Bundle bundle = new Bundle(1);
bundle.putString("value", value == null ? null : value.toLowerCase(Locale.ROOT));
param.setResult(bundle);
}
}
}
}
}
// Helper methods
private boolean hasEmptyValue(Object result) {
Bundle bundle = (Bundle) result;
if (bundle == null)
return true;
if (!bundle.containsKey("value"))
return true;
return (bundle.get("value") == null);
}
private String getIdForUri(String uri) {
if (uri.startsWith("content://com.android.contacts/contacts"))
return "_id";
else if (uri.startsWith("content://com.android.contacts/data"))
return "contact_id";
else if (uri.startsWith("content://com.android.contacts/phone_lookup"))
return "_id";
else if (uri.startsWith("content://com.android.contacts/raw_contacts"))
return "contact_id";
else
Util.log(this, Log.ERROR, "Unexpected uri=" + uri);
return null;
}
private void copyColumns(Cursor cursor, MatrixCursor result) {
copyColumns(cursor, result, cursor.getColumnCount());
}
private void copyColumns(Cursor cursor, MatrixCursor result, int count) {
try {
Object[] columns = new Object[count];
for (int i = 0; i < count; i++)
switch (cursor.getType(i)) {
case Cursor.FIELD_TYPE_NULL:
columns[i] = null;
break;
case Cursor.FIELD_TYPE_INTEGER:
columns[i] = cursor.getInt(i);
break;
case Cursor.FIELD_TYPE_FLOAT:
columns[i] = cursor.getFloat(i);
break;
case Cursor.FIELD_TYPE_STRING:
columns[i] = cursor.getString(i);
break;
case Cursor.FIELD_TYPE_BLOB:
columns[i] = cursor.getBlob(i);
break;
default:
Util.log(this, Log.WARN, "Unknown cursor data type=" + cursor.getType(i));
}
result.addRow(columns);
} catch (Throwable ex) {
Util.bug(this, ex);
}
}
@SuppressWarnings("unused")
private void _dumpCursor(String uri, Cursor cursor) {
_dumpHeader(uri, cursor);
int i = 0;
while (cursor.moveToNext() && i++ < 10)
_dumpColumns(cursor, "");
cursor.moveToFirst();
}
private void _dumpHeader(String uri, Cursor cursor) {
Util.log(this, Log.WARN, TextUtils.join(", ", cursor.getColumnNames()) + " uri=" + uri);
}
private void _dumpColumns(Cursor cursor, String msg) {
String[] columns = new String[cursor.getColumnCount()];
for (int i = 0; i < cursor.getColumnCount(); i++)
switch (cursor.getType(i)) {
case Cursor.FIELD_TYPE_NULL:
columns[i] = null;
break;
case Cursor.FIELD_TYPE_INTEGER:
columns[i] = Integer.toString(cursor.getInt(i));
break;
case Cursor.FIELD_TYPE_FLOAT:
columns[i] = Float.toString(cursor.getFloat(i));
break;
case Cursor.FIELD_TYPE_STRING:
columns[i] = cursor.getString(i);
break;
case Cursor.FIELD_TYPE_BLOB:
columns[i] = "[blob]";
break;
default:
Util.log(this, Log.WARN, "Unknown cursor data type=" + cursor.getType(i));
}
Util.log(this, Log.WARN, TextUtils.join(", ", columns) + " " + msg);
}
}