Improve photos content observer demo.

This is now much more like one would write to look for
photos being added/changed.  It looks at the URIs that have
changed and filters them to only files that it cares about
(in this case those under DCIM, that is photos added by the
camera), and also explicitly understands the difference
between individual URIs changing and a more general change
requiring a full re-query of the provider.

Change-Id: I509570aa66839f8b84b5a065c4b4e49fbd0f1c29
This commit is contained in:
Dianne Hackborn
2016-04-06 15:24:35 -07:00
parent a879ac494a
commit 96370959b6
2 changed files with 138 additions and 22 deletions

View File

@@ -16,7 +16,9 @@
package com.example.android.apis.content; package com.example.android.apis.content;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.pm.PackageManager;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
@@ -28,6 +30,8 @@ import android.widget.TextView;
import com.example.android.apis.R; import com.example.android.apis.R;
public class MediaContentObserver extends Activity { public class MediaContentObserver extends Activity {
public static final int REQ_PHOTOS_PERM = 1;
ContentObserver mContentObserver; ContentObserver mContentObserver;
View mScheduleMediaJob; View mScheduleMediaJob;
View mCancelMediaJob; View mCancelMediaJob;
@@ -74,9 +78,15 @@ public class MediaContentObserver extends Activity {
mSchedulePhotosJob.setOnClickListener(new View.OnClickListener() { mSchedulePhotosJob.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
REQ_PHOTOS_PERM);
} else {
PhotosContentJob.scheduleJob(MediaContentObserver.this); PhotosContentJob.scheduleJob(MediaContentObserver.this);
updateButtons(); updateButtons();
} }
}
}); });
mCancelPhotosJob.setOnClickListener(new View.OnClickListener() { mCancelPhotosJob.setOnClickListener(new View.OnClickListener() {
@Override @Override
@@ -91,6 +101,17 @@ public class MediaContentObserver extends Activity {
mContentObserver); mContentObserver);
} }
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
if (requestCode == REQ_PHOTOS_PERM) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
PhotosContentJob.scheduleJob(MediaContentObserver.this);
updateButtons();
}
}
}
void updateButtons() { void updateButtons() {
if (MediaContentJob.isScheduled(this)) { if (MediaContentJob.isScheduled(this)) {
mScheduleMediaJob.setEnabled(false); mScheduleMediaJob.setEnabled(false);

View File

@@ -22,7 +22,9 @@ import android.app.job.JobScheduler;
import android.app.job.JobService; import android.app.job.JobService;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Environment;
import android.os.Handler; import android.os.Handler;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
@@ -30,12 +32,47 @@ import android.widget.Toast;
import com.example.android.apis.R; import com.example.android.apis.R;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* Stub job to execute when there is a change to photos in the media provider. * Stub job to execute when there is a change to photos in the media provider.
*/ */
public class PhotosContentJob extends JobService { public class PhotosContentJob extends JobService {
// The root URI of the media provider, to monitor for generic changes to its content.
static final Uri MEDIA_URI = Uri.parse("content://" + MediaStore.AUTHORITY + "/");
// Path segments for image-specific URIs in the provider.
static final List<String> EXTERNAL_PATH_SEGMENTS
= MediaStore.Images.Media.EXTERNAL_CONTENT_URI.getPathSegments();
// The columns we want to retrieve about a particular image.
static final String[] PROJECTION = new String[] {
MediaStore.Images.ImageColumns._ID, MediaStore.Images.ImageColumns.DATA
};
static final int PROJECTION_ID = 0;
static final int PROJECTION_DATA = 1;
// This is the external storage directory where cameras place pictures.
static final String DCIM_DIR = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DCIM).getPath();
// A pre-built JobInfo we use for scheduling our job.
static final JobInfo JOB_INFO;
static {
JobInfo.Builder builder = new JobInfo.Builder(R.id.schedule_photos_job,
new ComponentName("com.example.android.apis", PhotosContentJob.class.getName()));
// Look for specific changes to images in the provider.
builder.addTriggerContentUri(new JobInfo.TriggerContentUri(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
// Also look for general reports of changes in the overall provider.
builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MEDIA_URI, 0));
JOB_INFO = builder.build();
}
// Fake job work. A real implementation would do some work on a separate thread.
final Handler mHandler = new Handler(); final Handler mHandler = new Handler();
final Runnable mWorker = new Runnable() { final Runnable mWorker = new Runnable() {
@Override public void run() { @Override public void run() {
@@ -46,17 +83,14 @@ public class PhotosContentJob extends JobService {
JobParameters mRunningParams; JobParameters mRunningParams;
// Schedule this job, replace any existing one.
public static void scheduleJob(Context context) { public static void scheduleJob(Context context) {
JobScheduler js = context.getSystemService(JobScheduler.class); JobScheduler js = context.getSystemService(JobScheduler.class);
JobInfo.Builder builder = new JobInfo.Builder(R.id.schedule_photos_job, js.schedule(JOB_INFO);
new ComponentName(context, PhotosContentJob.class));
builder.addTriggerContentUri(new JobInfo.TriggerContentUri(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
js.schedule(builder.build());
Log.i("PhotosContentJob", "JOB SCHEDULED!"); Log.i("PhotosContentJob", "JOB SCHEDULED!");
} }
// Check whether this job is currently scheduled.
public static boolean isScheduled(Context context) { public static boolean isScheduled(Context context) {
JobScheduler js = context.getSystemService(JobScheduler.class); JobScheduler js = context.getSystemService(JobScheduler.class);
List<JobInfo> jobs = js.getAllPendingJobs(); List<JobInfo> jobs = js.getAllPendingJobs();
@@ -71,6 +105,7 @@ public class PhotosContentJob extends JobService {
return false; return false;
} }
// Cancel this job, if currently scheduled.
public static void cancelJob(Context context) { public static void cancelJob(Context context) {
JobScheduler js = context.getSystemService(JobScheduler.class); JobScheduler js = context.getSystemService(JobScheduler.class);
js.cancel(R.id.schedule_photos_job); js.cancel(R.id.schedule_photos_job);
@@ -80,29 +115,89 @@ public class PhotosContentJob extends JobService {
public boolean onStartJob(JobParameters params) { public boolean onStartJob(JobParameters params) {
Log.i("PhotosContentJob", "JOB STARTED!"); Log.i("PhotosContentJob", "JOB STARTED!");
mRunningParams = params; mRunningParams = params;
// Instead of real work, we are going to build a string to show to the user.
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("Photos content has changed:\n");
// Did we trigger due to a content change?
if (params.getTriggeredContentAuthorities() != null) { if (params.getTriggeredContentAuthorities() != null) {
sb.append("Authorities: "); boolean rescanNeeded = false;
boolean first = true;
for (String auth : params.getTriggeredContentAuthorities()) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(auth);
}
if (params.getTriggeredContentUris() != null) { if (params.getTriggeredContentUris() != null) {
// If we have details about which URIs changed, then iterate through them
// and collect either the ids that were impacted or note that a generic
// change has happened.
ArrayList<String> ids = new ArrayList<>();
for (Uri uri : params.getTriggeredContentUris()) { for (Uri uri : params.getTriggeredContentUris()) {
sb.append("\n"); List<String> path = uri.getPathSegments();
sb.append(uri); if (path != null && path.size() == EXTERNAL_PATH_SEGMENTS.size()+1) {
// This is a specific file.
ids.add(path.get(path.size()-1));
} else {
// Oops, there is some general change!
rescanNeeded = true;
} }
} }
if (ids.size() > 0) {
// If we found some ids that changed, we want to determine what they are.
// First, we do a query with content provider to ask about all of them.
StringBuilder selection = new StringBuilder();
for (int i=0; i<ids.size(); i++) {
if (selection.length() > 0) {
selection.append(" OR ");
}
selection.append(MediaStore.Images.ImageColumns._ID);
selection.append("='");
selection.append(ids.get(i));
selection.append("'");
}
// Now we iterate through the query, looking at the filenames of
// the items to determine if they are ones we are interested in.
Cursor cursor = null;
boolean haveFiles = false;
try {
cursor = getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
PROJECTION, selection.toString(), null, null);
while (cursor.moveToNext()) {
// We only care about files in the DCIM directory.
String dir = cursor.getString(PROJECTION_DATA);
if (dir.startsWith(DCIM_DIR)) {
if (!haveFiles) {
haveFiles = true;
sb.append("New photos:\n");
}
sb.append(cursor.getInt(PROJECTION_ID));
sb.append(": ");
sb.append(dir);
sb.append("\n");
}
}
} catch (SecurityException e) {
sb.append("Error: no access to media!");
} finally {
if (cursor != null) {
cursor.close();
}
}
}
} else {
// We don't have any details about URIs (because too many changed at once),
// so just note that we need to do a full rescan.
rescanNeeded = true;
}
if (rescanNeeded) {
sb.append("Photos rescan needed!");
}
} else { } else {
sb.append("(No content)"); sb.append("(No photos content)");
} }
Toast.makeText(this, sb.toString(), Toast.LENGTH_LONG).show(); Toast.makeText(this, sb.toString(), Toast.LENGTH_LONG).show();
// We will emulate taking some time to do this work, so we can see batching happen. // We will emulate taking some time to do this work, so we can see batching happen.
mHandler.postDelayed(mWorker, 10*1000); mHandler.postDelayed(mWorker, 10*1000);
return true; return true;