blob: f5387a180745ae28c5c8977e3cfef422892b8bbb [file] [log] [blame]
Chiao Chengd80c4342012-12-03 17:15:58 -08001/*
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 Mai69c182a2016-12-05 13:07:03 -080016package com.android.contacts.vcard;
Chiao Chengd80c4342012-12-03 17:15:58 -080017
18import android.app.Service;
19import android.content.Intent;
Chiao Chengd80c4342012-12-03 17:15:58 -080020import android.media.MediaScannerConnection;
21import android.media.MediaScannerConnection.MediaScannerConnectionClient;
22import android.net.Uri;
23import android.os.Binder;
Chiao Chengd80c4342012-12-03 17:15:58 -080024import android.os.IBinder;
Chiao Chengd80c4342012-12-03 17:15:58 -080025import android.util.Log;
26import android.util.SparseArray;
27
Chiao Chengd80c4342012-12-03 17:15:58 -080028import java.util.ArrayList;
29import java.util.HashSet;
30import java.util.List;
31import java.util.Set;
32import java.util.concurrent.ExecutorService;
33import java.util.concurrent.Executors;
34import 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.
45public class VCardService extends Service {
46 private final static String LOG_TAG = "VCardService";
47
48 /* package */ final static boolean DEBUG = false;
49
Chiao Chengd80c4342012-12-03 17:15:58 -080050 /**
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 Jang54434262015-08-11 09:18:35 -070059 /* package */ static final String X_VCARD_MIME_TYPE = "text/x-vcard";
Chiao Chengd80c4342012-12-03 17:15:58 -080060
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 Chengd80c4342012-12-03 17:15:58 -0800103 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 Chengd80c4342012-12-03 17:15:58 -0800122 }
123
124 @Override
125 public int onStartCommand(Intent intent, int flags, int id) {
Yorke Lee41b8b642013-01-08 13:17:31 -0800126 if (intent != null && intent.getExtras() != null) {
127 mCallingActivity = intent.getExtras().getString(
128 VCardCommonArguments.ARG_CALLING_ACTIVITY);
129 } else {
130 mCallingActivity = null;
131 }
Chiao Chengd80c4342012-12-03 17:15:58 -0800132 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 Chengd80c4342012-12-03 17:15:58 -0800254 /**
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 Chengd80c4342012-12-03 17:15:58 -0800387}