/** * Copyright (c) 2016, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.apis.content; import com.example.android.apis.R; //BEGIN_INCLUDE(job) import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; import android.content.ComponentName; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Environment; import android.os.Handler; import android.provider.MediaStore; import android.util.Log; import android.widget.Toast; import java.util.ArrayList; import java.util.List; /** * Example stub job to monitor when there is a change to photos in the media provider. */ 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 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(JobIds.PHOTOS_CONTENT_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 Runnable mWorker = new Runnable() { @Override public void run() { scheduleJob(PhotosContentJob.this); jobFinished(mRunningParams, false); } }; JobParameters mRunningParams; // Schedule this job, replace any existing one. public static void scheduleJob(Context context) { JobScheduler js = context.getSystemService(JobScheduler.class); js.schedule(JOB_INFO); Log.i("PhotosContentJob", "JOB SCHEDULED!"); } // Check whether this job is currently scheduled. public static boolean isScheduled(Context context) { JobScheduler js = context.getSystemService(JobScheduler.class); List jobs = js.getAllPendingJobs(); if (jobs == null) { return false; } for (int i=0; i ids = new ArrayList<>(); for (Uri uri : params.getTriggeredContentUris()) { List path = uri.getPathSegments(); 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 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 { sb.append("(No photos content)"); } 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. mHandler.postDelayed(mWorker, 10*1000); return true; } @Override public boolean onStopJob(JobParameters params) { mHandler.removeCallbacks(mWorker); return false; } } //END_INCLUDE(job)