VCardService.java revision 783a09a8770f4322a45cee456adefbbc71218ece
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;
26783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawaimport android.content.res.Resources;
27ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawaimport android.net.Uri;
28476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawaimport android.os.Handler;
29f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawaimport android.os.IBinder;
30476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawaimport android.os.Message;
31476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawaimport android.os.Messenger;
32783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawaimport android.os.RemoteException;
33783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawaimport android.text.TextUtils;
34f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawaimport android.util.Log;
35ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawaimport android.widget.RemoteViews;
36f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawaimport android.widget.Toast;
37f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
38783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawaimport java.io.File;
397c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport java.util.HashMap;
40783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawaimport java.util.HashSet;
417c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport java.util.Map;
42783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawaimport java.util.Set;
437c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport java.util.concurrent.ExecutorService;
447c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport java.util.concurrent.Executors;
457c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawaimport java.util.concurrent.RejectedExecutionException;
461b918e58f4a3ae8d32af83f6f69bbf2de57a94f9Daisuke Miyakawa
47f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa/**
487c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa * The class responsible for handling vCard import/export requests.
497c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa *
507c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa * This Service creates one ImportRequest/ExportRequest object (as Runnable) per request and push
517c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa * it to {@link ExecutorService} with single thread executor. The executor handles each request
527c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa * one by one, and notifies users when needed.
53f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa */
547c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa// TODO: Using IntentService looks simpler than using Service + ServiceConnection though this
557c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa// works fine enough. Investigate the feasibility.
56d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawapublic class VCardService extends Service {
577c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    private final static String LOG_TAG = "VCardService";
58783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    /* package */ final static boolean DEBUG = true;
59f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
60ef41f8866e8e7d52e04907f7282adcf5f4749f25Daisuke Miyakawa    /* package */ static final int MSG_IMPORT_REQUEST = 1;
61d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa    /* package */ static final int MSG_EXPORT_REQUEST = 2;
62ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static final int MSG_CANCEL_REQUEST = 3;
63783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    /* package */ static final int MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION = 4;
64783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    /* package */ static final int MSG_SET_AVAILABLE_EXPORT_DESTINATION = 5;
65f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
66ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /**
67ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Specifies the type of operation. Used when constructing a {@link Notification}, canceling
68ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * some operation, etc.
69ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     */
70ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static final int TYPE_IMPORT = 1;
71ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static final int TYPE_EXPORT = 2;
72f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
73aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_";
74f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
75910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    private final Messenger mMessenger = new Messenger(new Handler() {
76476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa        @Override
77476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa        public void handleMessage(Message msg) {
78476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa            switch (msg.what) {
79ef41f8866e8e7d52e04907f7282adcf5f4749f25Daisuke Miyakawa                case MSG_IMPORT_REQUEST: {
807c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa                    handleImportRequest((ImportRequest)msg.obj);
81476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa                    break;
82915723665339c73c9bdebc7bf8ef56c414602c2cDaisuke Miyakawa                }
83d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa                case MSG_EXPORT_REQUEST: {
847c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa                    handleExportRequest((ExportRequest)msg.obj);
85d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa                    break;
86d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa                }
87ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                case MSG_CANCEL_REQUEST: {
88ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    handleCancelRequest((CancelRequest)msg.obj);
8918b5190d6ed37be04d153a5d6f205076b38ac479Daisuke Miyakawa                    break;
9018b5190d6ed37be04d153a5d6f205076b38ac479Daisuke Miyakawa                }
91783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                case MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION: {
92783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    handleRequestAvailableExportDestination(msg);
93783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    break;
94783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                }
957c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa                // TODO: add cancel capability for export..
96d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa                default: {
97aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa                    Log.w(LOG_TAG, "Received unknown request, ignoring it.");
98476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa                    super.hasMessages(msg.what);
99d8fb81a0024d30c027ea6ebf57d29d3ff10453fbDaisuke Miyakawa                }
100476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa            }
101f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa        }
102910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    });
103f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
104ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    private NotificationManager mNotificationManager;
105ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
1067c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    // Should be single thread, as we don't want to simultaneously handle import and export
1077c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    // requests.
108910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
1097c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
1107c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    private int mCurrentJobId;
111910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa
112910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    // Stores all unfinished import/export jobs which will be executed by mExecutorService.
113910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    // Key is jobId.
114910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    private final Map<Integer, ProcessorBase> mRunningJobMap =
115910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            new HashMap<Integer, ProcessorBase>();
116476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa
117783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    /* ** vCard exporter params ** */
118783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    // If true, VCardExporter is able to emits files longer than 8.3 format.
119783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private static final boolean ALLOW_LONG_FILE_NAME = false;
120783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private String mTargetDirectory;
121783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private String mFileNamePrefix;
122783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private String mFileNameSuffix;
123783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private int mFileIndexMinimum;
124783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private int mFileIndexMaximum;
125783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private String mFileNameExtension;
126783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private Set<String> mExtensionsToConsider;
127783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private String mErrorReason;
128783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
129783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    // File names currently reserved by some export job.
130783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private final Set<String> mReservedDestination = new HashSet<String>();
131783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    /* ** end of vCard exporter params ** */
132783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
133476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa    @Override
134ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    public void onCreate() {
135ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        super.onCreate();
136783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created.");
137ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
138783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        initExporterParams();
139783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    }
140783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
141783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private void initExporterParams() {
142783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        mTargetDirectory = getString(R.string.config_export_dir);
143783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        mFileNamePrefix = getString(R.string.config_export_file_prefix);
144783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        mFileNameSuffix = getString(R.string.config_export_file_suffix);
145783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        mFileNameExtension = getString(R.string.config_export_file_extension);
146783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
147783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        mExtensionsToConsider = new HashSet<String>();
148783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        mExtensionsToConsider.add(mFileNameExtension);
149783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
150783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        final String additionalExtensions =
151783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            getString(R.string.config_export_extensions_to_consider);
152783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (!TextUtils.isEmpty(additionalExtensions)) {
153783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            for (String extension : additionalExtensions.split(",")) {
154783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                String trimed = extension.trim();
155783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                if (trimed.length() > 0) {
156783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    mExtensionsToConsider.add(trimed);
157783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                }
158783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            }
159783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        }
160783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
161783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        final Resources resources = getResources();
162783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index);
163783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index);
164ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    }
165ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
166ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    @Override
167476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa    public int onStartCommand(Intent intent, int flags, int id) {
168476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa        return START_STICKY;
169f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa    }
170f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa
171f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa    @Override
172f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa    public IBinder onBind(Intent intent) {
173476004be0c4907b462b3d699671d9e1cff1a7bd7Daisuke Miyakawa        return mMessenger.getBinder();
174f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa    }
175aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa
176aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    @Override
177aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    public void onDestroy() {
178783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed.");
179910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        cancelAllRequestsAndShutdown();
180aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa        clearCache();
181aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa        super.onDestroy();
182aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    }
183aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa
1847c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    private synchronized void handleImportRequest(ImportRequest request) {
185783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (DEBUG) {
186783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            Log.d(LOG_TAG,
187783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    String.format("received import request (uri: %s, originalUri: %s)",
188783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                            request.uri, request.originalUri));
189783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        }
190ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        if (tryExecute(new ImportProcessor(this, request, mCurrentJobId))) {
191ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final String displayName = request.originalUri.getLastPathSegment();
192ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final String message = getString(R.string.vcard_import_will_start_message,
193ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    displayName);
194ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            // TODO: Ideally we should detect the current status of import/export and show
195ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            // "started" when we can import right now and show "will start" when we cannot.
196ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Toast.makeText(this, message, Toast.LENGTH_LONG).show();
197ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
198ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final Notification notification =
199ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    constructProgressNotification(
200ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                            this, TYPE_IMPORT, message, message, mCurrentJobId,
201ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                            displayName, -1, 0);
202ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            mNotificationManager.notify(mCurrentJobId, notification);
203ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            mCurrentJobId++;
204ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        } else {
205ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            // TODO: a little unkind to show Toast in this case, which is shown just a moment.
206ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            // Ideally we should show some persistent something users can notice more easily.
207ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Toast.makeText(this, getString(R.string.vcard_import_request_rejected_message),
208ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    Toast.LENGTH_LONG).show();
209ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        }
2107c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
2117c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
2127c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    private synchronized void handleExportRequest(ExportRequest request) {
213ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        if (tryExecute(new ExportProcessor(this, request, mCurrentJobId))) {
214ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final String displayName = request.destUri.getLastPathSegment();
215ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final String message = getString(R.string.vcard_export_will_start_message,
216ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    displayName);
217783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
218783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            final String path = request.destUri.getEncodedPath();
219783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path);
220783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            if (!mReservedDestination.add(path)) {
221783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                Log.w(LOG_TAG,
222783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                        String.format("The path %s is already reserved. Reject export request",
223783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                                path));
224783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                Toast.makeText(this, getString(R.string.vcard_export_request_rejected_message),
225783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                        Toast.LENGTH_LONG).show();
226783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                return;
227783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            }
228783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
229ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Toast.makeText(this, message, Toast.LENGTH_LONG).show();
230ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final Notification notification =
231ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    constructProgressNotification(this, TYPE_EXPORT, message, message,
232ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                            mCurrentJobId, displayName, -1, 0);
233ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            mNotificationManager.notify(mCurrentJobId, notification);
234ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            mCurrentJobId++;
235ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        } else {
236ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Toast.makeText(this, getString(R.string.vcard_export_request_rejected_message),
237ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    Toast.LENGTH_LONG).show();
238ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        }
239910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    }
240910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa
241910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    /**
242ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor.
243ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @return true when successful.
244910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa     */
245ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    private synchronized boolean tryExecute(ProcessorBase processor) {
2467c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        try {
247910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            mExecutorService.execute(processor);
248910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            mRunningJobMap.put(mCurrentJobId, processor);
249ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            return true;
2507c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        } catch (RejectedExecutionException e) {
251910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            Log.w(LOG_TAG, "Failed to excetute a job.", e);
252ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            return false;
2537c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        }
2547c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
2557c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
256783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private synchronized void handleCancelRequest(CancelRequest request) {
257ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final int jobId = request.jobId;
258783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
259ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final ProcessorBase processor = mRunningJobMap.remove(jobId);
2607c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
261ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        if (processor != null) {
262ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            processor.cancel(true);
263ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final String description = processor.getType() == TYPE_IMPORT ?
264ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    getString(R.string.importing_vcard_canceled_title, request.displayName) :
265ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                            getString(R.string.exporting_vcard_canceled_title, request.displayName);
266ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            final Notification notification = constructCancelNotification(this, description);
267ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            mNotificationManager.notify(jobId, notification);
268783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            if (processor.getType() == TYPE_EXPORT) {
269783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                final String path =
270783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                        ((ExportProcessor)processor).getRequest().destUri.getEncodedPath();
271783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                Log.i(LOG_TAG,
272783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                        String.format("Cancel reservation for the path %s if appropriate", path));
273783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                if (!mReservedDestination.remove(path)) {
274783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    Log.w(LOG_TAG, "Not reserved.");
275783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                }
276783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            }
277ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        } else {
278ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
2797c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        }
280f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        stopServiceWhenNoJob();
281f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa    }
282f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa
283783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private synchronized void handleRequestAvailableExportDestination(Message msg) {
284783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (DEBUG) Log.d(LOG_TAG, "Received available export destination request.");
285783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        final Messenger messenger = msg.replyTo;
286783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        final String path = getAppropriateDestination(mTargetDirectory);
287783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        final Message message;
288783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (path != null) {
289783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            message = Message.obtain(null,
290783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION, 0, 0, path);
291783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        } else {
292783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            message = Message.obtain(null,
293783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION,
294783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    R.id.dialog_fail_to_export_with_reason, 0, mErrorReason);
295783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        }
296783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        try {
297783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            messenger.send(message);
298783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        } catch (RemoteException e) {
299783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            Log.w(LOG_TAG, "Failed to send reply for available export destination request.", e);
300783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        }
301783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    }
302783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
303f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa    /**
304f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     * Checks job list and call {@link #stopSelf()} when there's no job now.
305f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     * A new job cannot be submitted any more after this call.
306f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     */
307f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa    private synchronized void stopServiceWhenNoJob() {
308910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        if (mRunningJobMap.size() > 0) {
309910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            for (final Map.Entry<Integer, ProcessorBase> entry : mRunningJobMap.entrySet()) {
310f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa                final int jobId = entry.getKey();
311910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa                final ProcessorBase processor = entry.getValue();
312910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa                if (processor.isDone()) {
313910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa                    mRunningJobMap.remove(jobId);
314f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa                } else {
315f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa                    Log.i(LOG_TAG, String.format("Found unfinished job (id: %d)", jobId));
316f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa                    return;
317f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa                }
318f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa            }
319f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        }
320f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa
321f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        Log.i(LOG_TAG, "No unfinished job. Stop this service.");
322f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        mExecutorService.shutdown();
323f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        stopSelf();
3247c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
3257c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
3267c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    /* package */ synchronized void handleFinishImportNotification(
3277c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa            int jobId, boolean successful) {
328783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (DEBUG) {
329783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            Log.d(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
330783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    + "Result: %b", jobId, (successful ? "success" : "failure")));
331783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        }
332910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        if (mRunningJobMap.remove(jobId) == null) {
3337c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
3347c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        }
335f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        stopServiceWhenNoJob();
3367c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
3377c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
3387c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    /* package */ synchronized void handleFinishExportNotification(
3397c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa            int jobId, boolean successful) {
340783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (DEBUG) {
341783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            Log.d(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
342783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    + "Result: %b", jobId, (successful ? "success" : "failure")));
343783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        }
344783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        final ProcessorBase job = mRunningJobMap.remove(jobId);
345783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (job == null) {
3467c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
347783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        } else if (!(job instanceof ExportProcessor)) {
348783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            Log.w(LOG_TAG,
349783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    String.format("Removed job (id: %s) isn't ExportProcessor", jobId));
350783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        } else {
351783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            final String path = ((ExportProcessor)job).getRequest().destUri.getEncodedPath();
352783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            if (DEBUG) Log.d(LOG_TAG, "Remove reserved path " + path);
353783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            mReservedDestination.remove(path);
3547c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        }
355783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
356f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        stopServiceWhenNoJob();
3577c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
3587c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
3597c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    /**
360910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa     * Cancels all the import/export requests and calls {@link ExecutorService#shutdown()}, which
361f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     * means this Service becomes no longer ready for import/export requests.
362f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     *
363f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa     * Mainly called from onDestroy().
3647c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa     */
365910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa    private synchronized void cancelAllRequestsAndShutdown() {
366910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        for (final Map.Entry<Integer, ProcessorBase> entry : mRunningJobMap.entrySet()) {
367910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa            entry.getValue().cancel(true);
3687c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa        }
369910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        mRunningJobMap.clear();
370f219f6ee48c503bb2b628c3f2aeff53b15c5a947Daisuke Miyakawa        mExecutorService.shutdown();
3717c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    }
3727c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa
3737c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa    /**
3747c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa     * Removes import caches stored locally.
3757c819a1a434e02c54f6d216aa3b1a0d08cc93f50Daisuke Miyakawa     */
376aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    private void clearCache() {
377910d3e7854e657d20ab8c3a5a330b2a3188b1c74Daisuke Miyakawa        for (final String fileName : fileList()) {
378aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa            if (fileName.startsWith(CACHE_FILE_PREFIX)) {
379aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa                // We don't want to keep all the caches so we remove cache files old enough.
380aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa                Log.i(LOG_TAG, "Remove a temporary file: " + fileName);
381aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa                deleteFile(fileName);
382aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa            }
383aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa        }
384aba4353bcf77ba91463cfd079feec3b6c6f59c5cDaisuke Miyakawa    }
385ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
386ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /**
387ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Constructs a {@link Notification} showing the current status of import/export.
388ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Users can cancel the process with the Notification.
389ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     *
390ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param context
391ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param type import/export
392ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param description Content of the Notification.
393ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param tickerText
394ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param jobId
395ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param displayName Name to be shown to the Notification (e.g. "finished importing XXXX").
396ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Typycally a file name.
397ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param totalCount The number of vCard entries to be imported. Used to show progress bar.
398ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * -1 lets the system show the progress bar with "indeterminate" state.
399ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param currentCount The index of current vCard. Used to show progress bar.
400ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     */
401ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static Notification constructProgressNotification(
402ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Context context, int type, String description, String tickerText,
403ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            int jobId, String displayName, int totalCount, int currentCount) {
404ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final RemoteViews remoteViews =
405ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                new RemoteViews(context.getPackageName(),
406ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                        R.layout.status_bar_ongoing_event_progress_bar);
407ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        remoteViews.setTextViewText(R.id.status_description, description);
408ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        remoteViews.setProgressBar(R.id.status_progress_bar, totalCount, currentCount,
409ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                totalCount == -1);
410ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final String percentage;
411ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        if (totalCount > 0) {
412ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            percentage = context.getString(R.string.percentage,
413ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                    String.valueOf(currentCount * 100/totalCount));
414ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        } else {
415ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            percentage = "";
416ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        }
417ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        remoteViews.setTextViewText(R.id.status_progress_text, percentage);
418ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final int icon = (type == TYPE_IMPORT ? android.R.drawable.stat_sys_download :
419ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                android.R.drawable.stat_sys_upload);
420ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        remoteViews.setImageViewResource(R.id.status_icon, icon);
421ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
422ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final Notification notification = new Notification();
423ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.icon = icon;
424ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.tickerText = tickerText;
425ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.contentView = remoteViews;
426ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.flags |= Notification.FLAG_ONGOING_EVENT;
427ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
428ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // Note: We cannot use extra values here (like setIntExtra()), as PendingIntent doesn't
429ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // preserve them across multiple Notifications. PendingIntent preserves the first extras
430ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // (when flag is not set), or update them when PendingIntent#getActivity() is called
431ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // (See PendingIntent#FLAG_UPDATE_CURRENT). In either case, we cannot preserve extras as we
432ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // expect (for each vCard import/export request).
433ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        //
434ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // We use query parameter in Uri instead.
435ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        // Scheme and Authority is arbitorary, assuming CancelActivity never refers them.
436ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final Intent intent = new Intent(context, CancelActivity.class);
437ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        final Uri uri = (new Uri.Builder())
438ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                .scheme("invalidscheme")
439ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                .authority("invalidauthority")
440ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                .appendQueryParameter(CancelActivity.JOB_ID, String.valueOf(jobId))
441ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                .appendQueryParameter(CancelActivity.DISPLAY_NAME, displayName)
442ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa                .appendQueryParameter(CancelActivity.TYPE, String.valueOf(type)).build();
443ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        intent.setData(uri);
444ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
445ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        notification.contentIntent = PendingIntent.getActivity(context, 0, intent, 0);
446ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa        return notification;
447ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    }
448ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
449ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /**
450ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Constructs a Notification telling users the process is canceled.
451ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     *
452ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param context
453ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param description Content of the Notification
454ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     */
455ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static Notification constructCancelNotification(
456ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa            Context context, String description) {
457783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        return new Notification.Builder(context)
458783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .setAutoCancel(true)
459783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .setSmallIcon(android.R.drawable.stat_notify_error)
460783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .setContentTitle(description)
461783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .setContentText(description)
462783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0))
463783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .getNotification();
464ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    }
465ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa
466ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /**
467ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * Constructs a Notification telling users the process is finished.
468ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     *
469ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param context
470ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param description Content of the Notification
471ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     * @param intent Intent to be launched when the Notification is clicked. Can be null.
472ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa     */
473ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    /* package */ static Notification constructFinishNotification(
474783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            Context context, String title, String description, Intent intent) {
475783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        return new Notification.Builder(context)
476783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .setAutoCancel(true)
477783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .setSmallIcon(android.R.drawable.stat_sys_download_done)
478783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .setContentTitle(title)
479783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .setContentText(description)
480783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0))
481783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                .getNotification();
482783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    }
483783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
484783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    /**
485783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa     * Returns an appropriate file name for vCard export. Returns null when impossible.
486783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa     *
487783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa     * @return destination path for a vCard file to be exported. null on error and mErrorReason
488783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa     * is correctly set.
489783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa     */
490783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa    private String getAppropriateDestination(final String destDirectory) {
491783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        /*
492783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         * Here, file names have 5 parts: directory, prefix, index, suffix, and extension.
493783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         * e.g. "/mnt/sdcard/prfx00001sfx.vcf" -> "/mnt/sdcard", "prfx", "00001", "sfx", and ".vcf"
494783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         *      (In default, prefix and suffix is empty, so usually the destination would be
495783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         *       /mnt/sdcard/00001.vcf.)
496783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         *
497783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         * This method increments "index" part from 1 to maximum, and checks whether any file name
498783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         * following naming rule is available. If there's no file named /mnt/sdcard/00001.vcf, the
499783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         * name will be returned to a caller. If there are 00001.vcf 00002.vcf, 00003.vcf is
500783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         * returned.
501783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         *
502783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         * There may not be any appropriate file name. If there are 99999 vCard files in the
503783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         * storage, for example, there's no appropriate name, so this method returns
504783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         * null.
505783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa         */
506783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
507783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        // Count the number of digits of mFileIndexMaximum
508783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        // e.g. When mFileIndexMaximum is 99999, fileIndexDigit becomes 5, as we will count the
509783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        int fileIndexDigit = 0;
510783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        {
511783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            // Calling Math.Log10() is costly.
512783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            int tmp;
513783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            for (fileIndexDigit = 0, tmp = mFileIndexMaximum; tmp > 0;
514783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                fileIndexDigit++, tmp /= 10) {
515783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            }
516783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        }
517783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
518783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        // %s05d%s (e.g. "p00001s")
519783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        final String bodyFormat = "%s%0" + fileIndexDigit + "d%s";
520783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
521783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        if (!ALLOW_LONG_FILE_NAME) {
522783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            final String possibleBody =
523783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    String.format(bodyFormat, mFileNamePrefix, 1, mFileNameSuffix);
524783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
525783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                Log.e(LOG_TAG, "This code does not allow any long file name.");
526783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                mErrorReason = getString(R.string.fail_reason_too_long_filename,
527783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                        String.format("%s.%s", possibleBody, mFileNameExtension));
528783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                Log.w(LOG_TAG, "File name becomes too long.");
529783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                return null;
530783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            }
531783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        }
532783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
533783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
534783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            boolean numberIsAvailable = true;
535783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            String body = null;
536783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            for (String possibleExtension : mExtensionsToConsider) {
537783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
538783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                final String path =
539783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                        String.format("%s/%s.%s", destDirectory, body, possibleExtension);
540783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                synchronized (this) {
541783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    if (mReservedDestination.contains(path)) {
542783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                        if (DEBUG) {
543783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                            Log.d(LOG_TAG, String.format("The path %s is reserved.", path));
544783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                        }
545783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                        numberIsAvailable = false;
546783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                        break;
547783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    }
548783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                }
549783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                final File file = new File(path);
550783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                if (file.exists()) {
551783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    numberIsAvailable = false;
552783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                    break;
553783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                }
554783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            }
555783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            if (numberIsAvailable) {
556783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa                return String.format("%s/%s.%s", destDirectory, body, mFileNameExtension);
557783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa            }
558783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        }
559783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa
560783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        Log.w(LOG_TAG, "Reached vCard number limit. Maybe there are too many vCard in the storage");
561783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        mErrorReason = getString(R.string.fail_reason_too_many_vcard);
562783a09a8770f4322a45cee456adefbbc71218eceDaisuke Miyakawa        return null;
563ab59660a17e896593f2a07c2e1191c2c23e3e353Daisuke Miyakawa    }
564f21bf27c13dacec9b4ed74cba9046a64948e97fbDaisuke Miyakawa}
565