1 /** 2 * Copyright (c) 2016, The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.apis.content; 18 19 import com.example.android.apis.R; 20 21 //BEGIN_INCLUDE(job) 22 import android.app.job.JobInfo; 23 import android.app.job.JobParameters; 24 import android.app.job.JobScheduler; 25 import android.app.job.JobService; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.database.Cursor; 29 import android.net.Uri; 30 import android.os.Environment; 31 import android.os.Handler; 32 import android.provider.MediaStore; 33 import android.util.Log; 34 import android.widget.Toast; 35 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** 40 * Example stub job to monitor when there is a change to photos in the media provider. 41 */ 42 public class PhotosContentJob extends JobService { 43 // The root URI of the media provider, to monitor for generic changes to its content. 44 static final Uri MEDIA_URI = Uri.parse("content://" + MediaStore.AUTHORITY + "/"); 45 46 // Path segments for image-specific URIs in the provider. 47 static final List<String> EXTERNAL_PATH_SEGMENTS 48 = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.getPathSegments(); 49 50 // The columns we want to retrieve about a particular image. 51 static final String[] PROJECTION = new String[] { 52 MediaStore.Images.ImageColumns._ID, MediaStore.Images.ImageColumns.DATA 53 }; 54 static final int PROJECTION_ID = 0; 55 static final int PROJECTION_DATA = 1; 56 57 // This is the external storage directory where cameras place pictures. 58 static final String DCIM_DIR = Environment.getExternalStoragePublicDirectory( 59 Environment.DIRECTORY_DCIM).getPath(); 60 61 // A pre-built JobInfo we use for scheduling our job. 62 static final JobInfo JOB_INFO; 63 64 static { 65 JobInfo.Builder builder = new JobInfo.Builder(JobIds.PHOTOS_CONTENT_JOB, 66 new ComponentName("com.example.android.apis", PhotosContentJob.class.getName())); 67 // 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))68 builder.addTriggerContentUri(new JobInfo.TriggerContentUri( 69 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 70 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)); 71 // Also look for general reports of changes in the overall provider. builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MEDIA_URI, 0))72 builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MEDIA_URI, 0)); 73 JOB_INFO = builder.build(); 74 } 75 76 // Fake job work. A real implementation would do some work on a separate thread. 77 final Handler mHandler = new Handler(); 78 final Runnable mWorker = new Runnable() { 79 @Override public void run() { 80 scheduleJob(PhotosContentJob.this); 81 jobFinished(mRunningParams, false); 82 } 83 }; 84 85 JobParameters mRunningParams; 86 87 // Schedule this job, replace any existing one. scheduleJob(Context context)88 public static void scheduleJob(Context context) { 89 JobScheduler js = context.getSystemService(JobScheduler.class); 90 js.schedule(JOB_INFO); 91 Log.i("PhotosContentJob", "JOB SCHEDULED!"); 92 } 93 94 // Check whether this job is currently scheduled. isScheduled(Context context)95 public static boolean isScheduled(Context context) { 96 JobScheduler js = context.getSystemService(JobScheduler.class); 97 List<JobInfo> jobs = js.getAllPendingJobs(); 98 if (jobs == null) { 99 return false; 100 } 101 for (int i=0; i<jobs.size(); i++) { 102 if (jobs.get(i).getId() == JobIds.PHOTOS_CONTENT_JOB) { 103 return true; 104 } 105 } 106 return false; 107 } 108 109 // Cancel this job, if currently scheduled. cancelJob(Context context)110 public static void cancelJob(Context context) { 111 JobScheduler js = context.getSystemService(JobScheduler.class); 112 js.cancel(JobIds.PHOTOS_CONTENT_JOB); 113 } 114 115 @Override onStartJob(JobParameters params)116 public boolean onStartJob(JobParameters params) { 117 Log.i("PhotosContentJob", "JOB STARTED!"); 118 mRunningParams = params; 119 120 // Instead of real work, we are going to build a string to show to the user. 121 StringBuilder sb = new StringBuilder(); 122 123 // Did we trigger due to a content change? 124 if (params.getTriggeredContentAuthorities() != null) { 125 boolean rescanNeeded = false; 126 127 if (params.getTriggeredContentUris() != null) { 128 // If we have details about which URIs changed, then iterate through them 129 // and collect either the ids that were impacted or note that a generic 130 // change has happened. 131 ArrayList<String> ids = new ArrayList<>(); 132 for (Uri uri : params.getTriggeredContentUris()) { 133 List<String> path = uri.getPathSegments(); 134 if (path != null && path.size() == EXTERNAL_PATH_SEGMENTS.size()+1) { 135 // This is a specific file. 136 ids.add(path.get(path.size()-1)); 137 } else { 138 // Oops, there is some general change! 139 rescanNeeded = true; 140 } 141 } 142 143 if (ids.size() > 0) { 144 // If we found some ids that changed, we want to determine what they are. 145 // First, we do a query with content provider to ask about all of them. 146 StringBuilder selection = new StringBuilder(); 147 for (int i=0; i<ids.size(); i++) { 148 if (selection.length() > 0) { 149 selection.append(" OR "); 150 } 151 selection.append(MediaStore.Images.ImageColumns._ID); 152 selection.append("='"); 153 selection.append(ids.get(i)); 154 selection.append("'"); 155 } 156 157 // Now we iterate through the query, looking at the filenames of 158 // the items to determine if they are ones we are interested in. 159 Cursor cursor = null; 160 boolean haveFiles = false; 161 try { 162 cursor = getContentResolver().query( 163 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 164 PROJECTION, selection.toString(), null, null); 165 while (cursor.moveToNext()) { 166 // We only care about files in the DCIM directory. 167 String dir = cursor.getString(PROJECTION_DATA); 168 if (dir.startsWith(DCIM_DIR)) { 169 if (!haveFiles) { 170 haveFiles = true; 171 sb.append("New photos:\n"); 172 } 173 sb.append(cursor.getInt(PROJECTION_ID)); 174 sb.append(": "); 175 sb.append(dir); 176 sb.append("\n"); 177 } 178 } 179 } catch (SecurityException e) { 180 sb.append("Error: no access to media!"); 181 } finally { 182 if (cursor != null) { 183 cursor.close(); 184 } 185 } 186 } 187 188 } else { 189 // We don't have any details about URIs (because too many changed at once), 190 // so just note that we need to do a full rescan. 191 rescanNeeded = true; 192 } 193 194 if (rescanNeeded) { 195 sb.append("Photos rescan needed!"); 196 } 197 } else { 198 sb.append("(No photos content)"); 199 } 200 Toast.makeText(this, sb.toString(), Toast.LENGTH_LONG).show(); 201 202 // We will emulate taking some time to do this work, so we can see batching happen. 203 mHandler.postDelayed(mWorker, 10*1000); 204 return true; 205 } 206 207 @Override onStopJob(JobParameters params)208 public boolean onStopJob(JobParameters params) { 209 mHandler.removeCallbacks(mWorker); 210 return false; 211 } 212 } 213 //END_INCLUDE(job) 214