Home | History | Annotate | Download | only in vcard
      1 /*
      2  * Copyright (C) 2010 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 package com.android.contacts.common.vcard;
     17 
     18 import android.app.Service;
     19 import android.content.Intent;
     20 import android.content.res.Resources;
     21 import android.media.MediaScannerConnection;
     22 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
     23 import android.net.Uri;
     24 import android.os.Binder;
     25 import android.os.Environment;
     26 import android.os.IBinder;
     27 import android.os.Message;
     28 import android.os.Messenger;
     29 import android.os.RemoteException;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 import android.util.SparseArray;
     33 
     34 import com.android.contacts.common.R;
     35 
     36 import java.io.File;
     37 import java.util.ArrayList;
     38 import java.util.HashSet;
     39 import java.util.List;
     40 import java.util.Locale;
     41 import java.util.Set;
     42 import java.util.concurrent.ExecutorService;
     43 import java.util.concurrent.Executors;
     44 import java.util.concurrent.RejectedExecutionException;
     45 
     46 /**
     47  * The class responsible for handling vCard import/export requests.
     48  *
     49  * This Service creates one ImportRequest/ExportRequest object (as Runnable) per request and push
     50  * it to {@link ExecutorService} with single thread executor. The executor handles each request
     51  * one by one, and notifies users when needed.
     52  */
     53 // TODO: Using IntentService looks simpler than using Service + ServiceConnection though this
     54 // works fine enough. Investigate the feasibility.
     55 public class VCardService extends Service {
     56     private final static String LOG_TAG = "VCardService";
     57 
     58     /* package */ final static boolean DEBUG = false;
     59 
     60     /**
     61      * Specifies the type of operation. Used when constructing a notification, canceling
     62      * some operation, etc.
     63      */
     64     /* package */ static final int TYPE_IMPORT = 1;
     65     /* package */ static final int TYPE_EXPORT = 2;
     66 
     67     /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_";
     68 
     69     /* package */ static final String X_VCARD_MIME_TYPE = "text/x-vcard";
     70 
     71     private class CustomMediaScannerConnectionClient implements MediaScannerConnectionClient {
     72         final MediaScannerConnection mConnection;
     73         final String mPath;
     74 
     75         public CustomMediaScannerConnectionClient(String path) {
     76             mConnection = new MediaScannerConnection(VCardService.this, this);
     77             mPath = path;
     78         }
     79 
     80         public void start() {
     81             mConnection.connect();
     82         }
     83 
     84         @Override
     85         public void onMediaScannerConnected() {
     86             if (DEBUG) { Log.d(LOG_TAG, "Connected to MediaScanner. Start scanning."); }
     87             mConnection.scanFile(mPath, null);
     88         }
     89 
     90         @Override
     91         public void onScanCompleted(String path, Uri uri) {
     92             if (DEBUG) { Log.d(LOG_TAG, "scan completed: " + path); }
     93             mConnection.disconnect();
     94             removeConnectionClient(this);
     95         }
     96     }
     97 
     98     // Should be single thread, as we don't want to simultaneously handle import and export
     99     // requests.
    100     private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
    101 
    102     private int mCurrentJobId;
    103 
    104     // Stores all unfinished import/export jobs which will be executed by mExecutorService.
    105     // Key is jobId.
    106     private final SparseArray<ProcessorBase> mRunningJobMap = new SparseArray<ProcessorBase>();
    107     // Stores ScannerConnectionClient objects until they finish scanning requested files.
    108     // Uses List class for simplicity. It's not costly as we won't have multiple objects in
    109     // almost all cases.
    110     private final List<CustomMediaScannerConnectionClient> mRemainingScannerConnections =
    111             new ArrayList<CustomMediaScannerConnectionClient>();
    112 
    113     private MyBinder mBinder;
    114 
    115     private String mCallingActivity;
    116 
    117     // File names currently reserved by some export job.
    118     private final Set<String> mReservedDestination = new HashSet<String>();
    119     /* ** end of vCard exporter params ** */
    120 
    121     public class MyBinder extends Binder {
    122         public VCardService getService() {
    123             return VCardService.this;
    124         }
    125     }
    126 
    127    @Override
    128     public void onCreate() {
    129         super.onCreate();
    130         mBinder = new MyBinder();
    131         if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created.");
    132     }
    133 
    134     @Override
    135     public int onStartCommand(Intent intent, int flags, int id) {
    136         if (intent != null && intent.getExtras() != null) {
    137             mCallingActivity = intent.getExtras().getString(
    138                     VCardCommonArguments.ARG_CALLING_ACTIVITY);
    139         } else {
    140             mCallingActivity = null;
    141         }
    142         return START_STICKY;
    143     }
    144 
    145     @Override
    146     public IBinder onBind(Intent intent) {
    147         return mBinder;
    148     }
    149 
    150     @Override
    151     public void onDestroy() {
    152         if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed.");
    153         cancelAllRequestsAndShutdown();
    154         clearCache();
    155         super.onDestroy();
    156     }
    157 
    158     public synchronized void handleImportRequest(List<ImportRequest> requests,
    159             VCardImportExportListener listener) {
    160         if (DEBUG) {
    161             final ArrayList<String> uris = new ArrayList<String>();
    162             final ArrayList<String> displayNames = new ArrayList<String>();
    163             for (ImportRequest request : requests) {
    164                 uris.add(request.uri.toString());
    165                 displayNames.add(request.displayName);
    166             }
    167             Log.d(LOG_TAG,
    168                     String.format("received multiple import request (uri: %s, displayName: %s)",
    169                             uris.toString(), displayNames.toString()));
    170         }
    171         final int size = requests.size();
    172         for (int i = 0; i < size; i++) {
    173             ImportRequest request = requests.get(i);
    174 
    175             if (tryExecute(new ImportProcessor(this, listener, request, mCurrentJobId))) {
    176                 if (listener != null) {
    177                     listener.onImportProcessed(request, mCurrentJobId, i);
    178                 }
    179                 mCurrentJobId++;
    180             } else {
    181                 if (listener != null) {
    182                     listener.onImportFailed(request);
    183                 }
    184                 // A rejection means executor doesn't run any more. Exit.
    185                 break;
    186             }
    187         }
    188     }
    189 
    190     public synchronized void handleExportRequest(ExportRequest request,
    191             VCardImportExportListener listener) {
    192         if (tryExecute(new ExportProcessor(this, request, mCurrentJobId, mCallingActivity))) {
    193             final String path = request.destUri.getEncodedPath();
    194             if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path);
    195             if (!mReservedDestination.add(path)) {
    196                 Log.w(LOG_TAG,
    197                         String.format("The path %s is already reserved. Reject export request",
    198                                 path));
    199                 if (listener != null) {
    200                     listener.onExportFailed(request);
    201                 }
    202                 return;
    203             }
    204 
    205             if (listener != null) {
    206                 listener.onExportProcessed(request, mCurrentJobId);
    207             }
    208             mCurrentJobId++;
    209         } else {
    210             if (listener != null) {
    211                 listener.onExportFailed(request);
    212             }
    213         }
    214     }
    215 
    216     /**
    217      * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor.
    218      * @return true when successful.
    219      */
    220     private synchronized boolean tryExecute(ProcessorBase processor) {
    221         try {
    222             if (DEBUG) {
    223                 Log.d(LOG_TAG, "Executor service status: shutdown: " + mExecutorService.isShutdown()
    224                         + ", terminated: " + mExecutorService.isTerminated());
    225             }
    226             mExecutorService.execute(processor);
    227             mRunningJobMap.put(mCurrentJobId, processor);
    228             return true;
    229         } catch (RejectedExecutionException e) {
    230             Log.w(LOG_TAG, "Failed to excetute a job.", e);
    231             return false;
    232         }
    233     }
    234 
    235     public synchronized void handleCancelRequest(CancelRequest request,
    236             VCardImportExportListener listener) {
    237         final int jobId = request.jobId;
    238         if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
    239 
    240         final ProcessorBase processor = mRunningJobMap.get(jobId);
    241         mRunningJobMap.remove(jobId);
    242 
    243         if (processor != null) {
    244             processor.cancel(true);
    245             final int type = processor.getType();
    246             if (listener != null) {
    247                 listener.onCancelRequest(request, type);
    248             }
    249             if (type == TYPE_EXPORT) {
    250                 final String path =
    251                         ((ExportProcessor)processor).getRequest().destUri.getEncodedPath();
    252                 Log.i(LOG_TAG,
    253                         String.format("Cancel reservation for the path %s if appropriate", path));
    254                 if (!mReservedDestination.remove(path)) {
    255                     Log.w(LOG_TAG, "Not reserved.");
    256                 }
    257             }
    258         } else {
    259             Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
    260         }
    261         stopServiceIfAppropriate();
    262     }
    263 
    264     /**
    265      * Checks job list and call {@link #stopSelf()} when there's no job and no scanner connection
    266      * is remaining.
    267      * A new job (import/export) cannot be submitted any more after this call.
    268      */
    269     private synchronized void stopServiceIfAppropriate() {
    270         if (mRunningJobMap.size() > 0) {
    271             final int size = mRunningJobMap.size();
    272 
    273             // Check if there are processors which aren't finished yet. If we still have ones to
    274             // process, we cannot stop the service yet. Also clean up already finished processors
    275             // here.
    276 
    277             // Job-ids to be removed. At first all elements in the array are invalid and will
    278             // be filled with real job-ids from the array's top. When we find a not-yet-finished
    279             // processor, then we start removing those finished jobs. In that case latter half of
    280             // this array will be invalid.
    281             final int[] toBeRemoved = new int[size];
    282             for (int i = 0; i < size; i++) {
    283                 final int jobId = mRunningJobMap.keyAt(i);
    284                 final ProcessorBase processor = mRunningJobMap.valueAt(i);
    285                 if (!processor.isDone()) {
    286                     Log.i(LOG_TAG, String.format("Found unfinished job (id: %d)", jobId));
    287 
    288                     // Remove processors which are already "done", all of which should be before
    289                     // processors which aren't done yet.
    290                     for (int j = 0; j < i; j++) {
    291                         mRunningJobMap.remove(toBeRemoved[j]);
    292                     }
    293                     return;
    294                 }
    295 
    296                 // Remember the finished processor.
    297                 toBeRemoved[i] = jobId;
    298             }
    299 
    300             // We're sure we can remove all. Instead of removing one by one, just call clear().
    301             mRunningJobMap.clear();
    302         }
    303 
    304         if (!mRemainingScannerConnections.isEmpty()) {
    305             Log.i(LOG_TAG, "MediaScanner update is in progress.");
    306             return;
    307         }
    308 
    309         Log.i(LOG_TAG, "No unfinished job. Stop this service.");
    310         mExecutorService.shutdown();
    311         stopSelf();
    312     }
    313 
    314     /* package */ synchronized void updateMediaScanner(String path) {
    315         if (DEBUG) {
    316             Log.d(LOG_TAG, "MediaScanner is being updated: " + path);
    317         }
    318 
    319         if (mExecutorService.isShutdown()) {
    320             Log.w(LOG_TAG, "MediaScanner update is requested after executor's being shut down. " +
    321                     "Ignoring the update request");
    322             return;
    323         }
    324         final CustomMediaScannerConnectionClient client =
    325                 new CustomMediaScannerConnectionClient(path);
    326         mRemainingScannerConnections.add(client);
    327         client.start();
    328     }
    329 
    330     private synchronized void removeConnectionClient(
    331             CustomMediaScannerConnectionClient client) {
    332         if (DEBUG) {
    333             Log.d(LOG_TAG, "Removing custom MediaScannerConnectionClient.");
    334         }
    335         mRemainingScannerConnections.remove(client);
    336         stopServiceIfAppropriate();
    337     }
    338 
    339     /* package */ synchronized void handleFinishImportNotification(
    340             int jobId, boolean successful) {
    341         if (DEBUG) {
    342             Log.d(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
    343                     + "Result: %b", jobId, (successful ? "success" : "failure")));
    344         }
    345         mRunningJobMap.remove(jobId);
    346         stopServiceIfAppropriate();
    347     }
    348 
    349     /* package */ synchronized void handleFinishExportNotification(
    350             int jobId, boolean successful) {
    351         if (DEBUG) {
    352             Log.d(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
    353                     + "Result: %b", jobId, (successful ? "success" : "failure")));
    354         }
    355         final ProcessorBase job = mRunningJobMap.get(jobId);
    356         mRunningJobMap.remove(jobId);
    357         if (job == null) {
    358             Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
    359         } else if (!(job instanceof ExportProcessor)) {
    360             Log.w(LOG_TAG,
    361                     String.format("Removed job (id: %s) isn't ExportProcessor", jobId));
    362         } else {
    363             final String path = ((ExportProcessor)job).getRequest().destUri.getEncodedPath();
    364             if (DEBUG) Log.d(LOG_TAG, "Remove reserved path " + path);
    365             mReservedDestination.remove(path);
    366         }
    367 
    368         stopServiceIfAppropriate();
    369     }
    370 
    371     /**
    372      * Cancels all the import/export requests and calls {@link ExecutorService#shutdown()}, which
    373      * means this Service becomes no longer ready for import/export requests.
    374      *
    375      * Mainly called from onDestroy().
    376      */
    377     private synchronized void cancelAllRequestsAndShutdown() {
    378         for (int i = 0; i < mRunningJobMap.size(); i++) {
    379             mRunningJobMap.valueAt(i).cancel(true);
    380         }
    381         mRunningJobMap.clear();
    382         mExecutorService.shutdown();
    383     }
    384 
    385     /**
    386      * Removes import caches stored locally.
    387      */
    388     private void clearCache() {
    389         for (final String fileName : fileList()) {
    390             if (fileName.startsWith(CACHE_FILE_PREFIX)) {
    391                 // We don't want to keep all the caches so we remove cache files old enough.
    392                 Log.i(LOG_TAG, "Remove a temporary file: " + fileName);
    393                 deleteFile(fileName);
    394             }
    395         }
    396     }
    397 }
    398