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.common.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.Environment;
26 import android.os.IBinder;
27 import android.os.Message;
28 import android.os.Messenger;
29 import android.os.RemoteException;
30 import android.text.TextUtils;
31 import android.util.Log;
32 import android.util.SparseArray;
33 
34 import com.android.contacts.common.R;
35 
36 import java.io.File;
37 import java.util.ArrayList;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.Set;
42 import java.util.concurrent.ExecutorService;
43 import java.util.concurrent.Executors;
44 import 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.
55 public class VCardService extends Service {
56     private final static String LOG_TAG = "VCardService";
57 
58     /* package */ final static boolean DEBUG = false;
59 
60     /**
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 
69     /* package */ static final String X_VCARD_MIME_TYPE = "text/x-vcard";
70 
71     private class CustomMediaScannerConnectionClient implements MediaScannerConnectionClient {
72         final MediaScannerConnection mConnection;
73         final String mPath;
74 
CustomMediaScannerConnectionClient(String path)75         public CustomMediaScannerConnectionClient(String path) {
76             mConnection = new MediaScannerConnection(VCardService.this, this);
77             mPath = path;
78         }
79 
start()80         public void start() {
81             mConnection.connect();
82         }
83 
84         @Override
onMediaScannerConnected()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
onScanCompleted(String path, Uri uri)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 
113     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 {
getService()122         public VCardService getService() {
123             return VCardService.this;
124         }
125     }
126 
127    @Override
onCreate()128     public void onCreate() {
129         super.onCreate();
130         mBinder = new MyBinder();
131         if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created.");
132     }
133 
134     @Override
onStartCommand(Intent intent, int flags, int id)135     public int onStartCommand(Intent intent, int flags, int id) {
136         if (intent != null && intent.getExtras() != null) {
137             mCallingActivity = intent.getExtras().getString(
138                     VCardCommonArguments.ARG_CALLING_ACTIVITY);
139         } else {
140             mCallingActivity = null;
141         }
142         return START_STICKY;
143     }
144 
145     @Override
onBind(Intent intent)146     public IBinder onBind(Intent intent) {
147         return mBinder;
148     }
149 
150     @Override
onDestroy()151     public void onDestroy() {
152         if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed.");
153         cancelAllRequestsAndShutdown();
154         clearCache();
155         super.onDestroy();
156     }
157 
handleImportRequest(List<ImportRequest> requests, VCardImportExportListener listener)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 
handleExportRequest(ExportRequest request, VCardImportExportListener listener)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      */
tryExecute(ProcessorBase processor)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 
handleCancelRequest(CancelRequest request, VCardImportExportListener listener)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 
264     /**
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      */
stopServiceIfAppropriate()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 
updateMediaScanner(String path)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 
removeConnectionClient( CustomMediaScannerConnectionClient client)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 
handleFinishImportNotification( int jobId, boolean successful)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 
handleFinishExportNotification( int jobId, boolean successful)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      */
cancelAllRequestsAndShutdown()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      */
clearCache()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     }
397 }
398