1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.contacts.vcard;
18
19import android.app.Activity;
20import android.app.Notification;
21import android.app.NotificationManager;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.ServiceConnection;
26import android.net.Uri;
27import android.nfc.NdefMessage;
28import android.nfc.NdefRecord;
29import android.nfc.NfcAdapter;
30import android.os.AsyncTask;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.IBinder;
34import android.provider.ContactsContract.RawContacts;
35import android.util.Log;
36import android.widget.Toast;
37
38import com.android.contacts.R;
39import com.android.contacts.activities.RequestPermissionsActivity;
40import com.android.contacts.model.AccountTypeManager;
41import com.android.contacts.model.account.AccountWithDataSet;
42import com.android.contacts.util.ImplicitIntentsUtil;
43import com.android.contactsbind.FeedbackHelper;
44import com.android.vcard.VCardEntry;
45import com.android.vcard.VCardEntryCounter;
46import com.android.vcard.VCardParser;
47import com.android.vcard.VCardParser_V21;
48import com.android.vcard.VCardParser_V30;
49import com.android.vcard.VCardSourceDetector;
50import com.android.vcard.exception.VCardException;
51import com.android.vcard.exception.VCardNestedException;
52import com.android.vcard.exception.VCardVersionException;
53
54import java.io.ByteArrayInputStream;
55import java.io.IOException;
56import java.util.ArrayList;
57import java.util.List;
58
59public class NfcImportVCardActivity extends Activity implements ServiceConnection,
60        VCardImportExportListener {
61    private static final String TAG = "NfcImportVCardActivity";
62
63    private static final int SELECT_ACCOUNT = 1;
64
65    private NdefRecord mRecord;
66    private AccountWithDataSet mAccount;
67    private Handler mHandler = new Handler();
68
69    /**
70     * Notification id used when error happened before sending an import request to VCardServer.
71     */
72    private static final int FAILURE_NOTIFICATION_ID = 1;
73
74    /* package */ class ImportTask extends AsyncTask<VCardService, Void, ImportRequest> {
75        @Override
76        public ImportRequest doInBackground(VCardService... services) {
77            ImportRequest request = createImportRequest();
78            if (request == null) {
79                return null;
80            }
81
82            ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
83            requests.add(request);
84            services[0].handleImportRequest(requests, NfcImportVCardActivity.this);
85            return request;
86        }
87
88        @Override
89        public void onCancelled() {
90            unbindService(NfcImportVCardActivity.this);
91        }
92
93        @Override
94        public void onPostExecute(ImportRequest request) {
95            if (request == null) {
96                // Finish the activity in case of error so it doesn't stay in view.
97                finish();
98            }
99            unbindService(NfcImportVCardActivity.this);
100        }
101    }
102
103    /* package */ ImportRequest createImportRequest() {
104        VCardParser parser;
105        VCardEntryCounter counter = null;
106        VCardSourceDetector detector = null;
107        int vcardVersion = ImportVCardActivity.VCARD_VERSION_V21;
108        try {
109            ByteArrayInputStream is = new ByteArrayInputStream(mRecord.getPayload());
110            is.mark(0);
111            parser = new VCardParser_V21();
112            try {
113                counter = new VCardEntryCounter();
114                detector = new VCardSourceDetector();
115                parser.addInterpreter(counter);
116                parser.addInterpreter(detector);
117                parser.parse(is);
118            } catch (VCardVersionException e1) {
119                is.reset();
120                vcardVersion = ImportVCardActivity.VCARD_VERSION_V30;
121                parser = new VCardParser_V30();
122                try {
123                    counter = new VCardEntryCounter();
124                    detector = new VCardSourceDetector();
125                    parser.addInterpreter(counter);
126                    parser.addInterpreter(detector);
127                    parser.parse(is);
128                } catch (VCardVersionException e2) {
129                    FeedbackHelper.sendFeedback(this, TAG, "vcard with unsupported version", e2);
130                    showFailureNotification(R.string.fail_reason_not_supported);
131                    return null;
132                }
133            } finally {
134                try {
135                    if (is != null) is.close();
136                } catch (IOException e) {
137                }
138            }
139        } catch (IOException e) {
140            FeedbackHelper.sendFeedback(this, TAG, "Failed to read vcard data", e);
141            showFailureNotification(R.string.fail_reason_io_error);
142            return null;
143        } catch (VCardNestedException e) {
144            Log.w(TAG, "Nested Exception is found (it may be false-positive).");
145            // Go through without throwing the Exception, as we may be able to detect the
146            // version before it
147        } catch (VCardException e) {
148            FeedbackHelper.sendFeedback(this, TAG, "Failed to parse vcard", e);
149            showFailureNotification(R.string.fail_reason_not_supported);
150            return null;
151        }
152
153        return new ImportRequest(mAccount, mRecord.getPayload(), null,
154                getString(R.string.nfc_vcard_file_name), detector.getEstimatedType(),
155                detector.getEstimatedCharset(), vcardVersion, counter.getCount());
156    }
157
158    @Override
159    public void onServiceConnected(ComponentName name, IBinder binder) {
160        VCardService service = ((VCardService.MyBinder) binder).getService();
161        new ImportTask().execute(service);
162    }
163
164    @Override
165    public void onServiceDisconnected(ComponentName name) {
166        // Do nothing
167    }
168
169    @Override
170    protected void onCreate(Bundle bundle) {
171        super.onCreate(bundle);
172
173        if (RequestPermissionsActivity.startPermissionActivityIfNeeded(this)) {
174            return;
175        }
176
177        Intent intent = getIntent();
178        if (!NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
179            Log.w(TAG, "Unknowon intent " + intent);
180            finish();
181            return;
182        }
183
184        String type = intent.getType();
185        if (type == null ||
186                (!"text/x-vcard".equals(type) && !"text/vcard".equals(type))) {
187            Log.w(TAG, "Not a vcard");
188            //setStatus(getString(R.string.fail_reason_not_supported));
189            finish();
190            return;
191        }
192        NdefMessage msg = (NdefMessage) intent.getParcelableArrayExtra(
193                NfcAdapter.EXTRA_NDEF_MESSAGES)[0];
194        mRecord = msg.getRecords()[0];
195
196        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
197        final List<AccountWithDataSet> accountList = accountTypes.blockForWritableAccounts();
198        if (accountList.size() == 0) {
199            mAccount = null;
200        } else if (accountList.size() == 1) {
201            mAccount = accountList.get(0);
202        } else {
203            startActivityForResult(new Intent(this, SelectAccountActivity.class), SELECT_ACCOUNT);
204            return;
205        }
206
207        startImport();
208    }
209
210    @Override
211    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
212        if (requestCode == SELECT_ACCOUNT) {
213            if (resultCode == RESULT_OK) {
214                mAccount = new AccountWithDataSet(
215                        intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
216                        intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
217                        intent.getStringExtra(SelectAccountActivity.DATA_SET));
218                startImport();
219            } else {
220                finish();
221            }
222        }
223    }
224
225    private void startImport() {
226        // We don't want the service finishes itself just after this connection.
227        Intent intent = new Intent(this, VCardService.class);
228        startService(intent);
229        bindService(intent, this, Context.BIND_AUTO_CREATE);
230    }
231
232    @Override
233    public Notification onImportProcessed(ImportRequest request, int jobId, int sequence) {
234        return null;
235    }
236
237    @Override
238    public Notification onImportParsed(ImportRequest request, int jobId, VCardEntry entry,
239            int currentCount, int totalCount) {
240        return null;
241    }
242
243    @Override
244    public void onImportFinished(ImportRequest request, int jobId, Uri uri) {
245        if (isFinishing()) {
246            Log.i(TAG, "Late import -- ignoring");
247            return;
248        }
249
250        if (uri != null) {
251            Uri contactUri = RawContacts.getContactLookupUri(getContentResolver(), uri);
252            Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
253            ImplicitIntentsUtil.startActivityInAppIfPossible(this, intent);
254            finish();
255        }
256    }
257
258    @Override
259    public void onImportFailed(ImportRequest request) {
260        if (isFinishing()) {
261            Log.i(TAG, "Late import failure -- ignoring");
262            return;
263        }
264        showFailureNotification(R.string.vcard_import_request_rejected_message);
265        finish();
266    }
267
268    @Override
269    public void onImportCanceled(ImportRequest request, int jobId) {
270        // do nothing
271    }
272
273    @Override
274    public Notification onExportProcessed(ExportRequest request, int jobId) {
275        return null;
276    }
277
278    @Override
279    public void onExportFailed(ExportRequest request) {
280        // do nothing
281    }
282
283    @Override
284    public void onCancelRequest(CancelRequest request, int type) {
285        // do nothing
286    }
287
288    /* package */ void showFailureNotification(int reasonId) {
289        final NotificationManager notificationManager =
290                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
291        final Notification notification =
292                NotificationImportExportListener.constructImportFailureNotification(
293                        this,
294                        getString(reasonId));
295        notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
296                FAILURE_NOTIFICATION_ID, notification);
297        mHandler.post(new Runnable() {
298            @Override
299            public void run() {
300                Toast.makeText(NfcImportVCardActivity.this,
301                        getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
302            }
303        });
304    }
305}
306