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