I'm currently testing Android's ContentProvider and running into some issues mocking it. I've already spent days on the internet but didn't find any useful examples except of the introduction on the Android Developer Site.
I implemented a ContentProvider and wrote some tests. Everything works here completely fine. The result of the tests are matching my expectations.
public class DirectMessageProviderTest extends ProviderTestCase2<DirectMessageProvider>{
#SmallTest
public void testInsert() {
Log.d(TAG, "testInsert");
/*
* result retrieves twitterId for the newly inserted item
*/
Uri result = provider.insert(
DirectMessageProvider.CONTENT_URI,
createContentValues());
Log.i(TAG, "INSERT; id for newly inserted item: " + result);
assertNotNull("INSERT!!! failed", result);
if (result != null) {
isDirectMessageInserted = true;
}
}
#SmallTest
public void testQuery() {
Log.d(TAG, "testQuery");
Uri uri = Uri.withAppendedPath(
DirectMessageProvider.CONTENT_URI,
String.valueOf(directMessage.getTwitterId()));
/*
* result retrieves a cursor or null
*/
Cursor result = provider.query(
uri,
null,
null,
null,
null);
Log.i(TAG, "QUERY; number of rows inside the cursor: " + result.getCount());
int expected = isDirectMessageInserted ? 1 : 0;
assertEquals("QUERY!!! failed", expected, result.getCount());
}
}
I also implemented a class which capsules the ContentProvider and provides more complex methods than delete, insert, query and update. Take a look!
public class DirectMessageDataAccessImpl implements
DirectMessageDataAccessInterface {
#Override
public boolean isStored(TwitterDirectMessage directMessage)
throws DataAccessException {
Log.d(TAG, "isStored");
try {
Uri uri = Uri.withAppendedPath(
DirectMessageProvider.CONTENT_URI,
String.valueOf(directMessage.getTwitterId()));
Cursor cursor = resolver.query(
uri,
null,
null,
null,
null);
cursor.moveToFirst();
boolean result = (cursor.getCount() > 0 ? true: false);
cursor.close();
return result;
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
throw new DataAccessException(e.getMessage());
}
}
}
Finally we`re coming to my problem writing a test for this class. I want to write independent tests for this class using a mocked ContentResolver. I've found this [example] (http://www.androidadb.com/source/npr-android-app-read-only/Npr_Test/src/org/npr/android/util/PlaylistProviderTest.java.html) on the internet and tried to use MockContentResolver.
First of all, I created a new ContentProvider which will retrieve the delete, insert, query and update calls from the tested class, to create the same answer again and again.
public class DirectMessageDataAccessTest extends ProviderTestCase2<DirectMessageProvider>{
private ContentProvider provider = new ContentProvider() {
#Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
Log.d(TAG, "update");
return 0;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Log.w(TAG, "query");
return null;
}
#Override
public boolean onCreate() {
Log.d(TAG, "onCreate");
return false;
}
#Override
public Uri insert(Uri uri, ContentValues values) {
Log.d(TAG, "insert");
return null;
}
#Override
public String getType(Uri uri) {
Log.d(TAG, "getType");
return null;
}
#Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
Log.d(TAG, "delete");
return 0;
}
};
}
After this, I modified my setUp Method in order to create a MockContentResolver and added my ContentProvider above
#Override
protected void setUp() throws Exception {
super.setUp();
/*
* MockContentResolver overrides Android's normal way of resolving providers by authority
*/
MockContentResolver resolver = new MockContentResolver();
/*
* Adds access to a provider based on its authority
*/
resolver.addProvider(DirectMessageProvider.AUTHORITY, provider);
context = new IsolatedContext(resolver, getContext());
this.setContext(context);
}
Last but not least, I wrote a test case for my isStored Method.
public void testIsStored() {
Log.d(TAG, "testIsStored");
TwitterDirectMessage directMessage = new TwitterDirectMessage();
directMessage.setTwitterId(123456);
DirectMessageDataAccessInterface dataAccess =
new DirectMessageDataAccessImpl(context);
try {
assertFalse(dataAccess.isStored(directMessage));
} catch (DataAccessException e) {
e.printStackTrace();
fail("...");
}
}
Unfortunately, android.content.ContentProvider always throws a NullPointerException. I'm calling resolver.query(...)in line 72 of DirectMessageDataAccessImpl
E/DirectMessageDataAccessImpl(1758): null
E/DirectMessageDataAccessImpl(1758): java.lang.NullPointerException
E/DirectMessageDataAccessImpl(1758): at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:284)
E/DirectMessageDataAccessImpl(1758): at android.content.ContentProvider$Transport.query(ContentProvider.java:177)
E/DirectMessageDataAccessImpl(1758): at android.content.ContentResolver.query(ContentResolver.java:311)
E/DirectMessageDataAccessImpl(1758): at xxx.xxxxx.xxxxxxx.app.database.dataaccess.implementation.DirectMessageDataAccessImpl.isStored(DirectMessageDataAccessImpl.java:72)
E/DirectMessageDataAccessImpl(1758): at xxx.xxxxx.xxxxxxx.app.test.dataaccess.DirectMessageDataAccessTest.testIsStored(DirectMessageDataAccessTest.java:107)
In constructor you should set
public YourClass() {
super(DBProvider.class, "com.yourpackage.main");// class of Your Content provider and application package
}
and then just call
MockContentResolver mockContentResolver = getMockContentResolver();
assertNotNull(mockContentResolver);
in a tests
Related
I have two app, from another app I want to get all images,
suppose there is StickerProvider and MainActivity different app
StickerProvider is ContentProvider, MainActivity has ContentResolver
StickerProvider App has Asset Folder
Asset----> Stickers ----------> a.png, b.png
public class StickerProvider extends ContentProvider {
private final static String LOG_TAG = StickerProvider.class.getName();
private static final String[] COLUMNS = {
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
#Override
public boolean onCreate() {
return true;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
/**
* Source: {#link FileProvider#query(Uri, String[], String, String[], String)} .
*/
if (projection == null) {
projection = COLUMNS;
}
String[] images = new String[0];
try {
images = getContext().getAssets().list("stickers");
ArrayList<String> listImages = new ArrayList<String>(Arrays.asList(images));
final MatrixCursor cursor = new MatrixCursor(new String[]{"path"}, 1);
for (int i = 0; i < listImages.size(); i++) {
cursor.addRow(new String[]{listImages.get(i)});
}
return cursor;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
#Override
public String getType(Uri uri) {
/**
* Source: {#link FileProvider#getType(Uri)} .
*/
final String file_name = uri.getLastPathSegment();
final int lastDot = file_name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = file_name.substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) {
return mime;
}
}
return "image/png";
}
#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) {
return 0;
}
}
It's menifest file is
<provider
android:name="com.example.StickerProvider"
android:authorities="com.example"
android:exported="true"
android:grantUriPermissions="true"
android:label="StickerProvider" />
Another app is MainActivity ------> From this I want to fetch all images of above apps
public class MainActivity extends AppCompatActivity {
int i=0;
private static final String TAG = "MainActivity";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView ivOne = (ImageView)findViewById(R.id.ivOne);
ImageView ivTwo = (ImageView)findViewById(R.id.ivTwo);
try {
ContentResolver resolver = getContentResolver();
Cursor cursor =
resolver.query(Uri.parse("content://com.example/stickers"),
null,
null,
null,
null);
if (cursor.moveToFirst()) {
do {
String word = cursor.getString(0);
if(i==0){
ivOne.setImageURI(Uri.parse("content://com.example/stickers/"+word));
}else if(i==1){
ivTwo.setImageURI(Uri.parse("content://com.example/stickers/"+word));
}
// do something meaningful
Log.d(TAG, "onCreate: "+word);
} while (cursor.moveToNext());
}
}catch(Exception e){
e.printStackTrace();
}
}
}
When ever I start MainActivity App I am getting below exception
Unable to open content: content://com.example/a.png
java.io.FileNotFoundException: No files supported by provider at content://com.example/a.png
at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:144)
at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:692)
at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1149)
at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:986)
at android.content.ContentResolver.openInputStream(ContentResolver.java:706)
at android.widget.ImageView.getDrawableFromUri(ImageView.java:900)
at android.widget.ImageView.resolveUri(ImageView.java:871)
at android.widget.ImageView.setImageURI(ImageView.java:490)
at android.support.v7.widget.AppCompatImageView.setImageURI(AppCompatImageView.java:124)
at com.pixr.photo.collage.MainActivity.onCreate(MainActivity.java:36)
at android.app.Activity.performCreate(Activity.java:6684)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1119)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2652)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2766)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1507)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6236)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:891)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:781)
ivOne.setImageURI(Uri.parse("content://com.example/stickers/"+word));
Your ContentProvider does not support this. It does not implement openFile() or any of its variants.
Either:
Use an existing ContentProvider that offers support for serving assets, such as my StreamProvider, or
Augment your provider to support openFile(), such as I do in this sample app
ic_launcher is related with app icon. There is no app icon. Check your icon in your drawable directory.
I'm trying to make a call to my contentResolver that accesses my sql DB in a reactive way. I know sqlBrite exists, but I have to use sqLite. I'm new to rxJava(2) and somehow stitched together 3 calls that all work as expected magically. I don't know which one to use. I'm using rxJava2 and some of the articles I read that brought me to this point seemed old. What's the preferred way to accomplish this? I'm also not using retroLambda...baby steps for me(I'll admit, it does make things look really nice though).
This is the starting call and subscribe function:
Observable<Cursor> dbObserver = mTmdbDatabaseService.getCursor1(123456);
dbObserver.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(testDBObserver());
private Observer<Cursor> testDBObserver() {
return new Observer<Cursor>() {
#Override
public void onSubscribe(Disposable d) {
Log.d(TAG, "testDBObserver.onSubscribe");
//Save the disposable to remove it later onDestroy
mCompositeDisposable.add(d);
}
#Override
public void onNext(Cursor c) {
int num = c.getCount();
Log.d(TAG, "testDBObserver.onNext: " + c.getCount());
}
#Override
public void onError(Throwable e) {
Log.d(TAG, "testDBObserver.onError");
}
#Override
public void onComplete() {
Log.d(TAG, "testDBObserver.onComplete");
}
};
}
These are the three RxJava functions that for now query my DB:
public Observable getCursor1(final int value) {
Observable<Cursor> cursorObservable = Observable.fromCallable(new Callable<Cursor>() {
#Override
public Cursor call() throws Exception {
int id = value;
String stringId = Integer.toString(id);
Uri uri = MovieContract.MovieEntry.CONTENT_URI;
uri = uri.buildUpon().appendPath(stringId).build();
Cursor c = mContext.getContentResolver().query(uri,
null,
null,
null,
MovieContract.MovieEntry.COLUMN_MOVIE_ID);
return c;
}
});
return cursorObservable;
}
public Observable<Cursor> getCursor2(final int value) {
return Observable.defer(new Callable<ObservableSource<? extends Cursor>>() {
#Override
public ObservableSource<? extends Cursor> call() throws Exception {
int id = value;
String stringId = Integer.toString(id);
Uri uri = MovieContract.MovieEntry.CONTENT_URI;
uri = uri.buildUpon().appendPath(stringId).build();
Cursor c = mContext.getContentResolver().query(uri,
null,
null,
null,
MovieContract.MovieEntry.COLUMN_MOVIE_ID);
return Observable.just(c);
}
});
}
public Observable<Cursor> getCursor3(final int value) {
Observable<Cursor> observable = Observable.create(new ObservableOnSubscribe<Cursor>() {
#Override
public void subscribe(#NonNull ObservableEmitter<Cursor> subscriber) throws Exception {
int id = value;
String stringId = Integer.toString(id);
Uri uri = MovieContract.MovieEntry.CONTENT_URI;
uri = uri.buildUpon().appendPath(stringId).build();
Cursor c = mContext.getContentResolver().query(uri,
null,
null,
null,
MovieContract.MovieEntry.COLUMN_MOVIE_ID);
subscriber.onNext(c);
subscriber.onComplete();
}
});
return observable;
}
Observable.fromCallable is the best choice here because it just fits your needs: execute some code and return a value. defer and create are meant for more complex cases such as wrapping callbacks or manipulating the lifecycle of the stream.
Tip: since you are using a Cursor, I think a better design solution would be to emit exact data from your data source (instead of cursor itself) and close the cursor onsite:
Observable<Result> cursorObservable = Observable.fromCallable ... {
#Override
public Cursor call() throws Exception {
...
Cursor c = ...
Result result = ... //get your data from the cursor
c.close();
return result;
}
});
P.S. SQLBrite is just a reactive wrapper around SQLite.
I'm using ProviderTestCase2 to test my ContentProvider, LinesCP, but calling getMockContentResolver() returns null.
The class path to LinesCP is 'com.olfybsppa.inglesaventurero/start/LinesCP.java'. This is a segment of the code showing the AUTHORITY.
public class LinesCP extends ContentProvider {
private static final String database_name = "lines.db";
public static final String AUTHORITY = "com.olfybsppa.inglesaventurero.start.LinesCP";
I am running it using BuildVariants Unit Test, my other unit tests work. Here is the test that isn't working.
public class ContentProviderUTest extends ProviderTestCase2<LinesCP> {
private static String ALPHABET = "AL-00";
private MockContentResolver cr;
public ContentProviderUTest () {
super(LinesCP.class, "com.olfybsppa.inglesaventurero.start");
//super(LinesCP.class, LinesCP.AUTHORITY);
}
public ContentProviderUTest (Class<LinesCP> providerClass, String providerAuthority) {
super(providerClass, providerAuthority);
}
#Override
protected void setUp() throws Exception {
super.setUp();
cr = getMockContentResolver(); // cr is null.
}
public void testSceneInsert() {
Uri uri = LinesCP.sceneTableUri;
ContentValues sceneValues = new ContentValues();
sceneValues.put(LinesCP.scene_id, ALPHABET);
sceneValues.put(LinesCP.english_title, "Alphabet");
sceneValues.put(LinesCP.spanish_title, "Alphabeto");
MockContentResolver cr = getMockContentResolver();
Uri resultingUri = getMockContentResolver().insert(uri, sceneValues);
assertNotNull(resultingUri);
long rowId = ContentUris.parseId(resultingUri);
assertTrue(rowId > 0);
}
}
I really think this should be working, any ideas welcome.
Quick Orientation: My ContentProvider is named LinesCP. LinesCP holds a table of CPHints. CPHint is my own class. My Test is ProviderInsertHintsTest which extends ProviderTestCase2
Even though ProviderTestCase2 does not inherit from InstrumentationTestCase, I am running ProviderInsertHintsTest using the Build Variants 'Android Instrumentation Tests'. ProviderInsertHintsTest is in my src/androidTest/java/ folder. So, I previously thought ProviderTestCase2 should be run with BuildVariants 'Unit Test', and I think that was my main mistake.
In ProviderInsertHintsTest, I test LineCP's insert(Uri, ContentValues) method. I get mMockResolver in setUp(). I use mMockResolver to insert a ContentValues I made from hint1. Then I use mMockResolver.query to get 'fromCP' back from the Content Provider. Then I assert that the original 'hint1' is equal to 'fromCP'.
public class ProviderInsertHintsTest extends ProviderTestCase2<LinesCP> {
private MockContentResolver mMockResolver;
private CPHint hint1;
private CPHint hint2;
public ProviderInsertHintsTest() {
super(LinesCP.class, LinesCP.AUTHORITY);
}
#Override
protected void setUp() throws Exception {
super.setUp();
mMockResolver = getMockContentResolver();
hint1 = new CPHint(1);
hint1.setNormalStartTime(1001);
hint1.setNormalEndTime(1010);
hint2 = new CPHint(2);
hint2.setNormalStartTime(2001);
hint2.setNormalEndTime(2010);
}
#Override
protected void tearDown() throws Exception {
super.tearDown();
}
public void testInsertHint () {
ContentValues cv = hint1.getContentValues(111);
mMockResolver.insert(LinesCP.hintTableUri, cv);
Cursor cursor = mMockResolver.query(LinesCP.hintTableUri, null, null, null, null);
cursor.moveToFirst();
assertEquals(1, cursor.getCount());
CPHint fromCP = CPHint.extractCPHint(cursor);
cursor.close();
assertTrue(fromCP.equals(hint1));
}
public void testInsertTwoHintsDeleteOne () {
ContentValues cv1 = hint1.getContentValues(111);
ContentValues cv2 = hint2.getContentValues(111);
mMockResolver.insert(LinesCP.hintTableUri, cv1);
mMockResolver.insert(LinesCP.hintTableUri, cv2);
Cursor cursor1 = mMockResolver.query(LinesCP.hintTableUri, null, null, null, null);
assertEquals(2, cursor1.getCount());
cursor1.close();
mMockResolver.delete(LinesCP.hintTableUri, Ez.where(BaseColumns._ID, "" + 1), null);
Cursor cursor2 = mMockResolver.query(LinesCP.hintTableUri, null, null, null, null);
assertEquals(1, cursor2.getCount());
cursor2.close();
}
}
The only leap of faith is the CPHint.extractCPHint(Cursor cursor). It is only for drying up the code. It asks what is the value at LineCP's column index that matches the column name, then creates a new CPHint. Here it is, just in case.
public static CPHint extractCPHint(Cursor cursor) {
Integer position = cursor.getInt(cursor.getColumnIndex(LinesCP.pos_id));
CPHint hint = new CPHint(position);
hint.setTimes(cursor.getLong(cursor.getColumnIndex(LinesCP.normal_start_time)),
cursor.getLong(cursor.getColumnIndex(LinesCP.normal_end_time)),
return hint;
}
To test delete, I take mMockResolver and add hint1 and hint2 to the ContentProvider. Then I delete the CPHint that matches BaseColumns.ID equal to 1. Then I query the Content Provider again and assert that it only has one row, where as before it had two rows.
codes:
First my Uris
public static final String PACKAGE = "my.url.contentprovider";
public static final String TABLE_NAME = "NetworkTransaction";
public static final String AUTHORITY = PACKAGE + ".NetTransContentProvider";
public static final Uri BASE_URI = Uri.parse("content://"+AUTHORITY);
public static final Uri CONTENT_URI_ANY_OBSERVER = Uri.withAppendedPath(BASE_URI,TABLE_NAME+"/*");
public static final Uri CONTENT_URI_FIND_BY_ID = Uri.withAppendedPath(BASE_URI,TABLE_NAME+"/FIND/ID");
public static final Uri CONTENT_URI_INSERT_OR_REPLACE_BY_ID = Uri.withAppendedPath(BASE_URI,TABLE_NAME+"/INSERT/REPLACE/ID");
public static final Uri CONTENT_URI_INSERT_BY_ID = Uri.withAppendedPath(BASE_URI,TABLE_NAME+"/INSERT/ID");
and my loader from activity code:
#Override
protected void onResume() {
super.onResume();
getSupportLoaderManager().restartLoader(NET_TRANS_LOADER_ID,mBundle,this).forceLoad();
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
Uri uri = null;
CursorLoader cl=null;
switch (id) {
case NET_TRANS_LOADER_ID:
uri = NetTransContentProvider.CONTENT_URI_FIND_BY_ID;
cl = new CursorLoader(ChoosingUserNameActivity.this, uri, NetTransDbUtils.allColumns,
NetTransDbUtils.COLUMN_ID + " = ? ",
new String[]{String.valueOf(bundle.getLong(EXTRA_TRANSACTION_ID,-1))}, null);
break;
default:
break;
}
return cl;
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
final int loaderId = loader.getId();
switch (loaderId) {
case NET_TRANS_LOADER_ID:
if(mTransactionId != null){
NetTrans netTrans = NetTransDbUtils.cursorToNetTrans(cursor);
if(netTrans != null && netTrans.getStatus() != null
&& !netTrans.getStatus().equals(NetTrans.STATUS_PENDING)){
EventBus.getDefault().post(new NetTransMsg(true, mTransactionId, netTrans.getMessage()));
}
}
break;
default:
break;
}
}
and at a runnable that runs on ExecutorService in a started service I call
mContext.getContentResolver().insert(NetTransContentProvider.CONTENT_URI_INSERT_OR_REPLACE_BY_ID, cv );
the value inserted but the loader dose not call:
#Override
public Uri insert(Uri uri, ContentValues values) {
int uriType = sUriMatcher.match(uri);
switch (uriType) {
case INSERT_OR_REPLACE_BY_ID:
mDatabase.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
break;
case INSERT_BY_ID:
mDatabase.insert(TABLE_NAME, null, values);
break;
default:
break;
}
getContext().getContentResolver().notifyChange(CONTENT_URI_ANY_OBSERVER, null);
return null;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder) {
Cursor cursor = mDatabase.query(TABLE_NAME,projection,selection, selectionArgs, null, null, null);
cursor.setNotificationUri(getContext().getContentResolver(), CONTENT_URI_ANY_OBSERVER);
return cursor;
}
my problem is getContext().getContentResolver().notifyChange(CONTENT_URI_ANY_OBSERVER, null); in the insert method can not make my loader restart.
UPDATE
I have created a sample project, you press the button, a new NetTrans object is created and written into database, then the thread sleeps 5000 ms and overwrites that value (to simulate network operation). but after that loader dose not restart. Where is my bug ?
If you want observers registered on CONTENT_URI_ANY_OBSERVER to be notified when a change happens on CONTENT_URI_FIND_BY_ID, you need to make sure of two things.
First, the CONTENT_URI_ANY_OBSERVER needs to be a parent of CONTENT_URI_FIND_BY_ID. If you think of it like folders on a file system, 'CONTENT_URI_ANY_OBSERVER' should contain `CONTENT_URI_FIND_BY_ID' in one of its sub folders.
Second, you must pass true for the notifyDescendants argument when registering your content observer.
There is no wild card considerations when Android attempts to find matching content observers (the link provided in the comments only applies to the UriMatcher). So, to fix your problem you should remove the /* from your CONTENT_URI_ANY_OBSERVER and it should start matching. You can see how my.url.contentprovider/NetworkTransaction is now a parent "folder" of my.url.contentprovider/NetworkTransaction/INSERT/REPLACE/ID where as before you had my.url.contentprovider/NetworkTransaction/*.
EDIT 1
After reviewing your sample project, I have found the other area where your problem lies. When you use a cursor loader, the cursor is owned by the loader. This means that you shouldn't alter it in anyway aside from just iterating through its data. In your NetTransDbUtils.cursorToNetTrans(cursor) method, you are closing your cursor which will prevent the CursorLoader from being able to effectively monitor changes to your cursor's data.
Simple answer: Don't call close cursor.close() in NetTransDbUtils.cursorToNetTrans(cursor); for this use case.
Replace your Uris with a simple parse:
public static final Uri CONTENT_URI_ANY_OBSERVER = Uri
.parse(BASE_URI + "/" + TABLE_NAME);
public static final Uri CONTENT_URI_FIND_BY_ID = Uri
.parse(BASE_URI + "/" + TABLE_NAME + "/FIND/ID");
public static final Uri CONTENT_URI_INSERT_OR_REPLACE_BY_ID = Uri
.parse(BASE_URI + "/" + TABLE_NAME + "/INSERT/REPLACE/ID");
public static final Uri CONTENT_URI_INSERT_BY_ID = Uri
.parse(BASE_URI + "/" + TABLE_NAME + "/INSERT/ID");
In your RestService class, use a handler for callbacks:
private Handler mHandler;
#Override
public void onCreate() {
...
mHandler = new Handler();
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
...
mHandler.post(task);
...
}
I am trying to create a button in my android app that allows the user to share an image using their choice of social media network. The image file is stored in the assets folder of the app.
My plan is to implement a custom ContentProvider to give external access to the image, then send a TYPE_SEND intent specifying the uri of the image within my content provider.
I have done this and it works for Google+ and GMail, but for other services it fails. The hardest part has been finding information on what I'm supposed to return from the query() method of my ContentProvider. Some apps specify a projection (e.g. Google+ asks for _id and _data), while some apps pass null as the projection. Even where the projection is specified, I've no idea what actual data (types) are expected in the columns. I can find no documentation on this.
I have also implemented the openAssetFile method of the ContentProvider and this gets called (twice by Google+!) but then inevitably the query method get called as well. Only the result of the query method seems to count.
Any ideas where I'm going wrong? What should I be returning from my query method?
Code below:
// my intent
Intent i = new Intent(android.content.Intent.ACTION_SEND);
i.setType("image/jpeg");
Uri uri = Uri.parse("content://com.me.provider/ic_launcher.jpg");
i.putExtra(Intent.EXTRA_STREAM, uri);
i.putExtra(android.content.Intent.EXTRA_TEXT, text);
startActivity(Intent.createChooser(i, "Share via"));
// my custom content provider
public class ImageProvider extends ContentProvider
{
private AssetManager _assetManager;
public static final Uri CONTENT_URI = Uri.parse("content://com.me.provider");
// not called
#Override
public int delete(Uri arg0, String arg1, String[] arg2)
{
return 0;
}
// not called
#Override
public String getType(Uri uri)
{
return "image/jpeg";
}
// not called
#Override
public Uri insert(Uri uri, ContentValues values)
{
return null;
}
#Override
public boolean onCreate()
{
_assetManager = getContext().getAssets();
return true;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
{
MatrixCursor c = new MatrixCursor(new String[] { "_id", "_data" });
try
{
// just a guess!! works for g+ :/
c.addRow(new Object[] { "ic_launcher.jpg", _assetManager.openFd("ic_launcher.jpg") });
} catch (IOException e)
{
e.printStackTrace();
return null;
}
return c;
}
// not called
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
{
return 0;
}
// not called
#Override
public String[] getStreamTypes(Uri uri, String mimeTypeFilter)
{
return new String[] { "image/jpeg" };
}
// called by most apps
#Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException
{
try
{
AssetFileDescriptor afd = _assetManager.openFd("ic_launcher.jpg");
return afd;
} catch (IOException e)
{
throw new FileNotFoundException("No asset found: " + uri);
}
}
// not called
#Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException
{
return super.openFile(uri, mode);
}
}
Thanks, your question solved mine ;)
I was having the exactly inverse problem of yours: every service would work except g+.
I was returning null in the query method, that made g+ crash.
The only thing to actually expose my images was to implement openFile().
I have my images stored on the filesystem, not in the assets, but I suppose you
could get a ParcelFileDescriptor from your AssetFileDescriptor and return it.
My openFile() method looks like this:
#Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
String path = uri.getLastPathSegment();
if (path == null) {
throw new IllegalArgumentException("Not a Path");
}
File f = new File(getContext().getFilesDir() + File.separator + "solved" + path + ".jpg");
int iMode;
if ("r".equals(mode)) {
iMode = ParcelFileDescriptor.MODE_READ_ONLY;
} else if ("rw".equals(mode)) {
iMode = ParcelFileDescriptor.MODE_READ_WRITE;
} else if ("rwt".equals(mode)) {
iMode = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_TRUNCATE;
} else {
throw new IllegalArgumentException("Invalid mode");
}
return ParcelFileDescriptor.open(f, iMode);
}
This works for every service I have installed with the ACTION_SEND intents except g+.
Using your query method makes it work for google plus, too.