VCardService.java revision ab59660a17e896593f2a07c2e1191c2c23e3e353
1f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa/*
2f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa * Copyright (C) 2010 The Android Open Source Project
3f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa *
4f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa * Licensed under the Apache License, Version 2.0 (the "License");
5f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa * you may not use this file except in compliance with the License.
6f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa * You may obtain a copy of the License at
7f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa *
8f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa *      http://www.apache.org/licenses/LICENSE-2.0
9f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa *
10f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa * Unless required by applicable law or agreed to in writing, software
11f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa * distributed under the License is distributed on an "AS IS" BASIS,
12f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa * See the License for the specific language governing permissions and
14f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa * limitations under the License.
15f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa */
161b918e58f4a3ae8d32af83f6f69bbf2de57a94f9Daisuke Miyakawapackage com.android.contacts.vcard;
17f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
187c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport com.android.contacts.R;
197c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
20ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawaimport android.app.Notification;
21ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawaimport android.app.NotificationManager;
22ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawaimport android.app.PendingIntent;
23f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawaimport android.app.Service;
24ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawaimport android.content.Context;
25f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawaimport android.content.Intent;
26ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawaimport android.net.Uri;
27476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawaimport android.os.Handler;
28f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawaimport android.os.IBinder;
29476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawaimport android.os.Message;
30476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawaimport android.os.Messenger;
31f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawaimport android.util.Log;
32ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawaimport android.widget.RemoteViews;
33f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawaimport android.widget.Toast;
34f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
357c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport java.util.HashMap;
367c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport java.util.Map;
377c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport java.util.concurrent.ExecutorService;
387c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport java.util.concurrent.Executors;
397c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport java.util.concurrent.RejectedExecutionException;
401b918e58f4a3ae8d32af83f6f69bbf2de57a94f9Daisuke Miyakawa
41f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa/**
427c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa * The class responsible for handling vCard import/export requests.
437c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa *
447c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa * This Service creates one ImportRequest/ExportRequest object (as Runnable) per request and push
457c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa * it to {@link ExecutorService} with single thread executor. The executor handles each request
467c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa * one by one, and notifies users when needed.
47f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa */
487c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa// TODO: Using IntentService looks simpler than using Service + ServiceConnection though this
497c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa// works fine enough. Investigate the feasibility.
50d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawapublic class VCardService extends Service {
517c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    private final static String LOG_TAG = "VCardService";
52f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
53ef41f8866e8e7d52e04907f7282adcf5f4749f25Daisuke Miyakawa    /* package */ static final int MSG_IMPORT_REQUEST = 1;
54d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa    /* package */ static final int MSG_EXPORT_REQUEST = 2;
55ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static final int MSG_CANCEL_REQUEST = 3;
56f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
57ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /**
58ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Specifies the type of operation. Used when constructing a {@link Notification}, canceling
59ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * some operation, etc.
60ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     */
61ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static final int TYPE_IMPORT = 1;
62ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static final int TYPE_EXPORT = 2;
63f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
64aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_";
65f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
66910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    private final Messenger mMessenger = new Messenger(new Handler() {
67476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa        @Override
68476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa        public void handleMessage(Message msg) {
69476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa            switch (msg.what) {
70ef41f8866e8e7d52e04907f7282adcf5f4749f25Daisuke Miyakawa                case MSG_IMPORT_REQUEST: {
717c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa                    handleImportRequest((ImportRequest)msg.obj);
72476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa                    break;
73915723665339c73c9bdebc7bf8ef56c414602c2cDaisuke Miyakawa                }
74d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa                case MSG_EXPORT_REQUEST: {
757c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa                    handleExportRequest((ExportRequest)msg.obj);
76d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa                    break;
77d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa                }
78ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                case MSG_CANCEL_REQUEST: {
79ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    handleCancelRequest((CancelRequest)msg.obj);
8018b5190d6ed37be04d153a5d6f205076b38ac479Daisuke Miyakawa                    break;
8118b5190d6ed37be04d153a5d6f205076b38ac479Daisuke Miyakawa                }
827c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa                // TODO: add cancel capability for export..
83d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa                default: {
84aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa                    Log.w(LOG_TAG, "Received unknown request, ignoring it.");
85476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa                    super.hasMessages(msg.what);
86d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa                }
87476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa            }
88f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa        }
89910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    });
90f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
91ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    private NotificationManager mNotificationManager;
92ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
937c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    // Should be single thread, as we don't want to simultaneously handle import and export
947c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    // requests.
95910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
967c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
977c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    private int mCurrentJobId;
98910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa
99910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    // Stores all unfinished import/export jobs which will be executed by mExecutorService.
100910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    // Key is jobId.
101910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    private final Map<Integer, ProcessorBase> mRunningJobMap =
102910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            new HashMap<Integer, ProcessorBase>();
103476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa
104476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa    @Override
105ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    public void onCreate() {
106ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        super.onCreate();
107ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
108ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    }
109ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
110ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    @Override
111476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa    public int onStartCommand(Intent intent, int flags, int id) {
112476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa        return START_STICKY;
113f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa    }
114f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
115f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa    @Override
116f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa    public IBinder onBind(Intent intent) {
117476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa        return mMessenger.getBinder();
118f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa    }
119aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa
120aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    @Override
121aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    public void onDestroy() {
122910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        Log.i(LOG_TAG, "VCardService is being destroyed.");
123910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        cancelAllRequestsAndShutdown();
124aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa        clearCache();
125aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa        super.onDestroy();
126aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    }
127aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa
1287c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    private synchronized void handleImportRequest(ImportRequest request) {
129ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        if (tryExecute(new ImportProcessor(this, request, mCurrentJobId))) {
130ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final String displayName = request.originalUri.getLastPathSegment();
131ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final String message = getString(R.string.vcard_import_will_start_message,
132ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    displayName);
133ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            // TODO: Ideally we should detect the current status of import/export and show
134ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            // "started" when we can import right now and show "will start" when we cannot.
135ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Toast.makeText(this, message, Toast.LENGTH_LONG).show();
136ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
137ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final Notification notification =
138ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    constructProgressNotification(
139ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                            this, TYPE_IMPORT, message, message, mCurrentJobId,
140ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                            displayName, -1, 0);
141ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            mNotificationManager.notify(mCurrentJobId, notification);
142ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            mCurrentJobId++;
143ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        } else {
144ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            // TODO: a little unkind to show Toast in this case, which is shown just a moment.
145ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            // Ideally we should show some persistent something users can notice more easily.
146ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Toast.makeText(this, getString(R.string.vcard_import_request_rejected_message),
147ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    Toast.LENGTH_LONG).show();
148ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        }
1497c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
1507c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
1517c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    private synchronized void handleExportRequest(ExportRequest request) {
152ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        if (tryExecute(new ExportProcessor(this, request, mCurrentJobId))) {
153ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final String displayName = request.destUri.getLastPathSegment();
154ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final String message = getString(R.string.vcard_export_will_start_message,
155ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    displayName);
156ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Toast.makeText(this, message, Toast.LENGTH_LONG).show();
157ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final Notification notification =
158ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    constructProgressNotification(this, TYPE_EXPORT, message, message,
159ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                            mCurrentJobId, displayName, -1, 0);
160ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            mNotificationManager.notify(mCurrentJobId, notification);
161ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            mCurrentJobId++;
162ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        } else {
163ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Toast.makeText(this, getString(R.string.vcard_export_request_rejected_message),
164ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    Toast.LENGTH_LONG).show();
165ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        }
166910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    }
167910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa
168910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    /**
169ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor.
170ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @return true when successful.
171910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa     */
172ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    private synchronized boolean tryExecute(ProcessorBase processor) {
1737c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        try {
174910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            mExecutorService.execute(processor);
175910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            mRunningJobMap.put(mCurrentJobId, processor);
176ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            return true;
1777c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        } catch (RejectedExecutionException e) {
178910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            Log.w(LOG_TAG, "Failed to excetute a job.", e);
179ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            return false;
1807c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        }
1817c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
1827c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
183ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    private void handleCancelRequest(CancelRequest request) {
184ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final int jobId = request.jobId;
185ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        Log.i(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
186ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final ProcessorBase processor = mRunningJobMap.remove(jobId);
1877c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
188ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        if (processor != null) {
189ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            processor.cancel(true);
190ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final String description = processor.getType() == TYPE_IMPORT ?
191ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    getString(R.string.importing_vcard_canceled_title, request.displayName) :
192ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                            getString(R.string.exporting_vcard_canceled_title, request.displayName);
193ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final Notification notification = constructCancelNotification(this, description);
194ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            mNotificationManager.notify(jobId, notification);
195ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        } else {
196ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
1977c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        }
198f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        stopServiceWhenNoJob();
199f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa    }
200f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa
201f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa    /**
202f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     * Checks job list and call {@link #stopSelf()} when there's no job now.
203f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     * A new job cannot be submitted any more after this call.
204f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     */
205f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa    private synchronized void stopServiceWhenNoJob() {
206910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        if (mRunningJobMap.size() > 0) {
207910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            for (final Map.Entry<Integer, ProcessorBase> entry : mRunningJobMap.entrySet()) {
208f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa                final int jobId = entry.getKey();
209910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa                final ProcessorBase processor = entry.getValue();
210910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa                if (processor.isDone()) {
211910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa                    mRunningJobMap.remove(jobId);
212f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa                } else {
213f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa                    Log.i(LOG_TAG, String.format("Found unfinished job (id: %d)", jobId));
214f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa                    return;
215f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa                }
216f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa            }
217f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        }
218f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa
219f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        Log.i(LOG_TAG, "No unfinished job. Stop this service.");
220f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        mExecutorService.shutdown();
221f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        stopSelf();
2227c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
2237c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
2247c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    /* package */ synchronized void handleFinishImportNotification(
2257c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa            int jobId, boolean successful) {
226910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        Log.v(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
2277c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa                + "Result: %b", jobId, (successful ? "success" : "failure")));
228910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        if (mRunningJobMap.remove(jobId) == null) {
2297c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
2307c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        }
231f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        stopServiceWhenNoJob();
2327c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
2337c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
2347c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    /* package */ synchronized void handleFinishExportNotification(
2357c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa            int jobId, boolean successful) {
236910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        Log.v(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
2377c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa                + "Result: %b", jobId, (successful ? "success" : "failure")));
238910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        if (mRunningJobMap.remove(jobId) == null) {
2397c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
2407c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        }
241f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        stopServiceWhenNoJob();
2427c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
2437c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
2447c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    /**
245910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa     * Cancels all the import/export requests and calls {@link ExecutorService#shutdown()}, which
246f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     * means this Service becomes no longer ready for import/export requests.
247f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     *
248f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     * Mainly called from onDestroy().
2497c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa     */
250910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    private synchronized void cancelAllRequestsAndShutdown() {
251910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        for (final Map.Entry<Integer, ProcessorBase> entry : mRunningJobMap.entrySet()) {
252910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            entry.getValue().cancel(true);
2537c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        }
254910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        mRunningJobMap.clear();
255f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        mExecutorService.shutdown();
2567c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
2577c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
2587c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    /**
2597c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa     * Removes import caches stored locally.
2607c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa     */
261aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    private void clearCache() {
262910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        for (final String fileName : fileList()) {
263aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa            if (fileName.startsWith(CACHE_FILE_PREFIX)) {
264aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa                // We don't want to keep all the caches so we remove cache files old enough.
265aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa                Log.i(LOG_TAG, "Remove a temporary file: " + fileName);
266aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa                deleteFile(fileName);
267aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa            }
268aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa        }
269aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    }
270ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
271ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /**
272ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Constructs a {@link Notification} showing the current status of import/export.
273ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Users can cancel the process with the Notification.
274ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     *
275ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param context
276ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param type import/export
277ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param description Content of the Notification.
278ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param tickerText
279ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param jobId
280ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param displayName Name to be shown to the Notification (e.g. "finished importing XXXX").
281ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Typycally a file name.
282ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param totalCount The number of vCard entries to be imported. Used to show progress bar.
283ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * -1 lets the system show the progress bar with "indeterminate" state.
284ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param currentCount The index of current vCard. Used to show progress bar.
285ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     */
286ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static Notification constructProgressNotification(
287ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Context context, int type, String description, String tickerText,
288ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            int jobId, String displayName, int totalCount, int currentCount) {
289ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final RemoteViews remoteViews =
290ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                new RemoteViews(context.getPackageName(),
291ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                        R.layout.status_bar_ongoing_event_progress_bar);
292ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        remoteViews.setTextViewText(R.id.status_description, description);
293ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        remoteViews.setProgressBar(R.id.status_progress_bar, totalCount, currentCount,
294ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                totalCount == -1);
295ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final String percentage;
296ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        if (totalCount > 0) {
297ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            percentage = context.getString(R.string.percentage,
298ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    String.valueOf(currentCount * 100/totalCount));
299ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        } else {
300ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            percentage = "";
301ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        }
302ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        remoteViews.setTextViewText(R.id.status_progress_text, percentage);
303ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final int icon = (type == TYPE_IMPORT ? android.R.drawable.stat_sys_download :
304ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                android.R.drawable.stat_sys_upload);
305ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        remoteViews.setImageViewResource(R.id.status_icon, icon);
306ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
307ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final Notification notification = new Notification();
308ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.icon = icon;
309ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.tickerText = tickerText;
310ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.contentView = remoteViews;
311ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.flags |= Notification.FLAG_ONGOING_EVENT;
312ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
313ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // Note: We cannot use extra values here (like setIntExtra()), as PendingIntent doesn't
314ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // preserve them across multiple Notifications. PendingIntent preserves the first extras
315ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // (when flag is not set), or update them when PendingIntent#getActivity() is called
316ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // (See PendingIntent#FLAG_UPDATE_CURRENT). In either case, we cannot preserve extras as we
317ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // expect (for each vCard import/export request).
318ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        //
319ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // We use query parameter in Uri instead.
320ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // Scheme and Authority is arbitorary, assuming CancelActivity never refers them.
321ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final Intent intent = new Intent(context, CancelActivity.class);
322ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final Uri uri = (new Uri.Builder())
323ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                .scheme("invalidscheme")
324ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                .authority("invalidauthority")
325ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                .appendQueryParameter(CancelActivity.JOB_ID, String.valueOf(jobId))
326ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                .appendQueryParameter(CancelActivity.DISPLAY_NAME, displayName)
327ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                .appendQueryParameter(CancelActivity.TYPE, String.valueOf(type)).build();
328ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        intent.setData(uri);
329ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
330ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.contentIntent = PendingIntent.getActivity(context, 0, intent, 0);
331ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        return notification;
332ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    }
333ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
334ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /**
335ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Constructs a Notification telling users the process is canceled.
336ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     *
337ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param context
338ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param description Content of the Notification
339ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     */
340ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static Notification constructCancelNotification(
341ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Context context, String description) {
342ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final Notification notification = new Notification();
343ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.flags |= Notification.FLAG_AUTO_CANCEL;
344ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.icon = android.R.drawable.stat_notify_error;
345ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.setLatestEventInfo(context, description, description,
346ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                PendingIntent.getActivity(context, 0, null, 0));
347ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        return notification;
348ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    }
349ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
350ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /**
351ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Constructs a Notification telling users the process is finished.
352ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     *
353ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param context
354ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param description Content of the Notification
355ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param intent Intent to be launched when the Notification is clicked. Can be null.
356ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     */
357ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static Notification constructFinishNotification(
358ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Context context, String description, Intent intent) {
359ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final Notification notification = new Notification();
360ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.flags |= Notification.FLAG_AUTO_CANCEL;
361ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.icon = android.R.drawable.stat_sys_download_done;
362ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.setLatestEventInfo(context, description, description,
363ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                PendingIntent.getActivity(context, 0, intent, 0));
364ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        return notification;
365ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    }
366f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa}
367