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