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.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 File 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     private String mCallingActivity;
    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 = Environment.getExternalStorageDirectory();
    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         if (intent != null && intent.getExtras() != null) {
    179             mCallingActivity = intent.getExtras().getString(
    180                     VCardCommonArguments.ARG_CALLING_ACTIVITY);
    181         } else {
    182             mCallingActivity = null;
    183         }
    184         return START_STICKY;
    185     }
    186 
    187     @Override
    188     public IBinder onBind(Intent intent) {
    189         return mBinder;
    190     }
    191 
    192     @Override
    193     public void onDestroy() {
    194         if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed.");
    195         cancelAllRequestsAndShutdown();
    196         clearCache();
    197         super.onDestroy();
    198     }
    199 
    200     public synchronized void handleImportRequest(List<ImportRequest> requests,
    201             VCardImportExportListener listener) {
    202         if (DEBUG) {
    203             final ArrayList<String> uris = new ArrayList<String>();
    204             final ArrayList<String> displayNames = new ArrayList<String>();
    205             for (ImportRequest request : requests) {
    206                 uris.add(request.uri.toString());
    207                 displayNames.add(request.displayName);
    208             }
    209             Log.d(LOG_TAG,
    210                     String.format("received multiple import request (uri: %s, displayName: %s)",
    211                             uris.toString(), displayNames.toString()));
    212         }
    213         final int size = requests.size();
    214         for (int i = 0; i < size; i++) {
    215             ImportRequest request = requests.get(i);
    216 
    217             if (tryExecute(new ImportProcessor(this, listener, request, mCurrentJobId))) {
    218                 if (listener != null) {
    219                     listener.onImportProcessed(request, mCurrentJobId, i);
    220                 }
    221                 mCurrentJobId++;
    222             } else {
    223                 if (listener != null) {
    224                     listener.onImportFailed(request);
    225                 }
    226                 // A rejection means executor doesn't run any more. Exit.
    227                 break;
    228             }
    229         }
    230     }
    231 
    232     public synchronized void handleExportRequest(ExportRequest request,
    233             VCardImportExportListener listener) {
    234         if (tryExecute(new ExportProcessor(this, request, mCurrentJobId, mCallingActivity))) {
    235             final String path = request.destUri.getEncodedPath();
    236             if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path);
    237             if (!mReservedDestination.add(path)) {
    238                 Log.w(LOG_TAG,
    239                         String.format("The path %s is already reserved. Reject export request",
    240                                 path));
    241                 if (listener != null) {
    242                     listener.onExportFailed(request);
    243                 }
    244                 return;
    245             }
    246 
    247             if (listener != null) {
    248                 listener.onExportProcessed(request, mCurrentJobId);
    249             }
    250             mCurrentJobId++;
    251         } else {
    252             if (listener != null) {
    253                 listener.onExportFailed(request);
    254             }
    255         }
    256     }
    257 
    258     /**
    259      * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor.
    260      * @return true when successful.
    261      */
    262     private synchronized boolean tryExecute(ProcessorBase processor) {
    263         try {
    264             if (DEBUG) {
    265                 Log.d(LOG_TAG, "Executor service status: shutdown: " + mExecutorService.isShutdown()
    266                         + ", terminated: " + mExecutorService.isTerminated());
    267             }
    268             mExecutorService.execute(processor);
    269             mRunningJobMap.put(mCurrentJobId, processor);
    270             return true;
    271         } catch (RejectedExecutionException e) {
    272             Log.w(LOG_TAG, "Failed to excetute a job.", e);
    273             return false;
    274         }
    275     }
    276 
    277     public synchronized void handleCancelRequest(CancelRequest request,
    278             VCardImportExportListener listener) {
    279         final int jobId = request.jobId;
    280         if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
    281 
    282         final ProcessorBase processor = mRunningJobMap.get(jobId);
    283         mRunningJobMap.remove(jobId);
    284 
    285         if (processor != null) {
    286             processor.cancel(true);
    287             final int type = processor.getType();
    288             if (listener != null) {
    289                 listener.onCancelRequest(request, type);
    290             }
    291             if (type == TYPE_EXPORT) {
    292                 final String path =
    293                         ((ExportProcessor)processor).getRequest().destUri.getEncodedPath();
    294                 Log.i(LOG_TAG,
    295                         String.format("Cancel reservation for the path %s if appropriate", path));
    296                 if (!mReservedDestination.remove(path)) {
    297                     Log.w(LOG_TAG, "Not reserved.");
    298                 }
    299             }
    300         } else {
    301             Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
    302         }
    303         stopServiceIfAppropriate();
    304     }
    305 
    306     public synchronized void handleRequestAvailableExportDestination(final Messenger messenger) {
    307         if (DEBUG) Log.d(LOG_TAG, "Received available export destination request.");
    308         final String path = getAppropriateDestination(mTargetDirectory);
    309         final Message message;
    310         if (path != null) {
    311             message = Message.obtain(null,
    312                     VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION, 0, 0, path);
    313         } else {
    314             message = Message.obtain(null,
    315                     VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION,
    316                     R.id.dialog_fail_to_export_with_reason, 0, mErrorReason);
    317         }
    318         try {
    319             messenger.send(message);
    320         } catch (RemoteException e) {
    321             Log.w(LOG_TAG, "Failed to send reply for available export destination request.", e);
    322         }
    323     }
    324 
    325     /**
    326      * Checks job list and call {@link #stopSelf()} when there's no job and no scanner connection
    327      * is remaining.
    328      * A new job (import/export) cannot be submitted any more after this call.
    329      */
    330     private synchronized void stopServiceIfAppropriate() {
    331         if (mRunningJobMap.size() > 0) {
    332             final int size = mRunningJobMap.size();
    333 
    334             // Check if there are processors which aren't finished yet. If we still have ones to
    335             // process, we cannot stop the service yet. Also clean up already finished processors
    336             // here.
    337 
    338             // Job-ids to be removed. At first all elements in the array are invalid and will
    339             // be filled with real job-ids from the array's top. When we find a not-yet-finished
    340             // processor, then we start removing those finished jobs. In that case latter half of
    341             // this array will be invalid.
    342             final int[] toBeRemoved = new int[size];
    343             for (int i = 0; i < size; i++) {
    344                 final int jobId = mRunningJobMap.keyAt(i);
    345                 final ProcessorBase processor = mRunningJobMap.valueAt(i);
    346                 if (!processor.isDone()) {
    347                     Log.i(LOG_TAG, String.format("Found unfinished job (id: %d)", jobId));
    348 
    349                     // Remove processors which are already "done", all of which should be before
    350                     // processors which aren't done yet.
    351                     for (int j = 0; j < i; j++) {
    352                         mRunningJobMap.remove(toBeRemoved[j]);
    353                     }
    354                     return;
    355                 }
    356 
    357                 // Remember the finished processor.
    358                 toBeRemoved[i] = jobId;
    359             }
    360 
    361             // We're sure we can remove all. Instead of removing one by one, just call clear().
    362             mRunningJobMap.clear();
    363         }
    364 
    365         if (!mRemainingScannerConnections.isEmpty()) {
    366             Log.i(LOG_TAG, "MediaScanner update is in progress.");
    367             return;
    368         }
    369 
    370         Log.i(LOG_TAG, "No unfinished job. Stop this service.");
    371         mExecutorService.shutdown();
    372         stopSelf();
    373     }
    374 
    375     /* package */ synchronized void updateMediaScanner(String path) {
    376         if (DEBUG) {
    377             Log.d(LOG_TAG, "MediaScanner is being updated: " + path);
    378         }
    379 
    380         if (mExecutorService.isShutdown()) {
    381             Log.w(LOG_TAG, "MediaScanner update is requested after executor's being shut down. " +
    382                     "Ignoring the update request");
    383             return;
    384         }
    385         final CustomMediaScannerConnectionClient client =
    386                 new CustomMediaScannerConnectionClient(path);
    387         mRemainingScannerConnections.add(client);
    388         client.start();
    389     }
    390 
    391     private synchronized void removeConnectionClient(
    392             CustomMediaScannerConnectionClient client) {
    393         if (DEBUG) {
    394             Log.d(LOG_TAG, "Removing custom MediaScannerConnectionClient.");
    395         }
    396         mRemainingScannerConnections.remove(client);
    397         stopServiceIfAppropriate();
    398     }
    399 
    400     /* package */ synchronized void handleFinishImportNotification(
    401             int jobId, boolean successful) {
    402         if (DEBUG) {
    403             Log.d(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
    404                     + "Result: %b", jobId, (successful ? "success" : "failure")));
    405         }
    406         mRunningJobMap.remove(jobId);
    407         stopServiceIfAppropriate();
    408     }
    409 
    410     /* package */ synchronized void handleFinishExportNotification(
    411             int jobId, boolean successful) {
    412         if (DEBUG) {
    413             Log.d(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
    414                     + "Result: %b", jobId, (successful ? "success" : "failure")));
    415         }
    416         final ProcessorBase job = mRunningJobMap.get(jobId);
    417         mRunningJobMap.remove(jobId);
    418         if (job == null) {
    419             Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
    420         } else if (!(job instanceof ExportProcessor)) {
    421             Log.w(LOG_TAG,
    422                     String.format("Removed job (id: %s) isn't ExportProcessor", jobId));
    423         } else {
    424             final String path = ((ExportProcessor)job).getRequest().destUri.getEncodedPath();
    425             if (DEBUG) Log.d(LOG_TAG, "Remove reserved path " + path);
    426             mReservedDestination.remove(path);
    427         }
    428 
    429         stopServiceIfAppropriate();
    430     }
    431 
    432     /**
    433      * Cancels all the import/export requests and calls {@link ExecutorService#shutdown()}, which
    434      * means this Service becomes no longer ready for import/export requests.
    435      *
    436      * Mainly called from onDestroy().
    437      */
    438     private synchronized void cancelAllRequestsAndShutdown() {
    439         for (int i = 0; i < mRunningJobMap.size(); i++) {
    440             mRunningJobMap.valueAt(i).cancel(true);
    441         }
    442         mRunningJobMap.clear();
    443         mExecutorService.shutdown();
    444     }
    445 
    446     /**
    447      * Removes import caches stored locally.
    448      */
    449     private void clearCache() {
    450         for (final String fileName : fileList()) {
    451             if (fileName.startsWith(CACHE_FILE_PREFIX)) {
    452                 // We don't want to keep all the caches so we remove cache files old enough.
    453                 Log.i(LOG_TAG, "Remove a temporary file: " + fileName);
    454                 deleteFile(fileName);
    455             }
    456         }
    457     }
    458 
    459     /**
    460      * Returns an appropriate file name for vCard export. Returns null when impossible.
    461      *
    462      * @return destination path for a vCard file to be exported. null on error and mErrorReason
    463      * is correctly set.
    464      */
    465     private String getAppropriateDestination(final File destDirectory) {
    466         /*
    467          * Here, file names have 5 parts: directory, prefix, index, suffix, and extension.
    468          * e.g. "/mnt/sdcard/prfx00001sfx.vcf" -> "/mnt/sdcard", "prfx", "00001", "sfx", and ".vcf"
    469          *      (In default, prefix and suffix is empty, so usually the destination would be
    470          *       /mnt/sdcard/00001.vcf.)
    471          *
    472          * This method increments "index" part from 1 to maximum, and checks whether any file name
    473          * following naming rule is available. If there's no file named /mnt/sdcard/00001.vcf, the
    474          * name will be returned to a caller. If there are 00001.vcf 00002.vcf, 00003.vcf is
    475          * returned.
    476          *
    477          * There may not be any appropriate file name. If there are 99999 vCard files in the
    478          * storage, for example, there's no appropriate name, so this method returns
    479          * null.
    480          */
    481 
    482         // Count the number of digits of mFileIndexMaximum
    483         // e.g. When mFileIndexMaximum is 99999, fileIndexDigit becomes 5, as we will count the
    484         int fileIndexDigit = 0;
    485         {
    486             // Calling Math.Log10() is costly.
    487             int tmp;
    488             for (fileIndexDigit = 0, tmp = mFileIndexMaximum; tmp > 0;
    489                 fileIndexDigit++, tmp /= 10) {
    490             }
    491         }
    492 
    493         // %s05d%s (e.g. "p00001s")
    494         final String bodyFormat = "%s%0" + fileIndexDigit + "d%s";
    495 
    496         if (!ALLOW_LONG_FILE_NAME) {
    497             final String possibleBody =
    498                     String.format(bodyFormat, mFileNamePrefix, 1, mFileNameSuffix);
    499             if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
    500                 Log.e(LOG_TAG, "This code does not allow any long file name.");
    501                 mErrorReason = getString(R.string.fail_reason_too_long_filename,
    502                         String.format("%s.%s", possibleBody, mFileNameExtension));
    503                 Log.w(LOG_TAG, "File name becomes too long.");
    504                 return null;
    505             }
    506         }
    507 
    508         for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
    509             boolean numberIsAvailable = true;
    510             final String body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
    511             // Make sure that none of the extensions of mExtensionsToConsider matches. If this
    512             // number is free, we'll go ahead with mFileNameExtension (which is included in
    513             // mExtensionsToConsider)
    514             for (String possibleExtension : mExtensionsToConsider) {
    515                 final File file = new File(destDirectory, body + "." + possibleExtension);
    516                 final String path = file.getAbsolutePath();
    517                 synchronized (this) {
    518                     // Is this being exported right now? Skip this number
    519                     if (mReservedDestination.contains(path)) {
    520                         if (DEBUG) {
    521                             Log.d(LOG_TAG, String.format("%s is already being exported.", path));
    522                         }
    523                         numberIsAvailable = false;
    524                         break;
    525                     }
    526                 }
    527                 if (file.exists()) {
    528                     numberIsAvailable = false;
    529                     break;
    530                 }
    531             }
    532             if (numberIsAvailable) {
    533                 return new File(destDirectory, body + "." + mFileNameExtension).getAbsolutePath();
    534             }
    535         }
    536 
    537         Log.w(LOG_TAG, "Reached vCard number limit. Maybe there are too many vCard in the storage");
    538         mErrorReason = getString(R.string.fail_reason_too_many_vcard);
    539         return null;
    540     }
    541 }
    542