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.vcard;
     17 
     18 import com.android.contacts.R;
     19 
     20 import android.app.Service;
     21 import android.content.Intent;
     22 import android.content.res.Resources;
     23 import android.media.MediaScannerConnection;
     24 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
     25 import android.net.Uri;
     26 import android.os.Binder;
     27 import android.os.Handler;
     28 import android.os.IBinder;
     29 import android.os.Message;
     30 import android.os.Messenger;
     31 import android.os.RemoteException;
     32 import android.text.TextUtils;
     33 import android.util.Log;
     34 
     35 import java.io.File;
     36 import java.util.ArrayList;
     37 import java.util.HashMap;
     38 import java.util.HashSet;
     39 import java.util.List;
     40 import java.util.Map;
     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     /* package */ static final int MSG_IMPORT_REQUEST = 1;
     61     /* package */ static final int MSG_EXPORT_REQUEST = 2;
     62     /* package */ static final int MSG_CANCEL_REQUEST = 3;
     63     /* package */ static final int MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION = 4;
     64     /* package */ static final int MSG_SET_AVAILABLE_EXPORT_DESTINATION = 5;
     65 
     66     /**
     67      * Specifies the type of operation. Used when constructing a notification, canceling
     68      * some operation, etc.
     69      */
     70     /* package */ static final int TYPE_IMPORT = 1;
     71     /* package */ static final int TYPE_EXPORT = 2;
     72 
     73     /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_";
     74 
     75 
     76     private class CustomMediaScannerConnectionClient implements MediaScannerConnectionClient {
     77         final MediaScannerConnection mConnection;
     78         final String mPath;
     79 
     80         public CustomMediaScannerConnectionClient(String path) {
     81             mConnection = new MediaScannerConnection(VCardService.this, this);
     82             mPath = path;
     83         }
     84 
     85         public void start() {
     86             mConnection.connect();
     87         }
     88 
     89         @Override
     90         public void onMediaScannerConnected() {
     91             if (DEBUG) { Log.d(LOG_TAG, "Connected to MediaScanner. Start scanning."); }
     92             mConnection.scanFile(mPath, null);
     93         }
     94 
     95         @Override
     96         public void onScanCompleted(String path, Uri uri) {
     97             if (DEBUG) { Log.d(LOG_TAG, "scan completed: " + path); }
     98             mConnection.disconnect();
     99             removeConnectionClient(this);
    100         }
    101     }
    102 
    103     // Should be single thread, as we don't want to simultaneously handle import and export
    104     // requests.
    105     private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
    106 
    107     private int mCurrentJobId;
    108 
    109     // Stores all unfinished import/export jobs which will be executed by mExecutorService.
    110     // Key is jobId.
    111     private final Map<Integer, ProcessorBase> mRunningJobMap =
    112             new HashMap<Integer, ProcessorBase>();
    113     // Stores ScannerConnectionClient objects until they finish scanning requested files.
    114     // Uses List class for simplicity. It's not costly as we won't have multiple objects in
    115     // almost all cases.
    116     private final List<CustomMediaScannerConnectionClient> mRemainingScannerConnections =
    117             new ArrayList<CustomMediaScannerConnectionClient>();
    118 
    119     /* ** vCard exporter params ** */
    120     // If true, VCardExporter is able to emits files longer than 8.3 format.
    121     private static final boolean ALLOW_LONG_FILE_NAME = false;
    122 
    123     private String mTargetDirectory;
    124     private String mFileNamePrefix;
    125     private String mFileNameSuffix;
    126     private int mFileIndexMinimum;
    127     private int mFileIndexMaximum;
    128     private String mFileNameExtension;
    129     private Set<String> mExtensionsToConsider;
    130     private String mErrorReason;
    131     private MyBinder mBinder;
    132 
    133     // File names currently reserved by some export job.
    134     private final Set<String> mReservedDestination = new HashSet<String>();
    135     /* ** end of vCard exporter params ** */
    136 
    137     public class MyBinder extends Binder {
    138         public VCardService getService() {
    139             return VCardService.this;
    140         }
    141     }
    142 
    143    @Override
    144     public void onCreate() {
    145         super.onCreate();
    146         mBinder = new MyBinder();
    147         if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created.");
    148         initExporterParams();
    149     }
    150 
    151     private void initExporterParams() {
    152         mTargetDirectory = getString(R.string.config_export_dir);
    153         mFileNamePrefix = getString(R.string.config_export_file_prefix);
    154         mFileNameSuffix = getString(R.string.config_export_file_suffix);
    155         mFileNameExtension = getString(R.string.config_export_file_extension);
    156 
    157         mExtensionsToConsider = new HashSet<String>();
    158         mExtensionsToConsider.add(mFileNameExtension);
    159 
    160         final String additionalExtensions =
    161             getString(R.string.config_export_extensions_to_consider);
    162         if (!TextUtils.isEmpty(additionalExtensions)) {
    163             for (String extension : additionalExtensions.split(",")) {
    164                 String trimed = extension.trim();
    165                 if (trimed.length() > 0) {
    166                     mExtensionsToConsider.add(trimed);
    167                 }
    168             }
    169         }
    170 
    171         final Resources resources = getResources();
    172         mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index);
    173         mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index);
    174     }
    175 
    176     @Override
    177     public int onStartCommand(Intent intent, int flags, int id) {
    178         return START_STICKY;
    179     }
    180 
    181     @Override
    182     public IBinder onBind(Intent intent) {
    183         return mBinder;
    184     }
    185 
    186     @Override
    187     public void onDestroy() {
    188         if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed.");
    189         cancelAllRequestsAndShutdown();
    190         clearCache();
    191         super.onDestroy();
    192     }
    193 
    194     public synchronized void handleImportRequest(List<ImportRequest> requests,
    195             VCardImportExportListener listener) {
    196         if (DEBUG) {
    197             final ArrayList<String> uris = new ArrayList<String>();
    198             final ArrayList<String> displayNames = new ArrayList<String>();
    199             for (ImportRequest request : requests) {
    200                 uris.add(request.uri.toString());
    201                 displayNames.add(request.displayName);
    202             }
    203             Log.d(LOG_TAG,
    204                     String.format("received multiple import request (uri: %s, displayName: %s)",
    205                             uris.toString(), displayNames.toString()));
    206         }
    207         final int size = requests.size();
    208         for (int i = 0; i < size; i++) {
    209             ImportRequest request = requests.get(i);
    210 
    211             if (tryExecute(new ImportProcessor(this, listener, request, mCurrentJobId))) {
    212                 if (listener != null) {
    213                     listener.onImportProcessed(request, mCurrentJobId, i);
    214                 }
    215                 mCurrentJobId++;
    216             } else {
    217                 if (listener != null) {
    218                     listener.onImportFailed(request);
    219                 }
    220                 // A rejection means executor doesn't run any more. Exit.
    221                 break;
    222             }
    223         }
    224     }
    225 
    226     public synchronized void handleExportRequest(ExportRequest request,
    227             VCardImportExportListener listener) {
    228         if (tryExecute(new ExportProcessor(this, request, mCurrentJobId))) {
    229             final String path = request.destUri.getEncodedPath();
    230             if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path);
    231             if (!mReservedDestination.add(path)) {
    232                 Log.w(LOG_TAG,
    233                         String.format("The path %s is already reserved. Reject export request",
    234                                 path));
    235                 if (listener != null) {
    236                     listener.onExportFailed(request);
    237                 }
    238                 return;
    239             }
    240 
    241             if (listener != null) {
    242                 listener.onExportProcessed(request, mCurrentJobId);
    243             }
    244             mCurrentJobId++;
    245         } else {
    246             if (listener != null) {
    247                 listener.onExportFailed(request);
    248             }
    249         }
    250     }
    251 
    252     /**
    253      * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor.
    254      * @return true when successful.
    255      */
    256     private synchronized boolean tryExecute(ProcessorBase processor) {
    257         try {
    258             if (DEBUG) {
    259                 Log.d(LOG_TAG, "Executor service status: shutdown: " + mExecutorService.isShutdown()
    260                         + ", terminated: " + mExecutorService.isTerminated());
    261             }
    262             mExecutorService.execute(processor);
    263             mRunningJobMap.put(mCurrentJobId, processor);
    264             return true;
    265         } catch (RejectedExecutionException e) {
    266             Log.w(LOG_TAG, "Failed to excetute a job.", e);
    267             return false;
    268         }
    269     }
    270 
    271     public synchronized void handleCancelRequest(CancelRequest request,
    272             VCardImportExportListener listener) {
    273         final int jobId = request.jobId;
    274         if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
    275         final ProcessorBase processor = mRunningJobMap.remove(jobId);
    276 
    277         if (processor != null) {
    278             processor.cancel(true);
    279             final int type = processor.getType();
    280             if (listener != null) {
    281                 listener.onCancelRequest(request, type);
    282             }
    283             if (type == TYPE_EXPORT) {
    284                 final String path =
    285                         ((ExportProcessor)processor).getRequest().destUri.getEncodedPath();
    286                 Log.i(LOG_TAG,
    287                         String.format("Cancel reservation for the path %s if appropriate", path));
    288                 if (!mReservedDestination.remove(path)) {
    289                     Log.w(LOG_TAG, "Not reserved.");
    290                 }
    291             }
    292         } else {
    293             Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
    294         }
    295         stopServiceIfAppropriate();
    296     }
    297 
    298     public synchronized void handleRequestAvailableExportDestination(final Messenger messenger) {
    299         if (DEBUG) Log.d(LOG_TAG, "Received available export destination request.");
    300         final String path = getAppropriateDestination(mTargetDirectory);
    301         final Message message;
    302         if (path != null) {
    303             message = Message.obtain(null,
    304                     VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION, 0, 0, path);
    305         } else {
    306             message = Message.obtain(null,
    307                     VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION,
    308                     R.id.dialog_fail_to_export_with_reason, 0, mErrorReason);
    309         }
    310         try {
    311             messenger.send(message);
    312         } catch (RemoteException e) {
    313             Log.w(LOG_TAG, "Failed to send reply for available export destination request.", e);
    314         }
    315     }
    316 
    317     /**
    318      * Checks job list and call {@link #stopSelf()} when there's no job and no scanner connection
    319      * is remaining.
    320      * A new job (import/export) cannot be submitted any more after this call.
    321      */
    322     private synchronized void stopServiceIfAppropriate() {
    323         if (mRunningJobMap.size() > 0) {
    324             for (final Map.Entry<Integer, ProcessorBase> entry : mRunningJobMap.entrySet()) {
    325                 final int jobId = entry.getKey();
    326                 final ProcessorBase processor = entry.getValue();
    327                 if (processor.isDone()) {
    328                     mRunningJobMap.remove(jobId);
    329                 } else {
    330                     Log.i(LOG_TAG, String.format("Found unfinished job (id: %d)", jobId));
    331                     return;
    332                 }
    333             }
    334         }
    335 
    336         if (!mRemainingScannerConnections.isEmpty()) {
    337             Log.i(LOG_TAG, "MediaScanner update is in progress.");
    338             return;
    339         }
    340 
    341         Log.i(LOG_TAG, "No unfinished job. Stop this service.");
    342         mExecutorService.shutdown();
    343         stopSelf();
    344     }
    345 
    346     /* package */ synchronized void updateMediaScanner(String path) {
    347         if (DEBUG) {
    348             Log.d(LOG_TAG, "MediaScanner is being updated: " + path);
    349         }
    350 
    351         if (mExecutorService.isShutdown()) {
    352             Log.w(LOG_TAG, "MediaScanner update is requested after executor's being shut down. " +
    353                     "Ignoring the update request");
    354             return;
    355         }
    356         final CustomMediaScannerConnectionClient client =
    357                 new CustomMediaScannerConnectionClient(path);
    358         mRemainingScannerConnections.add(client);
    359         client.start();
    360     }
    361 
    362     private synchronized void removeConnectionClient(
    363             CustomMediaScannerConnectionClient client) {
    364         if (DEBUG) {
    365             Log.d(LOG_TAG, "Removing custom MediaScannerConnectionClient.");
    366         }
    367         mRemainingScannerConnections.remove(client);
    368         stopServiceIfAppropriate();
    369     }
    370 
    371     /* package */ synchronized void handleFinishImportNotification(
    372             int jobId, boolean successful) {
    373         if (DEBUG) {
    374             Log.d(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
    375                     + "Result: %b", jobId, (successful ? "success" : "failure")));
    376         }
    377         if (mRunningJobMap.remove(jobId) == null) {
    378             Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
    379         }
    380         stopServiceIfAppropriate();
    381     }
    382 
    383     /* package */ synchronized void handleFinishExportNotification(
    384             int jobId, boolean successful) {
    385         if (DEBUG) {
    386             Log.d(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
    387                     + "Result: %b", jobId, (successful ? "success" : "failure")));
    388         }
    389         final ProcessorBase job = mRunningJobMap.remove(jobId);
    390         if (job == null) {
    391             Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
    392         } else if (!(job instanceof ExportProcessor)) {
    393             Log.w(LOG_TAG,
    394                     String.format("Removed job (id: %s) isn't ExportProcessor", jobId));
    395         } else {
    396             final String path = ((ExportProcessor)job).getRequest().destUri.getEncodedPath();
    397             if (DEBUG) Log.d(LOG_TAG, "Remove reserved path " + path);
    398             mReservedDestination.remove(path);
    399         }
    400 
    401         stopServiceIfAppropriate();
    402     }
    403 
    404     /**
    405      * Cancels all the import/export requests and calls {@link ExecutorService#shutdown()}, which
    406      * means this Service becomes no longer ready for import/export requests.
    407      *
    408      * Mainly called from onDestroy().
    409      */
    410     private synchronized void cancelAllRequestsAndShutdown() {
    411         for (final Map.Entry<Integer, ProcessorBase> entry : mRunningJobMap.entrySet()) {
    412             entry.getValue().cancel(true);
    413         }
    414         mRunningJobMap.clear();
    415         mExecutorService.shutdown();
    416     }
    417 
    418     /**
    419      * Removes import caches stored locally.
    420      */
    421     private void clearCache() {
    422         for (final String fileName : fileList()) {
    423             if (fileName.startsWith(CACHE_FILE_PREFIX)) {
    424                 // We don't want to keep all the caches so we remove cache files old enough.
    425                 Log.i(LOG_TAG, "Remove a temporary file: " + fileName);
    426                 deleteFile(fileName);
    427             }
    428         }
    429     }
    430 
    431     /**
    432      * Returns an appropriate file name for vCard export. Returns null when impossible.
    433      *
    434      * @return destination path for a vCard file to be exported. null on error and mErrorReason
    435      * is correctly set.
    436      */
    437     private String getAppropriateDestination(final String destDirectory) {
    438         /*
    439          * Here, file names have 5 parts: directory, prefix, index, suffix, and extension.
    440          * e.g. "/mnt/sdcard/prfx00001sfx.vcf" -> "/mnt/sdcard", "prfx", "00001", "sfx", and ".vcf"
    441          *      (In default, prefix and suffix is empty, so usually the destination would be
    442          *       /mnt/sdcard/00001.vcf.)
    443          *
    444          * This method increments "index" part from 1 to maximum, and checks whether any file name
    445          * following naming rule is available. If there's no file named /mnt/sdcard/00001.vcf, the
    446          * name will be returned to a caller. If there are 00001.vcf 00002.vcf, 00003.vcf is
    447          * returned.
    448          *
    449          * There may not be any appropriate file name. If there are 99999 vCard files in the
    450          * storage, for example, there's no appropriate name, so this method returns
    451          * null.
    452          */
    453 
    454         // Count the number of digits of mFileIndexMaximum
    455         // e.g. When mFileIndexMaximum is 99999, fileIndexDigit becomes 5, as we will count the
    456         int fileIndexDigit = 0;
    457         {
    458             // Calling Math.Log10() is costly.
    459             int tmp;
    460             for (fileIndexDigit = 0, tmp = mFileIndexMaximum; tmp > 0;
    461                 fileIndexDigit++, tmp /= 10) {
    462             }
    463         }
    464 
    465         // %s05d%s (e.g. "p00001s")
    466         final String bodyFormat = "%s%0" + fileIndexDigit + "d%s";
    467 
    468         if (!ALLOW_LONG_FILE_NAME) {
    469             final String possibleBody =
    470                     String.format(bodyFormat, mFileNamePrefix, 1, mFileNameSuffix);
    471             if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
    472                 Log.e(LOG_TAG, "This code does not allow any long file name.");
    473                 mErrorReason = getString(R.string.fail_reason_too_long_filename,
    474                         String.format("%s.%s", possibleBody, mFileNameExtension));
    475                 Log.w(LOG_TAG, "File name becomes too long.");
    476                 return null;
    477             }
    478         }
    479 
    480         for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
    481             boolean numberIsAvailable = true;
    482             String body = null;
    483             for (String possibleExtension : mExtensionsToConsider) {
    484                 body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
    485                 final String path =
    486                         String.format("%s/%s.%s", destDirectory, body, possibleExtension);
    487                 synchronized (this) {
    488                     if (mReservedDestination.contains(path)) {
    489                         if (DEBUG) {
    490                             Log.d(LOG_TAG, String.format("The path %s is reserved.", path));
    491                         }
    492                         numberIsAvailable = false;
    493                         break;
    494                     }
    495                 }
    496                 final File file = new File(path);
    497                 if (file.exists()) {
    498                     numberIsAvailable = false;
    499                     break;
    500                 }
    501             }
    502             if (numberIsAvailable) {
    503                 return String.format("%s/%s.%s", destDirectory, body, mFileNameExtension);
    504             }
    505         }
    506 
    507         Log.w(LOG_TAG, "Reached vCard number limit. Maybe there are too many vCard in the storage");
    508         mErrorReason = getString(R.string.fail_reason_too_many_vcard);
    509         return null;
    510     }
    511 }
    512