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