1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.contacts.vcard;
17
18import android.accounts.Account;
19import android.content.ContentResolver;
20import android.net.Uri;
21import android.util.Log;
22
23import com.android.vcard.VCardEntry;
24import com.android.vcard.VCardEntryCommitter;
25import com.android.vcard.VCardEntryConstructor;
26import com.android.vcard.VCardEntryHandler;
27import com.android.vcard.VCardInterpreter;
28import com.android.vcard.VCardParser;
29import com.android.vcard.VCardParser_V21;
30import com.android.vcard.VCardParser_V30;
31import com.android.vcard.exception.VCardException;
32import com.android.vcard.exception.VCardNestedException;
33import com.android.vcard.exception.VCardNotSupportedException;
34import com.android.vcard.exception.VCardVersionException;
35
36import java.io.ByteArrayInputStream;
37import java.io.IOException;
38import java.io.InputStream;
39import java.util.ArrayList;
40import java.util.List;
41
42/**
43 * Class for processing one import request from a user. Dropped after importing requested Uri(s).
44 * {@link VCardService} will create another object when there is another import request.
45 */
46public class ImportProcessor extends ProcessorBase implements VCardEntryHandler {
47    private static final String LOG_TAG = "VCardImport";
48    private static final boolean DEBUG = VCardService.DEBUG;
49
50    private final VCardService mService;
51    private final ContentResolver mResolver;
52    private final ImportRequest mImportRequest;
53    private final int mJobId;
54    private final VCardImportExportListener mListener;
55
56    // TODO: remove and show appropriate message instead.
57    private final List<Uri> mFailedUris = new ArrayList<Uri>();
58
59    private VCardParser mVCardParser;
60
61    private volatile boolean mCanceled;
62    private volatile boolean mDone;
63
64    private int mCurrentCount = 0;
65    private int mTotalCount = 0;
66
67    public ImportProcessor(final VCardService service, final VCardImportExportListener listener,
68            final ImportRequest request, final int jobId) {
69        mService = service;
70        mResolver = mService.getContentResolver();
71        mListener = listener;
72
73        mImportRequest = request;
74        mJobId = jobId;
75    }
76
77    @Override
78    public void onStart() {
79        // do nothing
80    }
81
82    @Override
83    public void onEnd() {
84        // do nothing
85    }
86
87    @Override
88    public void onEntryCreated(VCardEntry entry) {
89        mCurrentCount++;
90        if (mListener != null) {
91            mListener.onImportParsed(mImportRequest, mJobId, entry, mCurrentCount, mTotalCount);
92        }
93    }
94
95    @Override
96    public final int getType() {
97        return VCardService.TYPE_IMPORT;
98    }
99
100    @Override
101    public void run() {
102        // ExecutorService ignores RuntimeException, so we need to show it here.
103        try {
104            runInternal();
105
106            if (isCancelled() && mListener != null) {
107                mListener.onImportCanceled(mImportRequest, mJobId);
108            }
109        } catch (OutOfMemoryError e) {
110            Log.e(LOG_TAG, "OutOfMemoryError thrown during import", e);
111            throw e;
112        } catch (RuntimeException e) {
113            Log.e(LOG_TAG, "RuntimeException thrown during import", e);
114            throw e;
115        } finally {
116            synchronized (this) {
117                mDone = true;
118            }
119        }
120    }
121
122    private void runInternal() {
123        Log.i(LOG_TAG, String.format("vCard import (id: %d) has started.", mJobId));
124        final ImportRequest request = mImportRequest;
125        if (isCancelled()) {
126            Log.i(LOG_TAG, "Canceled before actually handling parameter (" + request.uri + ")");
127            return;
128        }
129        final int[] possibleVCardVersions;
130        if (request.vcardVersion == ImportVCardActivity.VCARD_VERSION_AUTO_DETECT) {
131            /**
132             * Note: this code assumes that a given Uri is able to be opened more than once,
133             * which may not be true in certain conditions.
134             */
135            possibleVCardVersions = new int[] {
136                    ImportVCardActivity.VCARD_VERSION_V21,
137                    ImportVCardActivity.VCARD_VERSION_V30
138            };
139        } else {
140            possibleVCardVersions = new int[] {
141                    request.vcardVersion
142            };
143        }
144
145        final Uri uri = request.uri;
146        final Account account = request.account;
147        final int estimatedVCardType = request.estimatedVCardType;
148        final String estimatedCharset = request.estimatedCharset;
149        final int entryCount = request.entryCount;
150        mTotalCount += entryCount;
151
152        final VCardEntryConstructor constructor =
153                new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
154        final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
155        constructor.addEntryHandler(committer);
156        constructor.addEntryHandler(this);
157
158        InputStream is = null;
159        boolean successful = false;
160        try {
161            if (uri != null) {
162                Log.i(LOG_TAG, "start importing one vCard (Uri: " + uri + ")");
163                is = mResolver.openInputStream(uri);
164            } else if (request.data != null){
165                Log.i(LOG_TAG, "start importing one vCard (byte[])");
166                is = new ByteArrayInputStream(request.data);
167            }
168
169            if (is != null) {
170                successful = readOneVCard(is, estimatedVCardType, estimatedCharset, constructor,
171                        possibleVCardVersions);
172            }
173        } catch (IOException e) {
174            successful = false;
175        } finally {
176            if (is != null) {
177                try {
178                    is.close();
179                } catch (Exception e) {
180                    // ignore
181                }
182            }
183        }
184
185        mService.handleFinishImportNotification(mJobId, successful);
186
187        if (successful) {
188            // TODO: successful becomes true even when cancelled. Should return more appropriate
189            // value
190            if (isCancelled()) {
191                Log.i(LOG_TAG, "vCard import has been canceled (uri: " + uri + ")");
192                // Cancel notification will be done outside this method.
193            } else {
194                Log.i(LOG_TAG, "Successfully finished importing one vCard file: " + uri);
195                List<Uri> uris = committer.getCreatedUris();
196                if (mListener != null) {
197                    if (uris != null && uris.size() > 0) {
198                        // TODO: construct intent showing a list of imported contact list.
199                        mListener.onImportFinished(mImportRequest, mJobId, uris.get(0));
200                    } else {
201                        // Not critical, but suspicious.
202                        Log.w(LOG_TAG,
203                                "Created Uris is null or 0 length " +
204                                "though the creation itself is successful.");
205                        mListener.onImportFinished(mImportRequest, mJobId, null);
206                    }
207                }
208            }
209        } else {
210            Log.w(LOG_TAG, "Failed to read one vCard file: " + uri);
211            mFailedUris.add(uri);
212        }
213    }
214
215    private boolean readOneVCard(InputStream is, int vcardType, String charset,
216            final VCardInterpreter interpreter,
217            final int[] possibleVCardVersions) {
218        boolean successful = false;
219        final int length = possibleVCardVersions.length;
220        for (int i = 0; i < length; i++) {
221            final int vcardVersion = possibleVCardVersions[i];
222            try {
223                if (i > 0 && (interpreter instanceof VCardEntryConstructor)) {
224                    // Let the object clean up internal temporary objects,
225                    ((VCardEntryConstructor) interpreter).clear();
226                }
227
228                // We need synchronized block here,
229                // since we need to handle mCanceled and mVCardParser at once.
230                // In the worst case, a user may call cancel() just before creating
231                // mVCardParser.
232                synchronized (this) {
233                    mVCardParser = (vcardVersion == ImportVCardActivity.VCARD_VERSION_V30 ?
234                            new VCardParser_V30(vcardType) :
235                                new VCardParser_V21(vcardType));
236                    if (isCancelled()) {
237                        Log.i(LOG_TAG, "ImportProcessor already recieves cancel request, so " +
238                                "send cancel request to vCard parser too.");
239                        mVCardParser.cancel();
240                    }
241                }
242                mVCardParser.parse(is, interpreter);
243
244                successful = true;
245                break;
246            } catch (IOException e) {
247                Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
248            } catch (VCardNestedException e) {
249                // This exception should not be thrown here. We should instead handle it
250                // in the preprocessing session in ImportVCardActivity, as we don't try
251                // to detect the type of given vCard here.
252                //
253                // TODO: Handle this case appropriately, which should mean we have to have
254                // code trying to auto-detect the type of given vCard twice (both in
255                // ImportVCardActivity and ImportVCardService).
256                Log.e(LOG_TAG, "Nested Exception is found.");
257            } catch (VCardNotSupportedException e) {
258                Log.e(LOG_TAG, e.toString());
259            } catch (VCardVersionException e) {
260                if (i == length - 1) {
261                    Log.e(LOG_TAG, "Appropriate version for this vCard is not found.");
262                } else {
263                    // We'll try the other (v30) version.
264                }
265            } catch (VCardException e) {
266                Log.e(LOG_TAG, e.toString());
267            } finally {
268                if (is != null) {
269                    try {
270                        is.close();
271                    } catch (IOException e) {
272                    }
273                }
274            }
275        }
276
277        return successful;
278    }
279
280    @Override
281    public synchronized boolean cancel(boolean mayInterruptIfRunning) {
282        if (DEBUG) Log.d(LOG_TAG, "ImportProcessor received cancel request");
283        if (mDone || mCanceled) {
284            return false;
285        }
286        mCanceled = true;
287        synchronized (this) {
288            if (mVCardParser != null) {
289                mVCardParser.cancel();
290            }
291        }
292        return true;
293    }
294
295    @Override
296    public synchronized boolean isCancelled() {
297        return mCanceled;
298    }
299
300
301    @Override
302    public synchronized boolean isDone() {
303        return mDone;
304    }
305}
306