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