Writing custom Content Provider for photos on phone (part 2) - android

I'm working on a custom Content Provider for my app. This is part of a course I'm taking on Android apps, so please don't expect the rationale for doing all this to be too great ;-) The whole point here is for me to learn about CP.
I've got a previous post which goes on and on with this, but I think I've managed to simplify my problem quite a bit. So, I'm working on a "gallery app". Since I don't know how and where thumbnail images are stored on the phone, I've decided to simply use MediaStore.Images.Thumbnails to access the thumbs, and show them in my GridView.
However, to fullfill the requirements of said course, I'll write a "PhotoProvider" to load single photos, full screen, in the DetailActivity. Also, for this to make some sense, and not just be a wrapper around MediaStore.Media.Images, I'm extending that data with some EXIF tags from the JPEG file.
Having found a great post here on StackOverflow, and continuing to wrangle the source code provided in class, I've come up with the following classes. Maybe you want to take a look, and help me out (point me in the right direction)?
The URIs I'll support, are context://AUTH/photo/#, to get a specific image (given an IMAGE_ID, from the thumbnail). Furthermore, to make it a bit more interesting, I also want to be able to write data to the EXIF tag UserComment: context://AUTH/photo/#/comment/* (the comment String is the last parameter there). Does that look sensible?
Some questions: (updated)
getType() is confusing. Again, this is lending from the app course. When returning an image, I guess the type should always be image/jpeg (or maybe PNG)?
Edit: Learning more stuff, I now understand that the URI returned from the insert() method is, I guess, optional, but it often useful to return a "link" (i.e. URI) to the new (inserted) data! For my case, after updating the EXIF tags, I could return either null, or an URI to the edited photo: context://AUTH/photo/7271 (where 7271 mimicks the PHOTO_ID).
Below is my (unfinished!) code. Please have a look, especially at the query() and insert() functions :-)
PhotoContract.java
package com.example.android.galleri.app.data;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.MediaStore;
public class PhotoContract {
public static final String CONTENT_AUTHORITY = "no.myapp.android.galleri.app";
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
public static final String PATH_PHOTO = "photo";
public static final String PATH_COMMENT = "comment";
public static final class PhotoEntry {
public static final Uri CONTENT_URI =
BASE_CONTENT_URI.buildUpon().appendPath(PATH_PHOTO).build();
public static final String COLUMN_DISPLAY_NAME = MediaStore.Images.Media.DISPLAY_NAME;
public static final String COLUMN_DATA = MediaStore.Images.Media.DATA;
public static final String COLUMN_DESC = MediaStore.Images.Media.DESCRIPTION;
public static final String COLUMN_DATE_TAKEN = MediaStore.Images.Media.DATE_TAKEN;
public static final String COLUMN_DATE_ADDED = MediaStore.Images.Media.DATE_ADDED;
public static final String COLUMN_TITLE = MediaStore.Images.Media.TITLE;
public static final String COLUMN_SIZE = MediaStore.Images.Media.SIZE;
public static final String COLUMN_ORIENTATION = MediaStore.Images.Media.ORIENTATION;
public static final String COLUMN_EXIF_COMMENT = "UserComment";
public static final String COLUMN_EXIF_AUTHOR = "Author";
// should these simply be image/png??
public static final String CONTENT_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PHOTO;
public static final String CONTENT_ITEM_TYPE =
ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PHOTO;
// makes an URI to a specific image_id
public static final Uri buildPhotoWithId(Long photo_id) {
return CONTENT_URI.buildUpon().appendPath(Long.toString(photo_id)).build();
}
// since we will "redirect" the URI towards MediaStore, we need to be able to extract IMAGE_ID
public static Long getImageIdFromUri(Uri uri) {
return Long.parseLong(uri.getPathSegments().get(1)); // TODO: is it position 1??
}
// get comment to set in EXIF tag
public static String getCommentFromUri(Uri uri) {
return uri.getPathSegments().get(2); // TODO: is it position 2??
}
}
}
PhotoProvider.java:
package com.example.android.galleri.app.data;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ExifInterface;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v4.content.CursorLoader;
import android.util.Log;
import java.io.IOException;
public class PhotoProvider extends ContentProvider {
// The URI Matcher used by this content provider.
private static final UriMatcher sUriMatcher = buildUriMatcher();
static final int PHOTO = 100;
static final int PHOTO_SET_COMMENT = 200;
static UriMatcher buildUriMatcher() {
// 1) The code passed into the constructor represents the code to return for the root
// URI. It's common to use NO_MATCH as the code for this case. Add the constructor below.
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
final String authority = PhotoContract.CONTENT_AUTHORITY;
// 2) Use the addURI function to match each of the types. Use the constants from
// WeatherContract to help define the types to the UriMatcher.
// matches photo/<any number> meaning any photo ID
matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#", PHOTO);
// matches photo/<photo id>/comment/<any comment>
matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#/" + PhotoContract.PATH_COMMENT + "/*", PHOTO_SET_COMMENT);
// 3) Return the new matcher!
return matcher;
}
#Override
public String getType(Uri uri) {
// Use the Uri Matcher to determine what kind of URI this is.
final int match = sUriMatcher.match(uri);
switch (match) {
case PHOTO_SET_COMMENT:
return PhotoContract.PhotoEntry.CONTENT_TYPE;
case PHOTO:
return PhotoContract.PhotoEntry.CONTENT_TYPE;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
#Override
public boolean onCreate() {
return true; // enough?
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
MatrixCursor retCursor = new MatrixCursor(projection);
// open the specified image through the MediaStore to get base columns
// then open image file through ExifInterface to get detail columns
Long IMAGE_ID = PhotoContract.PhotoEntry.getImageIdFromUri(uri);
//Uri baseUri = Uri.parse("content://media/external/images/media");
Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
baseUri = Uri.withAppendedPath(baseUri, ""+ IMAGE_ID);
String[] MS_projection = {
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DESCRIPTION,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.TITLE,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.ORIENTATION};
// http://androidsnippets.com/get-file-path-of-gallery-image
Cursor c = getContext().getContentResolver().query(baseUri, MS_projection, null, null, null);
// dump fields (the ones we want -- assuming for now we want ALL fields, in SAME ORDER) into MatrixCursor
Object[] row = new Object[projection.length];
row[0] = c.getString(0); // DISPLAY_NAME
row[1] = c.getBlob(1); // etc
row[2] = c.getString(2);
row[3] = c.getLong(3);
row[4] = c.getLong(4);
row[5] = c.getString(5);
row[6] = c.getInt(6);
row[7] = c.getInt(7);
// NB! Extra +2 fields, for EXIF data.
try {
ExifInterface exif = new ExifInterface((String)row[1]);
row[8] = exif.getAttribute("UserComment");
row[9] = exif.getAttribute("Author");
} catch (IOException e) {
e.printStackTrace();
}
retCursor.addRow(row);
return retCursor;
}
#Override
public Uri insert(Uri uri, ContentValues values) {
String comment_to_set = PhotoContract.PhotoEntry.getCommentFromUri(uri);
Long IMAGE_ID = PhotoContract.PhotoEntry.getImageIdFromUri(uri);
Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
baseUri = Uri.withAppendedPath(baseUri, ""+ IMAGE_ID);
// get DATA (path/filename) from MediaStore -- only need that specific piece of information
String[] MS_projection = {MediaStore.Images.Media.DATA};
// http://androidsnippets.com/get-file-path-of-gallery-image
Cursor c = getContext().getContentResolver().query(baseUri, MS_projection, null, null, null);
String thumbData = c.getString(0);
try {
ExifInterface exif = new ExifInterface(thumbData);
exif.setAttribute("UserComment", comment_to_set);
exif.saveAttributes();
} catch (IOException e) {
e.printStackTrace();
}
return PhotoContract.PhotoEntry.buildPhotoWithId(IMAGE_ID); // return URI to this specific image
}
#Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}

I believe I managed to figure this out, and it wasn't so mysterious after all. Writing a custom content, in this case, provider really boiled down to implementing my own query and update methods. I didn't need any insert or delete, since I'm only reading the MediaStore.
To get started, I designed two URIs; one for query and one for update. The query URI followed the pattern, content://AUTH/photo/#, where # is the IMAGE_ID from MediaStore.Images.Thumbnails. This method essentially runs another query against MediaStore.Images.Media, to get the "standard" data on the specific image, but then gets some additional meta data on the image via the ExifInterface. All this is returned in a MatrixCursor.
The update URI matches the pattern, content://AUTH/photo/#/comment, which then sets (writes) the UserComment EXIF tag in the file with that ID -- again via the ExifInterface.
To answer my question, I'm posting updated code from my PhotoContract and PhotoProvider classes. Essentially, if you're writing a content provider which does not interface the SQLite database -- but something else on the device -- all you need to do is implement those operations in the appropriate methods in your provider class.
PhotoContract
import android.content.ContentResolver;
import android.content.ContentUris;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.MediaStore;
public class PhotoContract {
public static final String CONTENT_AUTHORITY = "com.example.android.myFunkyApp.app";
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
public static final String PATH_PHOTO = "photo";
public static final String PATH_COMMENT = "comment";
//public static final String PATH_AUTHOR = "author";
public static final class ThumbEntry {
public static final String COLUMN_THUMB_ID = MediaStore.Images.Thumbnails._ID;
public static final String COLUMN_DATA = MediaStore.Images.Thumbnails.DATA;
public static final String COLUMN_IMAGE_ID = MediaStore.Images.Thumbnails.IMAGE_ID;
}
public static final class PhotoEntry {
public static final Uri CONTENT_URI =
BASE_CONTENT_URI.buildUpon().appendPath(PATH_PHOTO).build();
public static final String COLUMN_IMAGE_ID = MediaStore.Images.Media._ID;
public static final String COLUMN_DISPLAY_NAME = MediaStore.Images.Media.DISPLAY_NAME;
public static final String COLUMN_DATA = MediaStore.Images.Media.DATA;
public static final String COLUMN_DESC = MediaStore.Images.Media.DESCRIPTION;
public static final String COLUMN_DATE_TAKEN = MediaStore.Images.Media.DATE_TAKEN;
public static final String COLUMN_DATE_ADDED = MediaStore.Images.Media.DATE_ADDED;
public static final String COLUMN_TITLE = MediaStore.Images.Media.TITLE;
public static final String COLUMN_SIZE = MediaStore.Images.Media.SIZE;
public static final String COLUMN_ORIENTATION = MediaStore.Images.Media.ORIENTATION;
public static final String COLUMN_EXIF_COMMENT = "UserComment";
//public static final String COLUMN_EXIF_AUTHOR = "Author";
public static final String CONTENT_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PHOTO;
public static final String CONTENT_ITEM_TYPE =
ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PHOTO;
// makes an URI to a specific image_id
public static final Uri buildPhotoUriWithId(Long photo_id) {
return CONTENT_URI.buildUpon().appendPath(Long.toString(photo_id)).build();
}
// since we will "redirect" the URI towards MediaStore, we need to be able to extract IMAGE_ID
public static Long getImageIdFromUri(Uri uri) {
return Long.parseLong(uri.getPathSegments().get(1)); // always in position 1
}
}
}
PhotoProvider
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.AbstractWindowedCursor;
import android.database.Cursor;
import android.database.CursorWindow;
import android.database.CursorWrapper;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ExifInterface;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v4.content.CursorLoader;
import android.util.Log;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
public class PhotoProvider extends ContentProvider {
// The URI Matcher used by this content provider.
private static final UriMatcher sUriMatcher = buildUriMatcher();
static final int PHOTO = 100;
static final int PHOTO_COMMENT = 101;
static final int PHOTO_AUTHOR = 102;
static UriMatcher buildUriMatcher() {
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
final String authority = PhotoContract.CONTENT_AUTHORITY;
// matches photo/<any number> meaning any photo ID
matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#", PHOTO);
// matches photo/<photo id>/comment/ (comment text in ContentValues)
matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#/" + PhotoContract.PATH_COMMENT, PHOTO_COMMENT);
// matches photo/<photo id>/author/ (author name in ContentValues)
//matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#/" + PhotoContract.PATH_AUTHOR, PHOTO_AUTHOR);
return matcher;
}
#Override
public String getType(Uri uri) {
// Use the Uri Matcher to determine what kind of URI this is.
final int match = sUriMatcher.match(uri);
// Note: We always return single row of data, so content-type is always "a dir"
switch (match) {
case PHOTO_COMMENT:
return PhotoContract.PhotoEntry.CONTENT_TYPE;
case PHOTO_AUTHOR:
return PhotoContract.PhotoEntry.CONTENT_TYPE;
case PHOTO:
return PhotoContract.PhotoEntry.CONTENT_TYPE;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
#Override
public boolean onCreate() {
return true;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
MatrixCursor retCursor = new MatrixCursor(projection);
// open the specified image through the MediaStore to get base columns
// then open image file through ExifInterface to get detail columns
Long IMAGE_ID = PhotoContract.PhotoEntry.getImageIdFromUri(uri);
Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
baseUri = Uri.withAppendedPath(baseUri, ""+ IMAGE_ID);
// http://androidsnippets.com/get-file-path-of-gallery-image
// run query against MediaStore, projection = null means "get all fields"
Cursor c = getContext().getContentResolver().query(baseUri, null, null, null, null);
if (!c.moveToFirst()) {
return null;
}
// match returned fields against projection, and copy into row[]
Object[] row = new Object[projection.length];
int i = 0;
/* // Cursor.getType() Requires API level > 10...
for (String colName : projection) {
int idx = c.getColumnIndex(colName);
if (idx <= 0) return null; // ERROR
int colType = c.getType(idx);
switch (colType) {
case Cursor.FIELD_TYPE_INTEGER: {
row[i++] = c.getLong(idx);
break;
}
case Cursor.FIELD_TYPE_FLOAT: {
row[i++] = c.getFloat(idx);
break;
}
case Cursor.FIELD_TYPE_STRING: {
row[i++] = c.getString(idx);
break;
}
case Cursor.FIELD_TYPE_BLOB: {
row[i++] = c.getBlob(idx);
break;
}
}
}
*/
//http://stackoverflow.com/questions/11658239/cursor-gettype-for-api-level-11
CursorWrapper cw = (CursorWrapper)c;
Class<?> cursorWrapper = CursorWrapper.class;
Field mCursor = null;
try {
mCursor = cursorWrapper.getDeclaredField("mCursor");
mCursor.setAccessible(true);
AbstractWindowedCursor abstractWindowedCursor = (AbstractWindowedCursor)mCursor.get(cw);
CursorWindow cursorWindow = abstractWindowedCursor.getWindow();
int pos = abstractWindowedCursor.getPosition();
// NB! Expect resulting cursor to contain data in same order as projection!
for (String colName : projection) {
int idx = c.getColumnIndex(colName);
// simple solution: If column name NOT FOUND in MediaStore, assume it's an EXIF tag
// and skip
if (idx >= 0) {
if (cursorWindow.isNull(pos, idx)) {
//Cursor.FIELD_TYPE_NULL
row[i++] = null;
} else if (cursorWindow.isLong(pos, idx)) {
//Cursor.FIELD_TYPE_INTEGER
row[i++] = c.getLong(idx);
} else if (cursorWindow.isFloat(pos, idx)) {
//Cursor.FIELD_TYPE_FLOAT
row[i++] = c.getFloat(idx);
} else if (cursorWindow.isString(pos, idx)) {
//Cursor.FIELD_TYPE_STRING
row[i++] = c.getString(idx);
} else if (cursorWindow.isBlob(pos, idx)) {
//Cursor.FIELD_TYPE_BLOB
row[i++] = c.getBlob(idx);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
// have now handled the first i fields in projection. If there are any more, we expect
// these to be valid EXIF tags. Should obviously make this more robust...
try {
ExifInterface exif = new ExifInterface((String) row[2]);
while (i < projection.length) {
row[i] = exif.getAttribute("UserComment"); //projection[i]); // String (or null)
i++;
}
} catch (IOException e) {
e.printStackTrace();
}
retCursor.addRow(row);
return retCursor;
}
#Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
#Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
// URI identifies IMAGE_ID and which EXIF tag to set; content://AUTH/photo/<image_id>/comment or /author
// first, get IMAGE_ID and prepare URI (to get file path from MediaStore)
Long IMAGE_ID = PhotoContract.PhotoEntry.getImageIdFromUri(uri);
Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
baseUri = Uri.withAppendedPath(baseUri, ""+ IMAGE_ID);
// get DATA (path/filename) from MediaStore -- only need that specific piece of information
String[] MS_projection = {MediaStore.Images.Media.DATA};
// http://androidsnippets.com/get-file-path-of-gallery-image
Cursor c = getContext().getContentResolver().query(baseUri, MS_projection, null, null, null);
if (!c.moveToFirst()) return -1; // can't get image path/filename...
String thumbData = c.getString(0);
// then, use URIMatcher to identify the "operation" (i.e., which tag to set)
final int match = sUriMatcher.match(uri);
String EXIF_tag;
switch (match) {
case PHOTO_COMMENT: {
EXIF_tag = "UserComment";
break;
}
case PHOTO_AUTHOR: {
EXIF_tag = "Author";
break;
}
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
if (EXIF_tag == null) return -1;
// finally, set tag in image
try {
ExifInterface exif = new ExifInterface(thumbData);
String textToSet = values.get(EXIF_tag).toString();
if (textToSet == null)
throw new IOException("Can't retrieve text to set from ContentValues, on key = " + EXIF_tag);
if (textToSet.length() > 48)
throw new IOException("Data too long (" + textToSet.length() + "), on key = " + EXIF_tag);
exif.setAttribute(EXIF_tag, textToSet);
exif.saveAttributes();
} catch (IOException e) {
e.printStackTrace();
}
return 1; // 1 image updated
}
}

Related

Got Unknown URL content in Android

I am new to Android. I wanted to practice my knowledge about Content Provide, but my app somehow crush and tell me there is an unknown URL content. I try to search for solution but still can't fix the problem...
The logcat message:
2020-05-25 20:10:24.547 15120-15120/com.example.punchinandout E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.punchinandout, PID: 15120
java.lang.IllegalArgumentException: Unknown URL content://com.example.punchinandout.data/time
at android.content.ContentResolver.insert(ContentResolver.java:1535)
at com.example.punchinandout.MainActivity$1.onClick(MainActivity.java:65)
at android.view.View.performClick(View.java:6256)
at android.view.View$PerformClick.run(View.java:24701)
at android.os.Handler.handleCallback(Handler.java:789)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6541)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
My provider in Mainifest:
<provider
android:authorities="com.example.punchinandout"
android:name=".data.TimeProvider"
android:exported="false"/>
My Content Provider class:
package com.example.punchinandout.data;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;
import com.example.punchinandout.data.TimeContract.TimeEntry;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class TimeProvider extends ContentProvider {
private static final String TAG = TimeProvider.class.toString();
private static final int TIME = 100;
private static final int TIME_ID = 101;
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//Determine which content URI we have.
static{
sUriMatcher.addURI(TimeContract.CONTENT_AUTHORITY, TimeContract.CONTENT_PATH, TIME);
sUriMatcher.addURI(TimeContract.CONTENT_AUTHORITY, TimeContract.CONTENT_PATH, TIME_ID);
}
private TimeHelper mDbHelper;
#Override
public boolean onCreate() {
mDbHelper = new TimeHelper(getContext());
return true;
}
#Nullable
#Override
public Cursor query(#NonNull Uri uri, #Nullable String[] projection, #Nullable String selection, #Nullable String[] selectionArgs, #Nullable String sortOrder) {
SQLiteDatabase database = mDbHelper.getReadableDatabase();
Cursor cursor;
int match = sUriMatcher.match(uri);
switch (match){
case TIME:
cursor = database.query(TimeEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
break;
case TIME_ID:
selection = TimeEntry._ID + "=?";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
cursor = database.query(TimeEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Cannot query with unknown URI: " + uri);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri); //Set notification URI on cursor, so that the cursor will update when the URI is updated.
return cursor;
}
#Nullable
#Override
public Uri insert(#NonNull Uri uri, #Nullable ContentValues values) {
int match = sUriMatcher.match(uri);
switch (match){
case TIME:
return insertTimeRecord(uri, values);
default:
throw new IllegalArgumentException("Insertion is no support for " + uri);
}
}
private Uri insertTimeRecord(Uri uri, ContentValues values){
//Sanity check
if (values.containsKey(TimeEntry.COLUMN_RECORD)){
String time_record = values.getAsString(TimeEntry.COLUMN_RECORD);
if (time_record == null){
throw new IllegalArgumentException("Time record cannot be empty.");
}
}
//If there are no values to update, then don't try to update the database.
if (values.size() == 0){
return null;
}
SQLiteDatabase database = mDbHelper.getWritableDatabase();
long newRowId = database.insert(TimeEntry.TABLE_NAME, null, values);
//Check if the insertion is success.
if (newRowId == -1){
Log.e(TAG, "Insertion fail with " + uri);
return null;
}
getContext().getContentResolver().notifyChange(uri, null); //Notify all listener the data has changed
return ContentUris.withAppendedId(uri, newRowId);
}
#Override
public int update(#NonNull Uri uri, #Nullable ContentValues values, #Nullable String selection, #Nullable String[] selectionArgs) {
int match = sUriMatcher.match(uri);
switch (match){
case TIME:
return updateTimeRecord(uri, values, selection, selectionArgs);
case TIME_ID:
selection = TimeEntry._ID + "=?";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
return updateTimeRecord(uri, values, selection, selectionArgs);
default:
throw new IllegalArgumentException("Update fail with " + uri);
}
}
private int updateTimeRecord(Uri uri, ContentValues values, String selection, String[] selectionArgs){
//Sanity check
if (values.containsKey(TimeEntry.COLUMN_RECORD)){
String time_record = values.getAsString(TimeEntry.COLUMN_RECORD);
if (time_record == null){
return 0; //If the data is not inputted, update nothing in database.
}
}
//If there are no values to update, then don't try to update the database.
if (values.size() == 0){
return 0;
}
SQLiteDatabase database = mDbHelper.getWritableDatabase();
int rowUpdated = database.update(TimeEntry.TABLE_NAME, values, selection, selectionArgs);
if (rowUpdated != 0){
getContext().getContentResolver().notifyChange(uri, null); //Notify all listener the data has changed
}
return rowUpdated;
}
#Override
public int delete(#NonNull Uri uri, #Nullable String selection, #Nullable String[] selectionArgs) {
int match = sUriMatcher.match(uri);
switch (match){
case TIME:
return deleteTimeRecord(uri, selection, selectionArgs);
case TIME_ID:
selection = TimeEntry._ID + "=?";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
return deleteTimeRecord(uri, selection, selectionArgs);
default:
throw new IllegalArgumentException("Delete fail with " + uri);
}
}
private int deleteTimeRecord(Uri uri, String selection, String[] selectionArgs){
SQLiteDatabase database = mDbHelper.getWritableDatabase();
int rowDelected = database.delete(TimeEntry.TABLE_NAME, selection, selectionArgs);
if (rowDelected != 0){
getContext().getContentResolver().notifyChange(uri, null); //Notify all listener the data has changed
}
return rowDelected;
}
#Nullable
#Override
public String getType(#NonNull Uri uri) {
int match = sUriMatcher.match(uri);
switch (match){
case TIME:
return TimeEntry.CONTENT_LIST_TYPE;
case TIME_ID:
return TimeEntry.CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Unknown URI " + uri + " with match " + match);
}
}
}
My Contract class:
package com.example.punchinandout.data;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.BaseColumns;
public class TimeContract {
private TimeContract(){};
public static final String CONTENT_AUTHORITY = "com.example.punchinandout.data";
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
public static final String CONTENT_PATH = "time";
public static final class TimeEntry implements BaseColumns {
public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH);
//MIME type of all records.
public static final String CONTENT_LIST_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + CONTENT_PATH;
//MIME type of single record.
public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + CONTENT_PATH;
public static final String TABLE_NAME = "time";
public static final String _ID = BaseColumns._ID;
public static final String COLUMN_RECORD = "record";
}
}
Can someone help me? :(
I have fixed the problem!!
My provider in Mainifest is that:
<provider
android:authorities="com.example.punchinandout"
android:name=".data.TimeProvider"
android:exported="false"/>
And I change to :
<provider
android:authorities="com.example.punchinandout.data"
android:name=".data.TimeProvider"
android:exported="false"/>
Let the authorities be the same as the authority in Contract class and the problem is fixed!
Sorry for not placed the Mainifest in the question at first time as I had no idea where the problem in my app before.

Accessing data from Custom Content Providers

I've two content provider based apps A and B. Both have their own content providers and are setup for reading data from A and B and vice versa. Everything works fine when the other app is in background. But couldn't find another content provider if the app is killed or not present in background. For example, App B wants to read data from App A. 'B' can read data from 'A' successfully when 'A' is running in background, but gave fatal error (Match uri not found) if 'A' is not running in background.
Any thoughts ?
[EDIT]
I'm getting the same issue as this post.
I've this in both apps' manifest:
<provider
android:name="MyContentProvider"
android:authorities="com.example.${applicationId}-provider"
android:enabled="true"
android:exported="true"
android:grantUriPermissions="true">
</provider>
This the error I'm getting:
Writing exception to parcel
java.lang.IllegalArgumentException: Unsupported URI(Query): content://com.example.appA-provider/appA
at com.example.provider.MyContentProvider.query(MyContentProvider.java:142)
at android.content.ContentProvider.query(ContentProvider.java:1007)
at android.content.ContentProvider$Transport.query(ContentProvider.java:218)
at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:112)
at android.os.Binder.execTransact(Binder.java:461)
Note: This only happens when another app is not in background, otherwise it works as expected (can read each other's data fine).
[EDIT 2]
Here's code for MyContentProvider:
package com.example.provider;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
public class MyContentProvider extends ContentProvider {
private static DatabaseHelper dbHelper;
private static final int ALL_ENTRIES = 1;
private static final int SINGLE_ENTRY = 2;
private String mAuthority = BuildConfig.APPLICATION_ID;
private static UriMatcher uriMatcher;
public Uri CONTENT_URI= null;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
}
public MyContentProvider() {}
public void init(String packageName, String authority) {
if (authority == null) {
setAuthority(packageName, true);
} else {
setAuthority(authority, false);
}
uriMatcher.addURI(getAuthority(), TABLE_NAME, ALL_ENTRIES);
uriMatcher.addURI(getAuthority(), TABLE_NAME + "/#", SINGLE_ENTRY);
CONTENT_URI =
Uri.parse("content://" + getAuthority() + "/" + TABLE_NAME);
}
private void setAuthority(String packageName, boolean isPackageName) {
if (isPackageName) {
mAuthority = packageName + ".myprovider";
} else {
mAuthority = packageName;
}
}
public String getAuthority() {
return mAuthority;
}
public Uri getContentUri() {
return CONTENT_URI;
}
#Override
public boolean onCreate() {
dbHelper = new DatabaseHelper(getContext());
return false;
}
//Return the MIME type corresponding to a content URI
#Override
public String getType(Uri uri) {
if (uri == null) {
throw new IllegalArgumentException("Content uri is null: " + uri);
}
if (uriMatcher == null) {
throw new IllegalArgumentException("Unsupported Match URI: " + uri);
}
switch (uriMatcher.match(uri)) {
case ALL_ENTRIES:
return "vnd.android.cursor.dir/vnd." + getAuthority() + "." + TABLE_NAME;
case SINGLE_ENTRY:
return "vnd.android.cursor.item/vnd." + getAuthority() + "." + TABLE_NAME;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
}
#Override
public Uri insert(Uri uri, ContentValues values) {
Uri _uri = null;
long id = 0;
SQLiteDatabase db = dbHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case ALL_ENTRIES:
case SINGLE_ENTRY:
id = db.insert(TABLE_NAME, null, values);
getContext().getContentResolver().notifyChange(uri, null);
_uri = Uri.parse(CONTENT_URI + "/" + id);
break;
default:
throw new IllegalArgumentException("Unsupported URI (insert): " + uri);
}
return _uri;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
Cursor cursor = null;
String id = null;
switch (uriMatcher.match(uri)) {
case ALL_ENTRIES:
queryBuilder.setTables(TABLE_NAME);
cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
break;
case SINGLE_ENTRY:
queryBuilder.setTables(TABLE_NAME);
id = uri.getPathSegments().get(1);
if (id != null && !id.isEmpty()) {
queryBuilder.appendWhere(TABLE_NAME + "=" + id);
}
cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unsupported URI(Query): " + uri);
}
return cursor;
}
}
You can not initialize the content provider from somewhere else in your code like this, as the ContentProvider might be the first (or only) component of your app that's instantiated.
However, you can read the authority dynamically from the Manifest or a String resource. In my answer on Does Android Content Provider authority definition break the DRY rule? I outlined how we that in our OpenTasks-Provider.
I don't see a call to init() in content provider. Is it only called from elsewhere as part of the application's start ?
If so that may explains why the content provider fails when the application is not already started : In this case the UriMatcher is empty and the switch in the query() method falls back to default witch throws the IllegalArgumentException.
You should either call init() in onCreate() or fully initialize the UriMatcher in the static initializer.
You are setting the authority as
private void setAuthority(String packageName, boolean isPackageName) {
if (isPackageName) {
mAuthority = packageName + ".myprovider";
} else {
mAuthority = packageName;
}
}
So your mAuthority is either com.example.provider or com.example.provider.myprovider
However, you have defined authorities in the manifest as
android:authorities="com.example.${applicationId}-provider"
that is com.example.appA-provider

Android DBFlow and CursorLoader

Anyone knows how to use cursorLoader with DBFlow ? I seen this issue but this is not added to DBFlow.
Thanks.
You can find official docs here or you can implement it the way i have
DBFlow ver used 3
//I have edited my answer & provided easier way for content provider part below
add this to manifest inside application
<provider
android:authorities="com.hashx19.pristinekashmir.mycontentprovider"
android:exported="false"
android:name=".MyContentProvider"/>
Create java file named MyContentProvider & copy below code in it
& replace AUTHORITY ,ENDPOINT, AppDatabase(Your database name) ,TableClassName as per you project.
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.CursorIndexOutOfBoundsException;
import android.net.Uri;
import com.hashx19.pristinekashmir.MySQLiteHelper;
import com.raizlabs.android.dbflow.annotation.ConflictAction;
import com.raizlabs.android.dbflow.config.FlowManager;
import com.raizlabs.android.dbflow.structure.ModelAdapter;
import java.util.Arrays;
import java.util.HashSet;
/**
* Created by Filu on 8/25/2016.
*/
public class MyContentProvider extends ContentProvider {
public static final String AUTHORITY = "com.hashx19.pristinekashmir.mycontentprovider";
private static final String ENDPOOINT = "feeds";
// #ContentUri(path = ENDPOOINT, type = ContentUri.ContentType.VND_MULTIPLE + ENDPOOINT)
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
+ "/" + ENDPOOINT);
private static final int feeds_CONTENT_URI = 0;
private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
static {
MATCHER.addURI(AUTHORITY, ENDPOOINT, feeds_CONTENT_URI);
}
;
#Override
public final String getType(Uri uri) {
String type = null;
switch(MATCHER.match(uri)) {
case feeds_CONTENT_URI: {
type = "vnd.android.cursor.dir/" +ENDPOINT;
break;
}
default: {
throw new IllegalArgumentException("Unknown URI" + uri);
}
}
return type;
}
#Override
public boolean onCreate() {
return false;
}
#Override
public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
android.database.Cursor cursor = null;
switch(MATCHER.match(uri)) {
case feeds_CONTENT_URI: {
cursor = FlowManager.getDatabase("AppDatabase").getWritableDatabase().query("TableClassName", projection, selection, selectionArgs, null, null, sortOrder);
break;
}
}
if (cursor != null) {
cursor.setNotificationUri(getContext().getContentResolver(), uri);
}
return cursor;
}
#Override
public final Uri insert(Uri uri, ContentValues values) {
switch(MATCHER.match(uri)) {
case feeds_CONTENT_URI: {
ModelAdapter adapter = FlowManager.getModelAdapter(FlowManager.getTableClassForName("AppDatabase", "TableClassName"));
final long id = FlowManager.getDatabase("AppDatabase").getWritableDatabase().insertWithOnConflict("TableClassName", null, values, ConflictAction.getSQLiteDatabaseAlgorithmInt(adapter.getInsertOnConflictAction()));
getContext().getContentResolver().notifyChange(uri, null);
return ContentUris.withAppendedId(uri, id);
}
default: {
throw new IllegalStateException("Unknown Uri" + uri);
}
}
}
#Override
public final int delete(Uri uri, String selection, String[] selectionArgs) {
switch(MATCHER.match(uri)) {
case feeds_CONTENT_URI: {
long count = FlowManager.getDatabase("AppDatabase").getWritableDatabase().delete("TableClassName", selection, selectionArgs);
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return (int) count;
}
default: {
throw new IllegalArgumentException("Unknown URI" + uri);
}
}
}
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
switch(MATCHER.match(uri)) {
case feeds_CONTENT_URI: {
ModelAdapter adapter = FlowManager.getModelAdapter(FlowManager.getTableClassForName("AppDatabase", "TableClassName"));
long count = FlowManager.getDatabase("AppDatabase").getWritableDatabase().updateWithOnConflict("TableClassName", values, selection, selectionArgs, ConflictAction.getSQLiteDatabaseAlgorithmInt(adapter.getUpdateOnConflictAction()));
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return (int) count;
}
default: {
throw new IllegalStateException("Unknown Uri" + uri);
}
}
}
}
then when overriding Loader methods do something like this
getLoaderManager().initLoader(1, null, this);
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String selection, sortOrder;
String[] selectionArgs, projection;
selection = ...;
selectionArgs = ...;
sortOrder = ...;
projection= new String[]{"id","date", "link","title","content","excerpt","author",};
CursorLoader cursorLoader = new CursorLoader(getContext(),MyContentProvider.CONTENT_URI, projection,null,null,null);
return cursorLoader;
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
TableClass post = new TableClass();
while (!cursor.isAfterLast()) {
try{
post.setId(cursor.getInt(cursor.getColumnIndex("id")));
}catch (NullPointerException e){
e.printStackTrace();
}catch (CursorIndexOutOfBoundsException c){
c.printStackTrace();
}
}
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
}
editted
Figured out easier way to implement content provider .
add this to your manifest / or modify this way if you already have added Provider code .
modify your AppDatabase Class as
#ContentProvider(authority = AppDatabase.AUTHORITY,
database = AppDatabase.class,
baseContentUri = AppDatabase.BASE_CONTENT_URI)
#Database(name = AppDatabase.NAME, version = AppDatabase.VERSION)
public class AppDatabase {
public static final String NAME = "AppDatabase"; // we will add the .db extension
public static final int VERSION = 2;
public static final String AUTHORITY = "com.hashx19.pristinekashmir.dbflowcontentprovider";
public static final String BASE_CONTENT_URI = "content://"; }
modify each table you want to use as provider as
#TableEndpoint(name = PostData.ENDPOINT, contentProvider = AppDatabase.class)
#Table(database = AppDatabase.class ,allFields = true ,name = PostData.ENDPOINT)
public class PostData extends BaseModel {
public static final String ENDPOINT = "PostData";
#ContentUri(path = ENDPOINT, type = ContentUri.ContentType.VND_MULTIPLE + ENDPOINT)
public static final Uri CONTENT_URI = Uri.parse(AppDatabase.BASE_CONTENT_URI + AppDatabase.AUTHORITY
+ "/" + ENDPOINT);
#PrimaryKey
public int id;
public String image;
}
For using Content provider as in Cursor Loader use TableName.CONTENT_URI as in this case
CursorLoader cursorLoader = new CursorLoader(getContext(),PostData.CONTENT_URI,projection,null,null,null);

android forensic

I would like to retrieve the browser history from the com.android.browser and save it into a file in sdcard. I am able to do soo for the contacts but not the browser history.Don't know what went wrong. They only prompt the error bind or column index out of range: handle 0x27a6d0.
What would be the best format for the file to be xml, txt or??
These are my codes
package fypj.ReadContacts;
import android.app.Activity;
import android.os.Bundle;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import android.app.Activity;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.Browser;
import android.provider.Browser.BookmarkColumns;
import android.provider.ContactsContract;
import android.util.Log;
import android.widget.TextView;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
public class ReadContacts extends Activity {
// To suppress notational clutter and make structure clearer, define some shorthand constants.
private static final Uri URI = ContactsContract.Contacts.CONTENT_URI;
private static final Uri PURI = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
private static final Uri EURI = ContactsContract.CommonDataKinds.Email.CONTENT_URI;
private static final Uri AURI = ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI;
private static final String ID = ContactsContract.Contacts._ID;
private static final String DNAME = ContactsContract.Contacts.DISPLAY_NAME;
private static final String HPN = ContactsContract.Contacts.HAS_PHONE_NUMBER;
private static final String LOOKY = ContactsContract.Contacts.LOOKUP_KEY;
private static final String CID = ContactsContract.CommonDataKinds.Phone.CONTACT_ID;
private static final String EID = ContactsContract.CommonDataKinds.Email.CONTACT_ID;
private static final String AID = ContactsContract.CommonDataKinds.StructuredPostal.CONTACT_ID;
private static final String PNUM = ContactsContract.CommonDataKinds.Phone.NUMBER;
private static final String PHONETYPE = ContactsContract.CommonDataKinds.Phone.TYPE;
private static final String EMAIL = ContactsContract.CommonDataKinds.Email.DATA;
private static final String EMAILTYPE = ContactsContract.CommonDataKinds.Email.TYPE;
private static final String STREET = ContactsContract.CommonDataKinds.StructuredPostal.STREET;
private static final String CITY = ContactsContract.CommonDataKinds.StructuredPostal.CITY;
private static final String STATE = ContactsContract.CommonDataKinds.StructuredPostal.REGION;
private static final String POSTCODE = ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE;
private static final String COUNTRY = ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY;
private static final Uri URI1 = android.provider.Browser.BOOKMARKS_URI;
private static final String bHistoryTITLE = Browser.BookmarkColumns.TITLE;
private static final String bHistoryURL = Browser.BookmarkColumns.URL;
private static final String bHistoryBOOKMARK = Browser.BookmarkColumns.BOOKMARK;
private static final String bHistoryVISITS = Browser.BookmarkColumns.VISITS;
private static final String ID1 = Browser.BookmarkColumns._ID ;
//private static final String BID = Browser.BookmarkColumns.
//private static final String DNAME = Browser.BookmarkColumns.DISPLAY_NAME;
//private static final String HPN = ContactsContract.Contacts.HAS_PHONE_NUMBER;
//private static final String LOOKY = Browser.BookmarkColumns.LOOKUP_KEY;
private String id;
private String lookupKey;
private String name;
private String street;
private String city;
private String state;
private String postcode;
private String country;
private String ph[];
private String phType[];
private String em[];
private String emType[];
private String id1;
private String URL;
private String BTitle;
private String BURL;
private String BBOOKMARK;
private String BVISITS;
private String browserTITLE[];
private String browserURL[];
private String browserBOOKMARK[];
private String browserVISITS[];
private File root;
private int emcounter;
private int phcounter;
private int addcounter;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Check that external media available and writable
checkExternalMedia();
Contacts();
browser();
}
/** Method to check whether external media available and writable and to find the
root of the external file system. */
private void checkExternalMedia () {
// Check external media availability. This is adapted from
// http://developer.android.com/guide/topics/data/data-storage.html#filesExternal
boolean mExternalStorageAvailable = false;
boolean mExternalStorageWriteable = false;
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
// We can read and write the media
mExternalStorageAvailable = mExternalStorageWriteable = true;
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
// We can only read the media
mExternalStorageAvailable = true;
mExternalStorageWriteable = false;
} else {
// Can't read or write
mExternalStorageAvailable = mExternalStorageWriteable = false;
}
// Find the root of the external storage and output external storage info to screen
root = android.os.Environment.getExternalStorageDirectory();
}
public void Contacts(){
// Allow for up to 5 email and phone entries for a contact
em = new String[5];
emType = new String[5];
ph = new String[5];
phType = new String[5];
/** Open a PrintWriter wrapping a FileOutputStream so that we can send output from a
query of the Contacts database to a file on the SD card. Must wrap the whole thing
in a try-catch to catch file not found and i/o exceptions. Note that since we are writing
to external media we must add a WRITE_EXTERNAL_STORAGE permission to the
manifest file. Otherwise a FileNotFoundException will be thrown. */
// This will set up output to /sdcard/download/phoneData.txt if /sdcard is the root of
// the external storage. See the project WriteSDCard for more information about
// writing to a file on the SD card.
File dir = new File (root.getAbsolutePath() + "/download");
dir.mkdirs();
//File file = new File(dir, "phoneData.rtf");
File file = new File(dir, "phoneData.accdb");
try{
FileOutputStream f = new FileOutputStream(file);
PrintWriter pw = new PrintWriter(f);
// Main loop to query the contacts database, extracting the information. See
// http://www.higherpass.com/Android/Tutorials/Working-With-Android-Contacts/
ContentResolver cr = getContentResolver();
Cursor cu = cr.query(URI, null, null, null, null);
if (cu.getCount() > 0) {
// Loop over all contacts
while (cu.moveToNext()) {
// Initialize storage variables for the new contact
street = "";
city = "";
state = "";
postcode = "";
country = "";
// Get ID information (id, name and lookup key) for this contact. id is an identifier
// number, name is the name associated with this row in the database, and
// lookupKey is an opaque value that contains hints on how to find the contact
// if its row id changed as a result of a sync or aggregation.
id = cu.getString(cu.getColumnIndex(ID));
name = cu.getString(cu.getColumnIndex(DNAME));
lookupKey = cu.getString(cu.getColumnIndex(LOOKY));
// Append list of contacts to the scrollable TextView on the screen.
// Query phone numbers for this contact (may be more than one), so use a
// while-loop to move the cursor to the next row until moveToNext() returns
// false, indicating no more rows. Store the results in arrays since there may
// be more than one phone number stored per contact. The if-statement
// enclosing everything ensures that the contact has at least one phone
// number stored in the Contacts database.
phcounter = 0;
if (Integer.parseInt(cu.getString(cu.getColumnIndex(HPN))) > 0) {
Cursor pCur = cr.query(PURI, null, CID + " = ?", new String[]{id}, null);
while (pCur.moveToNext()) {
ph[phcounter] = pCur.getString(pCur.getColumnIndex(PNUM));
phType[phcounter] = pCur.getString(pCur.getColumnIndex(PHONETYPE));
phcounter ++;
}
pCur.close();
}
// Query email addresses for this contact (may be more than one), so use a
// while-loop to move the cursor to the next row until moveToNext() returns
// false, indicating no more rows. Store the results in arrays since there may
// be more than one email address stored per contact.
emcounter = 0;
Cursor emailCur = cr.query(EURI, null, EID + " = ?", new String[]{id}, null);
while (emailCur.moveToNext()) {
em[emcounter] = emailCur.getString(emailCur.getColumnIndex(EMAIL));
emType[emcounter] = emailCur.getString(emailCur.getColumnIndex(EMAILTYPE));
emcounter ++;
}
emailCur.close();
// Query Address (assume only one address stored for simplicity). If there is
// more than one address we loop through all with the while-loop but keep
// only the last one.
addcounter = 0;
Cursor addCur = cr.query(AURI, null, AID + " = ?", new String[]{id}, null);
while (addCur.moveToNext()) {
street = addCur.getString(addCur.getColumnIndex(STREET));
city = addCur.getString(addCur.getColumnIndex(CITY));
state = addCur.getString(addCur.getColumnIndex(STATE));
postcode = addCur.getString(addCur.getColumnIndex(POSTCODE));
country = addCur.getString(addCur.getColumnIndex(COUNTRY));
addcounter ++;
}
addCur.close();
// Write identifiers for this contact to the SD card file
//pw.println(name+" ID="+id+" LOOKUP_KEY="+lookupKey);
pw.println(name);
// Write list of phone numbers for this contact to SD card file
for(int i=0; i<phcounter; i++){
//pw.println(" phone="+ ph[i]+" type="+phType[i] + " ("
//+ getPhoneType(phType[i]) + ") ");
pw.println(" phone="+ ph[i]+" ("
+ getPhoneType(phType[i]) + ") ");
}
// Write list of email addresses for this contact to SD card file
for(int i=0; i<emcounter; i++){
pw.println(" email="+ em[i]+" type="+emType[i] + " ("
+ getEmailType(emType[i]) + ") ");
}
// If street address is stored for contact, write it to SD card file
if(addcounter > 0){
if(street != null) pw.println(" street="+street);
if(city != null) pw.println(" city="+city);
if(state != null) pw.println(" state/region="+state);
if(postcode != null) pw.println(" postcode="+postcode);
if(country != null) pw.println(" country="+country);
}
}
}
// Flush the PrintWriter to ensure everything pending is output before closing
pw.flush();
pw.close();
f.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.i("MEDIA", "*************** File not found. Did you" +
" add a WRITE_EXTERNAL_STORAGE permission to the manifest file? ");
} catch (IOException e) {
e.printStackTrace();
}
}
/** Method to return label corresponding to phone type code. Data for correspondence from
http://developer.android.com/reference/android/provider/ContactsContract.CommonDataKinds.Phone.html */
private String getPhoneType(String index){
if(index.trim().equals( "1")){
return "home";
} else if (index.trim().equals("2")){
return "mobile";
} else if (index.trim().equals("3")){
return "work";
} else if (index.trim().equals("7")){
return "other";
} else {
return "?";
}
}
/** Method to return label corresponding to email type code. Data for correspondence from
http://developer.android.com/reference/android/provider/ContactsContract.
CommonDataKinds.Email.html */
private String getEmailType(String index){
if(index.trim().equals( "1")){
return "home";
} else if (index.trim().equals("2")){
return "work";
} else if (index.trim().equals("3")){
return "other";
} else if (index.trim().equals("4")){
return "mobile";
} else {
return "?";
}
}
public void browser(){
//create file
File dir = new File (root.getAbsolutePath() + "/DATASTOREAGE");
dir.mkdirs();
File file = new File(dir, "browserHistory.rtf");
try{
FileOutputStream f = new FileOutputStream(file);
PrintWriter pw = new PrintWriter(f);
// Main loop to query the contacts database, extracting the information. See
// http://www.higherpass.com/Android/Tutorials/Working-With-Android-Contacts/
ContentResolver cr = getContentResolver();
Cursor cu = cr.query(URI1, null, null, null, null);
if (cu.getCount() > 0) {
// Loop over all contacts
while (cu.moveToNext()) {
// Initialize storage variables for the new contact
BTitle = "";
BURL = "";
BBOOKMARK = "";
BVISITS = "";
// Get ID information (id, name and lookup key) for this contact. id is an identifier
// number, name is the name associated with this row in the database, and
// lookupKey is an opaque value that contains hints on how to find the contact
// if its row id changed as a result of a sync or aggregation.
id1 = cu.getString(cu.getColumnIndex(ID1));
URL = cu.getString(cu.getColumnIndex(BookmarkColumns.TITLE));
//lookupKey = cu.getString(cu.getColumnIndex(LOOKY));
// Append list of contacts to the scrollable TextView on the screen.
// Query phone numbers for this contact (may be more than one), so use a
// while-loop to move the cursor to the next row until moveToNext() returns
// false, indicating no more rows. Store the results in arrays since there may
// be more than one phone number stored per contact. The if-statement
// enclosing everything ensures that the contact has at least one phone
// number stored in the Contacts database.
phcounter = 0;
Cursor bCur = cr.query(URI1, null, ID1, new String[]{id1}, null);
while (bCur.moveToNext()) {
browserTITLE[phcounter] = bCur.getString(bCur.getColumnIndex(bHistoryTITLE));
browserURL[phcounter] = bCur.getString(bCur.getColumnIndex(bHistoryURL));
browserBOOKMARK[phcounter] = bCur.getString(bCur.getColumnIndex(bHistoryBOOKMARK));
browserVISITS[phcounter] = bCur.getString(bCur.getColumnIndex(bHistoryVISITS));
phcounter ++;
}
bCur.close();
}
// Write identifiers for this contact to the SD card file
//pw.println(name+" ID="+id+" LOOKUP_KEY="+lookupKey);
pw.println(BTitle+" ID="+id1+" LOOKUP_KEY="+lookupKey);
pw.println(BURL);
pw.println(BBOOKMARK);
pw.println(BVISITS);
}
// Flush the PrintWriter to ensure everything pending is output before closing
pw.flush();
pw.close();
f.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.i("MEDIA", "*************** File not found. Did you" +
" add a WRITE_EXTERNAL_STORAGE permission to the manifest file? ");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Your selection column is wrong in the second query of URI1 the line should read:
Cursor bCur = cr.query(URI1, null, ID1+"=?", new String[]{id1}, null);
The second query isn't needed though. You are first quering all columns, extracting the ID from all columns. And then doing a nest query on the all columns for a particular id.
You don't need the nest query, the first query will have all the data you need.
The question of the best file format. That would depend on what is going to consume the data you are creating.

How to use QuickSearchBox in my Android application?

I want to implement a search method in my application. I have a database, I want to search in that database using a quick search box. Please help me to achieve this task.
Three files are relevant, DataProvider, DataIndex, changes to the AndroidManifest.
In my case the data objects I want to look for in my database are 'Locations' data objects, hence the name of my classes, but you can apply it for your logic without problem.
LocationProvider.java:
package com.myapp.android.search;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import com.myapp.android.MyApp;
import com.myapp.android.model.Location;
import java.util.ArrayList;
/**
* Provides search suggestions for a list of words and their definitions.
*/
public class LocationProvider extends ContentProvider {
public static String AUTHORITY = "myapp_locations";
private static final int SEARCH_SUGGEST = 0;
private static final int SHORTCUT_REFRESH = 1;
private static final UriMatcher sURIMatcher = buildUriMatcher();
/**
* The columns we'll include in our search suggestions. There are others that could be used
* to further customize the suggestions, see the docs in {#link SearchManager} for the details
* on additional columns that are supported.
*/
private static final String[] COLUMNS = {
"_id", // must include this column
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_TEXT_2,
SearchManager.SUGGEST_COLUMN_INTENT_DATA,
};
/**
* Sets up a uri matcher for search suggestion and shortcut refresh queries.
*/
private static UriMatcher buildUriMatcher() {
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST);
matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT, SHORTCUT_REFRESH);
matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SHORTCUT_REFRESH);
return matcher;
}
#Override
public boolean onCreate() {
Resources resources = getContext().getResources();
// LocationIndex.getInstance(this.getContext()).ensureLoaded(resources);
return true;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
if (!TextUtils.isEmpty(selection)) {
throw new IllegalArgumentException("selection not allowed for " + uri);
}
if (selectionArgs != null && selectionArgs.length != 0) {
throw new IllegalArgumentException("selectionArgs not allowed for " + uri);
}
if (!TextUtils.isEmpty(sortOrder)) {
throw new IllegalArgumentException("sortOrder not allowed for " + uri);
}
switch (sURIMatcher.match(uri)) {
case SEARCH_SUGGEST:
String query = null;
if (uri.getPathSegments().size() > 1) {
query = uri.getLastPathSegment().toLowerCase();
}
return getSuggestions(query, projection);
case SHORTCUT_REFRESH:
String shortcutId = null;
if (uri.getPathSegments().size() > 1) {
shortcutId = uri.getLastPathSegment();
}
return refreshShortcut(shortcutId, projection);
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
}
private Cursor getSuggestions(String query, String[] projection) {
String processedQuery = query == null ? "" : query.toLowerCase();
ArrayList<Location> words = LocationIndex.getInstance().getMatches(processedQuery);
MatrixCursor cursor = new MatrixCursor(COLUMNS);
long id = 0;
for (Location word : words) {
cursor.addRow(columnValuesOfWord(id++, word));
}
return cursor;
}
private Object[] columnValuesOfWord(long id, Location loc) {
return new Object[] {
id, // _id
loc.getTitle(), // text1
loc.getDescription(), // text2
loc.getTitle(), // intent_data (included when clicking on item)
};
}
/**
* Note: this is unused as is, but if we included
* {#link SearchManager#SUGGEST_COLUMN_SHORTCUT_ID} as a column in our results, we
* could expect to receive refresh queries on this uri for the id provided, in which case we
* would return a cursor with a single item representing the refreshed suggestion data.
*/
private Cursor refreshShortcut(String shortcutId, String[] projection) {
return null;
}
/**
* All queries for this provider are for the search suggestion and shortcut refresh mime type.
*/
public String getType(Uri uri) {
switch (sURIMatcher.match(uri)) {
case SEARCH_SUGGEST:
return SearchManager.SUGGEST_MIME_TYPE;
case SHORTCUT_REFRESH:
return SearchManager.SHORTCUT_MIME_TYPE;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
}
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException();
}
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
}
LocationIndex.java
package com.myapp.android.search;
import com.myapp.android.model.Location;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
public class LocationIndex {
private boolean mLoaded = false;
private static final LocationIndex sInstance = new LocationIndex();
private ArrayList<Location> locations;
private final ConcurrentHashMap<String, ArrayList<Location>> mDict = new ConcurrentHashMap<String, ArrayList<Location>>();
public static LocationIndex getInstance() {
return sInstance;
}
public synchronized void loadWords(ArrayList<Location> locations) { //throws IOException, Resources resources
if (mLoaded) return;
this.locations = locations;
for (Iterator<Location> iter=locations.iterator();iter.hasNext();) {
Location loc = iter.next();
if (loc!=null) addLocation(loc);
}
mLoaded = true;
}
public ArrayList<Location> getMatches(String query) {
ArrayList<Location> list = mDict.get(query);
return list == null ? new ArrayList<Location>() : list;
}
private void addLocation(Location loc) {
final int len = loc.getTitle().length();
for (int i = 0; i < len; i++) {
final String prefix = loc.getTitle().substring(0, len - i);
addMatch(prefix, loc);
}
}
private void addMatch(String query, Location loc) {
ArrayList<Location> matches = mDict.get(query);
if (matches == null) {
matches = new ArrayList<Location>();
mDict.put(query.toLowerCase(), matches);
}
matches.add(loc);
}
public ConcurrentHashMap<String, ArrayList<Location>> getmDict() {
return mDict;
}
}
AndroidManifest.xml
add the following to your manifest...
<!-- Provides search suggestions for words and their definitions. -->
<provider android:name=".search.LocationProvider"
android:authorities="myapp_locations"
android:syncable="false"/>
<provider android:name=".content.LocalFileContentProvider"
android:authorities="com.myapp.android.localfile"
android:syncable="false"/>
and the intent filter in the activities you want to activate the search for (maybe all):
<intent-filter>
<action android:name="android.intent.action.SEARCH"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>

Categories

Resources