VCardService.java revision 7c819a1a434e02c54f6d216aa3b1a0d08cc93f50
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 */
16package com.android.contacts.vcard;
17
18import com.android.contacts.R;
19
20import android.app.Service;
21import android.content.Intent;
22import android.os.Handler;
23import android.os.IBinder;
24import android.os.Message;
25import android.os.Messenger;
26import android.util.Log;
27import android.widget.Toast;
28
29import java.io.File;
30import java.util.Date;
31import java.util.HashMap;
32import java.util.Map;
33import java.util.concurrent.ExecutorService;
34import java.util.concurrent.Executors;
35import 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.
46public class VCardService extends Service {
47    private final static String LOG_TAG = "VCardService";
48
49    /* package */ static final int MSG_IMPORT_REQUEST = 1;
50    /* package */ static final int MSG_EXPORT_REQUEST = 2;
51    /* package */ static final int MSG_CANCEL_IMPORT_REQUEST = 3;
52
53    /* package */ static final int IMPORT_NOTIFICATION_ID = 1000;
54    /* package */ static final int EXPORT_NOTIFICATION_ID = 1001;
55
56    /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_";
57
58    public class RequestHandler extends Handler {
59        @Override
60        public void handleMessage(Message msg) {
61            switch (msg.what) {
62                case MSG_IMPORT_REQUEST: {
63                    handleImportRequest((ImportRequest)msg.obj);
64                    break;
65                }
66                case MSG_EXPORT_REQUEST: {
67                    handleExportRequest((ExportRequest)msg.obj);
68                    break;
69                }
70                case MSG_CANCEL_IMPORT_REQUEST: {
71                    handleCancelAllImportRequest();
72                    break;
73                }
74                // TODO: add cancel capability for export..
75                default: {
76                    Log.w(LOG_TAG, "Received unknown request, ignoring it.");
77                    super.hasMessages(msg.what);
78                }
79            }
80        }
81    }
82
83    private final Handler mHandler = new RequestHandler();
84    private final Messenger mMessenger = new Messenger(mHandler);
85    // Should be single thread, as we don't want to simultaneously handle import and export
86    // requests.
87    private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
88
89    private int mCurrentJobId;
90    private final Map<Integer, ImportProcessor> mRunningJobMapForImport =
91            new HashMap<Integer, ImportProcessor>();
92    private final Map<Integer, ExportProcessor> mRunningJobMapForExport =
93            new HashMap<Integer, ExportProcessor>();
94
95    @Override
96    public int onStartCommand(Intent intent, int flags, int id) {
97        return START_STICKY;
98    }
99
100    @Override
101    public IBinder onBind(Intent intent) {
102        return mMessenger.getBinder();
103    }
104
105    @Override
106    public void onDestroy() {
107        Log.i(LOG_TAG, "VCardService is finishing()");
108        cancelRequestsAndshutdown();
109        clearCache();
110        super.onDestroy();
111    }
112
113    private synchronized void handleImportRequest(ImportRequest request) {
114        Log.i(LOG_TAG, String.format("Received vCard import request. id: %d", mCurrentJobId));
115        final ImportProcessor importProcessor =
116                new ImportProcessor(this, request, mCurrentJobId);
117        try {
118            mExecutorService.submit(importProcessor);
119        } catch (RejectedExecutionException e) {
120            Log.w(LOG_TAG, "vCard import request is rejected.", e);
121            // TODO: a little unkind to show Toast in this case, which is shown just a moment.
122            // Ideally we should show some persistent something users can notice more easily.
123            Toast.makeText(this, getString(R.string.vcard_import_request_rejected_message),
124                    Toast.LENGTH_LONG).show();
125            return;
126        }
127        mRunningJobMapForImport.put(mCurrentJobId, importProcessor);
128        mCurrentJobId++;
129        // TODO: Ideally we should detect the current status of import/export and show "started"
130        // when we can import right now and show "will start" when we cannot.
131        Toast.makeText(this, getString(R.string.vcard_import_will_start_message),
132                Toast.LENGTH_LONG).show();
133    }
134
135    private synchronized void handleExportRequest(ExportRequest request) {
136        Log.i(LOG_TAG, String.format("Received vCard export request. id: %d", mCurrentJobId));
137        final ExportProcessor exportProcessor =
138                new ExportProcessor(this, request, mCurrentJobId);
139        try {
140            mExecutorService.submit(exportProcessor);
141        } catch (RejectedExecutionException e) {
142            Log.w(LOG_TAG, "vCard export request is rejected.", e);
143            Toast.makeText(this, getString(R.string.vcard_export_request_rejected_message),
144                    Toast.LENGTH_LONG).show();
145            return;
146        }
147        mRunningJobMapForExport.put(mCurrentJobId, exportProcessor);
148        mCurrentJobId++;
149        // See the comment in handleImportRequest()
150        Toast.makeText(this, getString(R.string.vcard_export_will_start_message),
151                Toast.LENGTH_LONG).show();
152    }
153
154    private synchronized void handleCancelAllImportRequest() {
155        Log.i(LOG_TAG, "Received cancel import request.");
156        cancelAllImportRequest();
157        mRunningJobMapForImport.clear();
158    }
159
160    private void cancelAllImportRequest() {
161        for (final Map.Entry<Integer, ImportProcessor> entry :
162                mRunningJobMapForImport.entrySet()) {
163            final int jobId = entry.getKey();
164            final ImportProcessor importProcessor = entry.getValue();
165            importProcessor.cancel();
166            Log.i(LOG_TAG, String.format("Canceling job %d", jobId));
167        }
168    }
169
170    private void cancelAllExportRequest() {
171        for (final Map.Entry<Integer, ExportProcessor> entry :
172                mRunningJobMapForExport.entrySet()) {
173            final int jobId = entry.getKey();
174            final ExportProcessor exportProcessor = entry.getValue();
175            exportProcessor.cancel();
176            Log.i(LOG_TAG, String.format("Canceling job %d", jobId));
177        }
178    }
179
180    /* package */ synchronized void handleFinishImportNotification(
181            int jobId, boolean successful) {
182        Log.i(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
183                + "Result: %b", jobId, (successful ? "success" : "failure")));
184        if (mRunningJobMapForImport.remove(jobId) == null) {
185            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
186        }
187    }
188
189    /* package */ synchronized void handleFinishExportNotification(
190            int jobId, boolean successful) {
191        Log.i(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
192                + "Result: %b", jobId, (successful ? "success" : "failure")));
193        if (mRunningJobMapForExport.remove(jobId) == null) {
194            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
195        }
196    }
197
198    /**
199     * Cancels all the import/export requests and call {@link ExecutorService#shutdown()}, which
200     * means this Service becomes no longer ready for import/export requests. Mainly used in
201     * onDestroy().
202     */
203    private synchronized void cancelRequestsAndshutdown() {
204        synchronized (this) {
205            if (mRunningJobMapForImport.size() > 0) {
206                Log.i(LOG_TAG,
207                        String.format("Cancel existing all import requests (remains: ",
208                                mRunningJobMapForImport.size()));
209                cancelAllImportRequest();
210            }
211            if (mRunningJobMapForExport.size() > 0) {
212                Log.i(LOG_TAG,
213                        String.format("Cancel existing all import requests (remains: ",
214                                mRunningJobMapForExport.size()));
215                cancelAllExportRequest();
216            }
217            mExecutorService.shutdown();
218        }
219    }
220
221    /**
222     * Removes import caches stored locally.
223     */
224    private void clearCache() {
225        Log.i(LOG_TAG, "start removing cache files if exist.");
226        final String[] fileLists = fileList();
227        for (String fileName : fileLists) {
228            if (fileName.startsWith(CACHE_FILE_PREFIX)) {
229                // We don't want to keep all the caches so we remove cache files old enough.
230                // TODO: Ideally we should ask VCardService whether the file is being used or
231                // going to be used.
232                final Date now = new Date();
233                final File file = getFileStreamPath(fileName);
234                Log.i(LOG_TAG, "Remove a temporary file: " + fileName);
235                deleteFile(fileName);
236            }
237        }
238    }
239}
240