Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 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 | */ |
Gary Mai | 69c182a | 2016-12-05 13:07:03 -0800 | [diff] [blame] | 16 | package com.android.contacts.vcard; |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 17 | |
| 18 | import android.app.Service; |
| 19 | import android.content.Intent; |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 20 | import android.media.MediaScannerConnection; |
| 21 | import android.media.MediaScannerConnection.MediaScannerConnectionClient; |
| 22 | import android.net.Uri; |
| 23 | import android.os.Binder; |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 24 | import android.os.IBinder; |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 25 | import android.util.Log; |
| 26 | import android.util.SparseArray; |
| 27 | |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 28 | import java.util.ArrayList; |
| 29 | import java.util.HashSet; |
| 30 | import java.util.List; |
| 31 | import java.util.Set; |
| 32 | import java.util.concurrent.ExecutorService; |
| 33 | import java.util.concurrent.Executors; |
| 34 | import java.util.concurrent.RejectedExecutionException; |
| 35 | |
| 36 | /** |
| 37 | * The class responsible for handling vCard import/export requests. |
| 38 | * |
| 39 | * This Service creates one ImportRequest/ExportRequest object (as Runnable) per request and push |
| 40 | * it to {@link ExecutorService} with single thread executor. The executor handles each request |
| 41 | * one by one, and notifies users when needed. |
| 42 | */ |
| 43 | // TODO: Using IntentService looks simpler than using Service + ServiceConnection though this |
| 44 | // works fine enough. Investigate the feasibility. |
| 45 | public class VCardService extends Service { |
| 46 | private final static String LOG_TAG = "VCardService"; |
| 47 | |
| 48 | /* package */ final static boolean DEBUG = false; |
| 49 | |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 50 | /** |
| 51 | * Specifies the type of operation. Used when constructing a notification, canceling |
| 52 | * some operation, etc. |
| 53 | */ |
| 54 | /* package */ static final int TYPE_IMPORT = 1; |
| 55 | /* package */ static final int TYPE_EXPORT = 2; |
| 56 | |
| 57 | /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_"; |
| 58 | |
Walter Jang | 5443426 | 2015-08-11 09:18:35 -0700 | [diff] [blame] | 59 | /* package */ static final String X_VCARD_MIME_TYPE = "text/x-vcard"; |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 60 | |
| 61 | private class CustomMediaScannerConnectionClient implements MediaScannerConnectionClient { |
| 62 | final MediaScannerConnection mConnection; |
| 63 | final String mPath; |
| 64 | |
| 65 | public CustomMediaScannerConnectionClient(String path) { |
| 66 | mConnection = new MediaScannerConnection(VCardService.this, this); |
| 67 | mPath = path; |
| 68 | } |
| 69 | |
| 70 | public void start() { |
| 71 | mConnection.connect(); |
| 72 | } |
| 73 | |
| 74 | @Override |
| 75 | public void onMediaScannerConnected() { |
| 76 | if (DEBUG) { Log.d(LOG_TAG, "Connected to MediaScanner. Start scanning."); } |
| 77 | mConnection.scanFile(mPath, null); |
| 78 | } |
| 79 | |
| 80 | @Override |
| 81 | public void onScanCompleted(String path, Uri uri) { |
| 82 | if (DEBUG) { Log.d(LOG_TAG, "scan completed: " + path); } |
| 83 | mConnection.disconnect(); |
| 84 | removeConnectionClient(this); |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | // Should be single thread, as we don't want to simultaneously handle import and export |
| 89 | // requests. |
| 90 | private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor(); |
| 91 | |
| 92 | private int mCurrentJobId; |
| 93 | |
| 94 | // Stores all unfinished import/export jobs which will be executed by mExecutorService. |
| 95 | // Key is jobId. |
| 96 | private final SparseArray<ProcessorBase> mRunningJobMap = new SparseArray<ProcessorBase>(); |
| 97 | // Stores ScannerConnectionClient objects until they finish scanning requested files. |
| 98 | // Uses List class for simplicity. It's not costly as we won't have multiple objects in |
| 99 | // almost all cases. |
| 100 | private final List<CustomMediaScannerConnectionClient> mRemainingScannerConnections = |
| 101 | new ArrayList<CustomMediaScannerConnectionClient>(); |
| 102 | |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 103 | private MyBinder mBinder; |
| 104 | |
| 105 | private String mCallingActivity; |
| 106 | |
| 107 | // File names currently reserved by some export job. |
| 108 | private final Set<String> mReservedDestination = new HashSet<String>(); |
| 109 | /* ** end of vCard exporter params ** */ |
| 110 | |
| 111 | public class MyBinder extends Binder { |
| 112 | public VCardService getService() { |
| 113 | return VCardService.this; |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | @Override |
| 118 | public void onCreate() { |
| 119 | super.onCreate(); |
| 120 | mBinder = new MyBinder(); |
| 121 | if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created."); |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 122 | } |
| 123 | |
| 124 | @Override |
| 125 | public int onStartCommand(Intent intent, int flags, int id) { |
Yorke Lee | 41b8b64 | 2013-01-08 13:17:31 -0800 | [diff] [blame] | 126 | if (intent != null && intent.getExtras() != null) { |
| 127 | mCallingActivity = intent.getExtras().getString( |
| 128 | VCardCommonArguments.ARG_CALLING_ACTIVITY); |
| 129 | } else { |
| 130 | mCallingActivity = null; |
| 131 | } |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 132 | return START_STICKY; |
| 133 | } |
| 134 | |
| 135 | @Override |
| 136 | public IBinder onBind(Intent intent) { |
| 137 | return mBinder; |
| 138 | } |
| 139 | |
| 140 | @Override |
| 141 | public void onDestroy() { |
| 142 | if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed."); |
| 143 | cancelAllRequestsAndShutdown(); |
| 144 | clearCache(); |
| 145 | super.onDestroy(); |
| 146 | } |
| 147 | |
| 148 | public synchronized void handleImportRequest(List<ImportRequest> requests, |
| 149 | VCardImportExportListener listener) { |
| 150 | if (DEBUG) { |
| 151 | final ArrayList<String> uris = new ArrayList<String>(); |
| 152 | final ArrayList<String> displayNames = new ArrayList<String>(); |
| 153 | for (ImportRequest request : requests) { |
| 154 | uris.add(request.uri.toString()); |
| 155 | displayNames.add(request.displayName); |
| 156 | } |
| 157 | Log.d(LOG_TAG, |
| 158 | String.format("received multiple import request (uri: %s, displayName: %s)", |
| 159 | uris.toString(), displayNames.toString())); |
| 160 | } |
| 161 | final int size = requests.size(); |
| 162 | for (int i = 0; i < size; i++) { |
| 163 | ImportRequest request = requests.get(i); |
| 164 | |
| 165 | if (tryExecute(new ImportProcessor(this, listener, request, mCurrentJobId))) { |
| 166 | if (listener != null) { |
| 167 | listener.onImportProcessed(request, mCurrentJobId, i); |
| 168 | } |
| 169 | mCurrentJobId++; |
| 170 | } else { |
| 171 | if (listener != null) { |
| 172 | listener.onImportFailed(request); |
| 173 | } |
| 174 | // A rejection means executor doesn't run any more. Exit. |
| 175 | break; |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | public synchronized void handleExportRequest(ExportRequest request, |
| 181 | VCardImportExportListener listener) { |
| 182 | if (tryExecute(new ExportProcessor(this, request, mCurrentJobId, mCallingActivity))) { |
| 183 | final String path = request.destUri.getEncodedPath(); |
| 184 | if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path); |
| 185 | if (!mReservedDestination.add(path)) { |
| 186 | Log.w(LOG_TAG, |
| 187 | String.format("The path %s is already reserved. Reject export request", |
| 188 | path)); |
| 189 | if (listener != null) { |
| 190 | listener.onExportFailed(request); |
| 191 | } |
| 192 | return; |
| 193 | } |
| 194 | |
| 195 | if (listener != null) { |
| 196 | listener.onExportProcessed(request, mCurrentJobId); |
| 197 | } |
| 198 | mCurrentJobId++; |
| 199 | } else { |
| 200 | if (listener != null) { |
| 201 | listener.onExportFailed(request); |
| 202 | } |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor. |
| 208 | * @return true when successful. |
| 209 | */ |
| 210 | private synchronized boolean tryExecute(ProcessorBase processor) { |
| 211 | try { |
| 212 | if (DEBUG) { |
| 213 | Log.d(LOG_TAG, "Executor service status: shutdown: " + mExecutorService.isShutdown() |
| 214 | + ", terminated: " + mExecutorService.isTerminated()); |
| 215 | } |
| 216 | mExecutorService.execute(processor); |
| 217 | mRunningJobMap.put(mCurrentJobId, processor); |
| 218 | return true; |
| 219 | } catch (RejectedExecutionException e) { |
| 220 | Log.w(LOG_TAG, "Failed to excetute a job.", e); |
| 221 | return false; |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | public synchronized void handleCancelRequest(CancelRequest request, |
| 226 | VCardImportExportListener listener) { |
| 227 | final int jobId = request.jobId; |
| 228 | if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId)); |
| 229 | |
| 230 | final ProcessorBase processor = mRunningJobMap.get(jobId); |
| 231 | mRunningJobMap.remove(jobId); |
| 232 | |
| 233 | if (processor != null) { |
| 234 | processor.cancel(true); |
| 235 | final int type = processor.getType(); |
| 236 | if (listener != null) { |
| 237 | listener.onCancelRequest(request, type); |
| 238 | } |
| 239 | if (type == TYPE_EXPORT) { |
| 240 | final String path = |
| 241 | ((ExportProcessor)processor).getRequest().destUri.getEncodedPath(); |
| 242 | Log.i(LOG_TAG, |
| 243 | String.format("Cancel reservation for the path %s if appropriate", path)); |
| 244 | if (!mReservedDestination.remove(path)) { |
| 245 | Log.w(LOG_TAG, "Not reserved."); |
| 246 | } |
| 247 | } |
| 248 | } else { |
| 249 | Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId)); |
| 250 | } |
| 251 | stopServiceIfAppropriate(); |
| 252 | } |
| 253 | |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 254 | /** |
| 255 | * Checks job list and call {@link #stopSelf()} when there's no job and no scanner connection |
| 256 | * is remaining. |
| 257 | * A new job (import/export) cannot be submitted any more after this call. |
| 258 | */ |
| 259 | private synchronized void stopServiceIfAppropriate() { |
| 260 | if (mRunningJobMap.size() > 0) { |
| 261 | final int size = mRunningJobMap.size(); |
| 262 | |
| 263 | // Check if there are processors which aren't finished yet. If we still have ones to |
| 264 | // process, we cannot stop the service yet. Also clean up already finished processors |
| 265 | // here. |
| 266 | |
| 267 | // Job-ids to be removed. At first all elements in the array are invalid and will |
| 268 | // be filled with real job-ids from the array's top. When we find a not-yet-finished |
| 269 | // processor, then we start removing those finished jobs. In that case latter half of |
| 270 | // this array will be invalid. |
| 271 | final int[] toBeRemoved = new int[size]; |
| 272 | for (int i = 0; i < size; i++) { |
| 273 | final int jobId = mRunningJobMap.keyAt(i); |
| 274 | final ProcessorBase processor = mRunningJobMap.valueAt(i); |
| 275 | if (!processor.isDone()) { |
| 276 | Log.i(LOG_TAG, String.format("Found unfinished job (id: %d)", jobId)); |
| 277 | |
| 278 | // Remove processors which are already "done", all of which should be before |
| 279 | // processors which aren't done yet. |
| 280 | for (int j = 0; j < i; j++) { |
| 281 | mRunningJobMap.remove(toBeRemoved[j]); |
| 282 | } |
| 283 | return; |
| 284 | } |
| 285 | |
| 286 | // Remember the finished processor. |
| 287 | toBeRemoved[i] = jobId; |
| 288 | } |
| 289 | |
| 290 | // We're sure we can remove all. Instead of removing one by one, just call clear(). |
| 291 | mRunningJobMap.clear(); |
| 292 | } |
| 293 | |
| 294 | if (!mRemainingScannerConnections.isEmpty()) { |
| 295 | Log.i(LOG_TAG, "MediaScanner update is in progress."); |
| 296 | return; |
| 297 | } |
| 298 | |
| 299 | Log.i(LOG_TAG, "No unfinished job. Stop this service."); |
| 300 | mExecutorService.shutdown(); |
| 301 | stopSelf(); |
| 302 | } |
| 303 | |
| 304 | /* package */ synchronized void updateMediaScanner(String path) { |
| 305 | if (DEBUG) { |
| 306 | Log.d(LOG_TAG, "MediaScanner is being updated: " + path); |
| 307 | } |
| 308 | |
| 309 | if (mExecutorService.isShutdown()) { |
| 310 | Log.w(LOG_TAG, "MediaScanner update is requested after executor's being shut down. " + |
| 311 | "Ignoring the update request"); |
| 312 | return; |
| 313 | } |
| 314 | final CustomMediaScannerConnectionClient client = |
| 315 | new CustomMediaScannerConnectionClient(path); |
| 316 | mRemainingScannerConnections.add(client); |
| 317 | client.start(); |
| 318 | } |
| 319 | |
| 320 | private synchronized void removeConnectionClient( |
| 321 | CustomMediaScannerConnectionClient client) { |
| 322 | if (DEBUG) { |
| 323 | Log.d(LOG_TAG, "Removing custom MediaScannerConnectionClient."); |
| 324 | } |
| 325 | mRemainingScannerConnections.remove(client); |
| 326 | stopServiceIfAppropriate(); |
| 327 | } |
| 328 | |
| 329 | /* package */ synchronized void handleFinishImportNotification( |
| 330 | int jobId, boolean successful) { |
| 331 | if (DEBUG) { |
| 332 | Log.d(LOG_TAG, String.format("Received vCard import finish notification (id: %d). " |
| 333 | + "Result: %b", jobId, (successful ? "success" : "failure"))); |
| 334 | } |
| 335 | mRunningJobMap.remove(jobId); |
| 336 | stopServiceIfAppropriate(); |
| 337 | } |
| 338 | |
| 339 | /* package */ synchronized void handleFinishExportNotification( |
| 340 | int jobId, boolean successful) { |
| 341 | if (DEBUG) { |
| 342 | Log.d(LOG_TAG, String.format("Received vCard export finish notification (id: %d). " |
| 343 | + "Result: %b", jobId, (successful ? "success" : "failure"))); |
| 344 | } |
| 345 | final ProcessorBase job = mRunningJobMap.get(jobId); |
| 346 | mRunningJobMap.remove(jobId); |
| 347 | if (job == null) { |
| 348 | Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId)); |
| 349 | } else if (!(job instanceof ExportProcessor)) { |
| 350 | Log.w(LOG_TAG, |
| 351 | String.format("Removed job (id: %s) isn't ExportProcessor", jobId)); |
| 352 | } else { |
| 353 | final String path = ((ExportProcessor)job).getRequest().destUri.getEncodedPath(); |
| 354 | if (DEBUG) Log.d(LOG_TAG, "Remove reserved path " + path); |
| 355 | mReservedDestination.remove(path); |
| 356 | } |
| 357 | |
| 358 | stopServiceIfAppropriate(); |
| 359 | } |
| 360 | |
| 361 | /** |
| 362 | * Cancels all the import/export requests and calls {@link ExecutorService#shutdown()}, which |
| 363 | * means this Service becomes no longer ready for import/export requests. |
| 364 | * |
| 365 | * Mainly called from onDestroy(). |
| 366 | */ |
| 367 | private synchronized void cancelAllRequestsAndShutdown() { |
| 368 | for (int i = 0; i < mRunningJobMap.size(); i++) { |
| 369 | mRunningJobMap.valueAt(i).cancel(true); |
| 370 | } |
| 371 | mRunningJobMap.clear(); |
| 372 | mExecutorService.shutdown(); |
| 373 | } |
| 374 | |
| 375 | /** |
| 376 | * Removes import caches stored locally. |
| 377 | */ |
| 378 | private void clearCache() { |
| 379 | for (final String fileName : fileList()) { |
| 380 | if (fileName.startsWith(CACHE_FILE_PREFIX)) { |
| 381 | // We don't want to keep all the caches so we remove cache files old enough. |
| 382 | Log.i(LOG_TAG, "Remove a temporary file: " + fileName); |
| 383 | deleteFile(fileName); |
| 384 | } |
| 385 | } |
| 386 | } |
Chiao Cheng | d80c434 | 2012-12-03 17:15:58 -0800 | [diff] [blame] | 387 | } |