diff options
author | Chiao Cheng <chiaocheng@google.com> | 2012-12-03 17:15:58 -0800 |
---|---|---|
committer | Chiao Cheng <chiaocheng@google.com> | 2012-12-03 17:15:58 -0800 |
commit | 7903d2473e1120e32fa5380a7d7532d0a21e2180 (patch) | |
tree | e38ffca11c65aeb7b12aeac85035548f1f7f4a2f /src/com/android/contacts/common/vcard/VCardService.java | |
parent | f0fb1f31683808c3ebfce234dc0a882e6e6cac1c (diff) | |
download | android_packages_apps_ContactsCommon-7903d2473e1120e32fa5380a7d7532d0a21e2180.tar.gz android_packages_apps_ContactsCommon-7903d2473e1120e32fa5380a7d7532d0a21e2180.tar.bz2 android_packages_apps_ContactsCommon-7903d2473e1120e32fa5380a7d7532d0a21e2180.zip |
Moving vcard UI to ContactsCommon.
Moving all class in vcard directory in preparation to move
ImportExportDialogFragment.
Bug: 6993891
Change-Id: I4391c6e63d20ebe91e240001885a6ce18388e51f
Diffstat (limited to 'src/com/android/contacts/common/vcard/VCardService.java')
-rw-r--r-- | src/com/android/contacts/common/vcard/VCardService.java | 537 |
1 files changed, 537 insertions, 0 deletions
diff --git a/src/com/android/contacts/common/vcard/VCardService.java b/src/com/android/contacts/common/vcard/VCardService.java new file mode 100644 index 00000000..07c2a3c1 --- /dev/null +++ b/src/com/android/contacts/common/vcard/VCardService.java @@ -0,0 +1,537 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.vcard; + +import android.app.Service; +import android.content.Intent; +import android.content.res.Resources; +import android.media.MediaScannerConnection; +import android.media.MediaScannerConnection.MediaScannerConnectionClient; +import android.net.Uri; +import android.os.Binder; +import android.os.Environment; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; + +import com.android.contacts.common.R; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +/** + * The class responsible for handling vCard import/export requests. + * + * This Service creates one ImportRequest/ExportRequest object (as Runnable) per request and push + * it to {@link ExecutorService} with single thread executor. The executor handles each request + * one by one, and notifies users when needed. + */ +// TODO: Using IntentService looks simpler than using Service + ServiceConnection though this +// works fine enough. Investigate the feasibility. +public class VCardService extends Service { + private final static String LOG_TAG = "VCardService"; + + /* package */ final static boolean DEBUG = false; + + /* package */ static final int MSG_IMPORT_REQUEST = 1; + /* package */ static final int MSG_EXPORT_REQUEST = 2; + /* package */ static final int MSG_CANCEL_REQUEST = 3; + /* package */ static final int MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION = 4; + /* package */ static final int MSG_SET_AVAILABLE_EXPORT_DESTINATION = 5; + + /** + * Specifies the type of operation. Used when constructing a notification, canceling + * some operation, etc. + */ + /* package */ static final int TYPE_IMPORT = 1; + /* package */ static final int TYPE_EXPORT = 2; + + /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_"; + + + private class CustomMediaScannerConnectionClient implements MediaScannerConnectionClient { + final MediaScannerConnection mConnection; + final String mPath; + + public CustomMediaScannerConnectionClient(String path) { + mConnection = new MediaScannerConnection(VCardService.this, this); + mPath = path; + } + + public void start() { + mConnection.connect(); + } + + @Override + public void onMediaScannerConnected() { + if (DEBUG) { Log.d(LOG_TAG, "Connected to MediaScanner. Start scanning."); } + mConnection.scanFile(mPath, null); + } + + @Override + public void onScanCompleted(String path, Uri uri) { + if (DEBUG) { Log.d(LOG_TAG, "scan completed: " + path); } + mConnection.disconnect(); + removeConnectionClient(this); + } + } + + // Should be single thread, as we don't want to simultaneously handle import and export + // requests. + private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor(); + + private int mCurrentJobId; + + // Stores all unfinished import/export jobs which will be executed by mExecutorService. + // Key is jobId. + private final SparseArray<ProcessorBase> mRunningJobMap = new SparseArray<ProcessorBase>(); + // Stores ScannerConnectionClient objects until they finish scanning requested files. + // Uses List class for simplicity. It's not costly as we won't have multiple objects in + // almost all cases. + private final List<CustomMediaScannerConnectionClient> mRemainingScannerConnections = + new ArrayList<CustomMediaScannerConnectionClient>(); + + /* ** vCard exporter params ** */ + // If true, VCardExporter is able to emits files longer than 8.3 format. + private static final boolean ALLOW_LONG_FILE_NAME = false; + + private File mTargetDirectory; + private String mFileNamePrefix; + private String mFileNameSuffix; + private int mFileIndexMinimum; + private int mFileIndexMaximum; + private String mFileNameExtension; + private Set<String> mExtensionsToConsider; + private String mErrorReason; + private MyBinder mBinder; + + private String mCallingActivity; + + // File names currently reserved by some export job. + private final Set<String> mReservedDestination = new HashSet<String>(); + /* ** end of vCard exporter params ** */ + + public class MyBinder extends Binder { + public VCardService getService() { + return VCardService.this; + } + } + + @Override + public void onCreate() { + super.onCreate(); + mBinder = new MyBinder(); + if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created."); + initExporterParams(); + } + + private void initExporterParams() { + mTargetDirectory = Environment.getExternalStorageDirectory(); + mFileNamePrefix = getString(R.string.config_export_file_prefix); + mFileNameSuffix = getString(R.string.config_export_file_suffix); + mFileNameExtension = getString(R.string.config_export_file_extension); + + mExtensionsToConsider = new HashSet<String>(); + mExtensionsToConsider.add(mFileNameExtension); + + final String additionalExtensions = + getString(R.string.config_export_extensions_to_consider); + if (!TextUtils.isEmpty(additionalExtensions)) { + for (String extension : additionalExtensions.split(",")) { + String trimed = extension.trim(); + if (trimed.length() > 0) { + mExtensionsToConsider.add(trimed); + } + } + } + + final Resources resources = getResources(); + mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index); + mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index); + } + + @Override + public int onStartCommand(Intent intent, int flags, int id) { + mCallingActivity = intent.getExtras().getString( + VCardCommonArguments.ARG_CALLING_ACTIVITY); + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed."); + cancelAllRequestsAndShutdown(); + clearCache(); + super.onDestroy(); + } + + public synchronized void handleImportRequest(List<ImportRequest> requests, + VCardImportExportListener listener) { + if (DEBUG) { + final ArrayList<String> uris = new ArrayList<String>(); + final ArrayList<String> displayNames = new ArrayList<String>(); + for (ImportRequest request : requests) { + uris.add(request.uri.toString()); + displayNames.add(request.displayName); + } + Log.d(LOG_TAG, + String.format("received multiple import request (uri: %s, displayName: %s)", + uris.toString(), displayNames.toString())); + } + final int size = requests.size(); + for (int i = 0; i < size; i++) { + ImportRequest request = requests.get(i); + + if (tryExecute(new ImportProcessor(this, listener, request, mCurrentJobId))) { + if (listener != null) { + listener.onImportProcessed(request, mCurrentJobId, i); + } + mCurrentJobId++; + } else { + if (listener != null) { + listener.onImportFailed(request); + } + // A rejection means executor doesn't run any more. Exit. + break; + } + } + } + + public synchronized void handleExportRequest(ExportRequest request, + VCardImportExportListener listener) { + if (tryExecute(new ExportProcessor(this, request, mCurrentJobId, mCallingActivity))) { + final String path = request.destUri.getEncodedPath(); + if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path); + if (!mReservedDestination.add(path)) { + Log.w(LOG_TAG, + String.format("The path %s is already reserved. Reject export request", + path)); + if (listener != null) { + listener.onExportFailed(request); + } + return; + } + + if (listener != null) { + listener.onExportProcessed(request, mCurrentJobId); + } + mCurrentJobId++; + } else { + if (listener != null) { + listener.onExportFailed(request); + } + } + } + + /** + * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor. + * @return true when successful. + */ + private synchronized boolean tryExecute(ProcessorBase processor) { + try { + if (DEBUG) { + Log.d(LOG_TAG, "Executor service status: shutdown: " + mExecutorService.isShutdown() + + ", terminated: " + mExecutorService.isTerminated()); + } + mExecutorService.execute(processor); + mRunningJobMap.put(mCurrentJobId, processor); + return true; + } catch (RejectedExecutionException e) { + Log.w(LOG_TAG, "Failed to excetute a job.", e); + return false; + } + } + + public synchronized void handleCancelRequest(CancelRequest request, + VCardImportExportListener listener) { + final int jobId = request.jobId; + if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId)); + + final ProcessorBase processor = mRunningJobMap.get(jobId); + mRunningJobMap.remove(jobId); + + if (processor != null) { + processor.cancel(true); + final int type = processor.getType(); + if (listener != null) { + listener.onCancelRequest(request, type); + } + if (type == TYPE_EXPORT) { + final String path = + ((ExportProcessor)processor).getRequest().destUri.getEncodedPath(); + Log.i(LOG_TAG, + String.format("Cancel reservation for the path %s if appropriate", path)); + if (!mReservedDestination.remove(path)) { + Log.w(LOG_TAG, "Not reserved."); + } + } + } else { + Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId)); + } + stopServiceIfAppropriate(); + } + + public synchronized void handleRequestAvailableExportDestination(final Messenger messenger) { + if (DEBUG) Log.d(LOG_TAG, "Received available export destination request."); + final String path = getAppropriateDestination(mTargetDirectory); + final Message message; + if (path != null) { + message = Message.obtain(null, + VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION, 0, 0, path); + } else { + message = Message.obtain(null, + VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION, + R.id.dialog_fail_to_export_with_reason, 0, mErrorReason); + } + try { + messenger.send(message); + } catch (RemoteException e) { + Log.w(LOG_TAG, "Failed to send reply for available export destination request.", e); + } + } + + /** + * Checks job list and call {@link #stopSelf()} when there's no job and no scanner connection + * is remaining. + * A new job (import/export) cannot be submitted any more after this call. + */ + private synchronized void stopServiceIfAppropriate() { + if (mRunningJobMap.size() > 0) { + final int size = mRunningJobMap.size(); + + // Check if there are processors which aren't finished yet. If we still have ones to + // process, we cannot stop the service yet. Also clean up already finished processors + // here. + + // Job-ids to be removed. At first all elements in the array are invalid and will + // be filled with real job-ids from the array's top. When we find a not-yet-finished + // processor, then we start removing those finished jobs. In that case latter half of + // this array will be invalid. + final int[] toBeRemoved = new int[size]; + for (int i = 0; i < size; i++) { + final int jobId = mRunningJobMap.keyAt(i); + final ProcessorBase processor = mRunningJobMap.valueAt(i); + if (!processor.isDone()) { + Log.i(LOG_TAG, String.format("Found unfinished job (id: %d)", jobId)); + + // Remove processors which are already "done", all of which should be before + // processors which aren't done yet. + for (int j = 0; j < i; j++) { + mRunningJobMap.remove(toBeRemoved[j]); + } + return; + } + + // Remember the finished processor. + toBeRemoved[i] = jobId; + } + + // We're sure we can remove all. Instead of removing one by one, just call clear(). + mRunningJobMap.clear(); + } + + if (!mRemainingScannerConnections.isEmpty()) { + Log.i(LOG_TAG, "MediaScanner update is in progress."); + return; + } + + Log.i(LOG_TAG, "No unfinished job. Stop this service."); + mExecutorService.shutdown(); + stopSelf(); + } + + /* package */ synchronized void updateMediaScanner(String path) { + if (DEBUG) { + Log.d(LOG_TAG, "MediaScanner is being updated: " + path); + } + + if (mExecutorService.isShutdown()) { + Log.w(LOG_TAG, "MediaScanner update is requested after executor's being shut down. " + + "Ignoring the update request"); + return; + } + final CustomMediaScannerConnectionClient client = + new CustomMediaScannerConnectionClient(path); + mRemainingScannerConnections.add(client); + client.start(); + } + + private synchronized void removeConnectionClient( + CustomMediaScannerConnectionClient client) { + if (DEBUG) { + Log.d(LOG_TAG, "Removing custom MediaScannerConnectionClient."); + } + mRemainingScannerConnections.remove(client); + stopServiceIfAppropriate(); + } + + /* package */ synchronized void handleFinishImportNotification( + int jobId, boolean successful) { + if (DEBUG) { + Log.d(LOG_TAG, String.format("Received vCard import finish notification (id: %d). " + + "Result: %b", jobId, (successful ? "success" : "failure"))); + } + mRunningJobMap.remove(jobId); + stopServiceIfAppropriate(); + } + + /* package */ synchronized void handleFinishExportNotification( + int jobId, boolean successful) { + if (DEBUG) { + Log.d(LOG_TAG, String.format("Received vCard export finish notification (id: %d). " + + "Result: %b", jobId, (successful ? "success" : "failure"))); + } + final ProcessorBase job = mRunningJobMap.get(jobId); + mRunningJobMap.remove(jobId); + if (job == null) { + Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId)); + } else if (!(job instanceof ExportProcessor)) { + Log.w(LOG_TAG, + String.format("Removed job (id: %s) isn't ExportProcessor", jobId)); + } else { + final String path = ((ExportProcessor)job).getRequest().destUri.getEncodedPath(); + if (DEBUG) Log.d(LOG_TAG, "Remove reserved path " + path); + mReservedDestination.remove(path); + } + + stopServiceIfAppropriate(); + } + + /** + * Cancels all the import/export requests and calls {@link ExecutorService#shutdown()}, which + * means this Service becomes no longer ready for import/export requests. + * + * Mainly called from onDestroy(). + */ + private synchronized void cancelAllRequestsAndShutdown() { + for (int i = 0; i < mRunningJobMap.size(); i++) { + mRunningJobMap.valueAt(i).cancel(true); + } + mRunningJobMap.clear(); + mExecutorService.shutdown(); + } + + /** + * Removes import caches stored locally. + */ + private void clearCache() { + for (final String fileName : fileList()) { + if (fileName.startsWith(CACHE_FILE_PREFIX)) { + // We don't want to keep all the caches so we remove cache files old enough. + Log.i(LOG_TAG, "Remove a temporary file: " + fileName); + deleteFile(fileName); + } + } + } + + /** + * Returns an appropriate file name for vCard export. Returns null when impossible. + * + * @return destination path for a vCard file to be exported. null on error and mErrorReason + * is correctly set. + */ + private String getAppropriateDestination(final File destDirectory) { + /* + * Here, file names have 5 parts: directory, prefix, index, suffix, and extension. + * e.g. "/mnt/sdcard/prfx00001sfx.vcf" -> "/mnt/sdcard", "prfx", "00001", "sfx", and ".vcf" + * (In default, prefix and suffix is empty, so usually the destination would be + * /mnt/sdcard/00001.vcf.) + * + * This method increments "index" part from 1 to maximum, and checks whether any file name + * following naming rule is available. If there's no file named /mnt/sdcard/00001.vcf, the + * name will be returned to a caller. If there are 00001.vcf 00002.vcf, 00003.vcf is + * returned. + * + * There may not be any appropriate file name. If there are 99999 vCard files in the + * storage, for example, there's no appropriate name, so this method returns + * null. + */ + + // Count the number of digits of mFileIndexMaximum + // e.g. When mFileIndexMaximum is 99999, fileIndexDigit becomes 5, as we will count the + int fileIndexDigit = 0; + { + // Calling Math.Log10() is costly. + int tmp; + for (fileIndexDigit = 0, tmp = mFileIndexMaximum; tmp > 0; + fileIndexDigit++, tmp /= 10) { + } + } + + // %s05d%s (e.g. "p00001s") + final String bodyFormat = "%s%0" + fileIndexDigit + "d%s"; + + if (!ALLOW_LONG_FILE_NAME) { + final String possibleBody = + String.format(bodyFormat, mFileNamePrefix, 1, mFileNameSuffix); + if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) { + Log.e(LOG_TAG, "This code does not allow any long file name."); + mErrorReason = getString(R.string.fail_reason_too_long_filename, + String.format("%s.%s", possibleBody, mFileNameExtension)); + Log.w(LOG_TAG, "File name becomes too long."); + return null; + } + } + + for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) { + boolean numberIsAvailable = true; + final String body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix); + // Make sure that none of the extensions of mExtensionsToConsider matches. If this + // number is free, we'll go ahead with mFileNameExtension (which is included in + // mExtensionsToConsider) + for (String possibleExtension : mExtensionsToConsider) { + final File file = new File(destDirectory, body + "." + possibleExtension); + final String path = file.getAbsolutePath(); + synchronized (this) { + // Is this being exported right now? Skip this number + if (mReservedDestination.contains(path)) { + if (DEBUG) { + Log.d(LOG_TAG, String.format("%s is already being exported.", path)); + } + numberIsAvailable = false; + break; + } + } + if (file.exists()) { + numberIsAvailable = false; + break; + } + } + if (numberIsAvailable) { + return new File(destDirectory, body + "." + mFileNameExtension).getAbsolutePath(); + } + } + + Log.w(LOG_TAG, "Reached vCard number limit. Maybe there are too many vCard in the storage"); + mErrorReason = getString(R.string.fail_reason_too_many_vcard); + return null; + } +} |