Home | History | Annotate | Download | only in content
      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.
     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.
     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.
     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.
     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.
    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
    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
    208     public boolean onStopJob(JobParameters params) {
    209         mHandler.removeCallbacks(mWorker);
    210         return false;
    211     }
    212 }
    213 //END_INCLUDE(job)
    214