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