From 8fd0ceee4723f2fa5a8b5e3ed73f6ece038d085e Mon Sep 17 00:00:00 2001 From: M66B Date: Tue, 28 Sep 2021 14:03:58 +0200 Subject: [PATCH] Added importing vCards into local contact database --- ATTRIBUTION.md | 1 + CHANGELOG.md | 5 + app/build.gradle | 4 + app/src/main/assets/ATTRIBUTION.md | 1 + app/src/main/assets/CHANGELOG.md | 5 + .../eu/faircode/email/FragmentContacts.java | 142 +++++++++++++++++- app/src/main/res/menu/menu_contacts.xml | 17 ++- app/src/main/res/values/strings.xml | 1 + metadata/en-US/changelogs/1739.txt | 5 + 9 files changed, 172 insertions(+), 9 deletions(-) diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index ce4d598ee5..0e4abe7f12 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -35,3 +35,4 @@ FairEmail uses: * [AndroidSVG](https://github.com/BigBadaboom/androidsvg). Copyright 2013 Paul LeBeau, Cave Rock Software Ltd. [Apache License 2.0](https://github.com/BigBadaboom/androidsvg/blob/master/LICENSE). * [Public Suffix List](https://publicsuffix.org/). Copyright © 2007–20 Mozilla Foundation. [Mozilla Public License, v. 2.0](https://mozilla.org/MPL/2.0/). * [Outlook Message Parser](https://github.com/bbottema/outlook-message-parser). Copyright (C) 2017 Benny Bottema. [Apache License 2.0](https://github.com/bbottema/outlook-message-parser/blob/master/LICENSE-2.0.txt). +* [ez-vcard](https://github.com/mangstadt/ez-vcard). Copyright (c) 2012-2021, Michael Angstadt. All rights reserved. [FreeBSD License](https://github.com/mangstadt/ez-vcard/blob/master/LICENSE). diff --git a/CHANGELOG.md b/CHANGELOG.md index 04abf0ed5f..f76515ebc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ### [Zanabazar](https://en.wikipedia.org/wiki/Zanabazar_junior) +### Next version + +* Added importing of vCards into local contact database +* Reduced memory usage + ### 1.1739 * Showing search index state diff --git a/app/build.gradle b/app/build.gradle index 0d83ce391c..6a245c391c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -326,6 +326,7 @@ dependencies { def badge_version = "1.1.22" def bugsnag_version = "5.12.0" def biweekly_version = "0.6.6" + def vcard_version = "0.11.3" def relinker_version = "1.4.3" def markwon_version = "4.6.2" def bouncycastle_version = "1.69" @@ -484,6 +485,9 @@ dependencies { exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' } + // https://github.com/mangstadt/ez-vcard + implementation "com.googlecode.ez-vcard:ez-vcard:$vcard_version" + // https://github.com/KeepSafe/ReLinker // https://mvnrepository.com/artifact/com.getkeepsafe.relinker/relinker implementation "com.getkeepsafe.relinker:relinker:$relinker_version" diff --git a/app/src/main/assets/ATTRIBUTION.md b/app/src/main/assets/ATTRIBUTION.md index ce4d598ee5..0e4abe7f12 100644 --- a/app/src/main/assets/ATTRIBUTION.md +++ b/app/src/main/assets/ATTRIBUTION.md @@ -35,3 +35,4 @@ FairEmail uses: * [AndroidSVG](https://github.com/BigBadaboom/androidsvg). Copyright 2013 Paul LeBeau, Cave Rock Software Ltd. [Apache License 2.0](https://github.com/BigBadaboom/androidsvg/blob/master/LICENSE). * [Public Suffix List](https://publicsuffix.org/). Copyright © 2007–20 Mozilla Foundation. [Mozilla Public License, v. 2.0](https://mozilla.org/MPL/2.0/). * [Outlook Message Parser](https://github.com/bbottema/outlook-message-parser). Copyright (C) 2017 Benny Bottema. [Apache License 2.0](https://github.com/bbottema/outlook-message-parser/blob/master/LICENSE-2.0.txt). +* [ez-vcard](https://github.com/mangstadt/ez-vcard). Copyright (c) 2012-2021, Michael Angstadt. All rights reserved. [FreeBSD License](https://github.com/mangstadt/ez-vcard/blob/master/LICENSE). diff --git a/app/src/main/assets/CHANGELOG.md b/app/src/main/assets/CHANGELOG.md index 04abf0ed5f..f76515ebc6 100644 --- a/app/src/main/assets/CHANGELOG.md +++ b/app/src/main/assets/CHANGELOG.md @@ -4,6 +4,11 @@ ### [Zanabazar](https://en.wikipedia.org/wiki/Zanabazar_junior) +### Next version + +* Added importing of vCards into local contact database +* Reduced memory usage + ### 1.1739 * Showing search index state diff --git a/app/src/main/java/eu/faircode/email/FragmentContacts.java b/app/src/main/java/eu/faircode/email/FragmentContacts.java index 2f3d63d17e..b0f8cff82e 100644 --- a/app/src/main/java/eu/faircode/email/FragmentContacts.java +++ b/app/src/main/java/eu/faircode/email/FragmentContacts.java @@ -19,9 +19,16 @@ package eu.faircode.email; Copyright 2018-2021 by Marcel Bokhorst (M66B) */ +import static android.app.Activity.RESULT_OK; + +import android.Manifest; import android.app.Dialog; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; @@ -33,6 +40,7 @@ import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageButton; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -43,10 +51,24 @@ import androidx.lifecycle.Observer; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import java.io.BufferedInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.List; +import javax.mail.Address; +import javax.mail.internet.InternetAddress; + +import ezvcard.Ezvcard; +import ezvcard.VCard; +import ezvcard.io.text.VCardReader; +import ezvcard.property.Email; +import ezvcard.property.FormattedName; + public class FragmentContacts extends FragmentBase { private RecyclerView rvContacts; private ContentLoadingProgressBar pbWait; @@ -56,6 +78,8 @@ public class FragmentContacts extends FragmentBase { private String searching = null; private AdapterContact adapter; + static final int REQUEST_IMPORT = 1; + @Override @Nullable public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -166,13 +190,16 @@ public class FragmentContacts extends FragmentBase { if (itemId == R.id.menu_help) { onMenuHelp(); return true; - } else if (itemId == R.id.menu_delete) { - new FragmentDelete().show(getParentFragmentManager(), "contacts:delete"); - return true; } else if (itemId == R.id.menu_junk) { item.setChecked(!item.isChecked()); onMenuJunk(item.isChecked()); return true; + } else if (itemId == R.id.menu_import) { + onMenuImport(); + return true; + } else if (itemId == R.id.menu_delete) { + onMenuDelete(); + return true; } return super.onOptionsItemSelected(item); } @@ -188,6 +215,115 @@ public class FragmentContacts extends FragmentBase { : new ArrayList<>()); } + private void onMenuImport() { + final Context context = getContext(); + PackageManager pm = context.getPackageManager(); + + Intent open = new Intent(Intent.ACTION_GET_CONTENT); + open.addCategory(Intent.CATEGORY_OPENABLE); + open.setType("*/*"); + if (open.resolveActivity(pm) == null) // system whitelisted + ToastEx.makeText(context, R.string.title_no_saf, Toast.LENGTH_LONG).show(); + else + startActivityForResult(Helper.getChooser(context, open), REQUEST_IMPORT); + } + + private void onMenuDelete() { + new FragmentDelete().show(getParentFragmentManager(), "contacts:delete"); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + try { + switch (requestCode) { + case REQUEST_IMPORT: + if (resultCode == RESULT_OK && data != null) + handleImport(data); + break; + } + } catch (Throwable ex) { + Log.e(ex); + } + } + + private void handleImport(Intent data) { + Uri uri = data.getData(); + + Bundle args = new Bundle(); + args.putParcelable("uri", uri); + + new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + ToastEx.makeText(getContext(), R.string.title_executing, Toast.LENGTH_LONG).show(); + } + + @Override + protected Void onExecute(Context context, Bundle args) throws Throwable { + Uri uri = args.getParcelable("uri"); + + if (uri == null) + throw new FileNotFoundException(); + + if (!"content".equals(uri.getScheme()) && + !Helper.hasPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE)) { + Log.w("Import uri=" + uri); + throw new IllegalArgumentException(context.getString(R.string.title_no_stream)); + } + + long now = new Date().getTime(); + DB db = DB.getInstance(context); + List accounts = db.account().getSynchronizingAccounts(); + + Log.i("Reading URI=" + uri); + ContentResolver resolver = context.getContentResolver(); + try (InputStream is = new BufferedInputStream(resolver.openInputStream(uri))) { + VCardReader reader = new VCardReader(is); + VCard vcard; + while ((vcard = reader.readNext()) != null) { + List emails = vcard.getEmails(); + if (emails == null) + continue; + + FormattedName fn = vcard.getFormattedName(); + String name = (fn == null) ? null : fn.getValue(); + + List
addresses = new ArrayList<>(); + for (Email email : emails) { + String address = email.getValue(); + if (address == null) + continue; + addresses.add(new InternetAddress(address, name, StandardCharsets.UTF_8.name())); + } + + for (EntityAccount account : accounts) + EntityContact.update(context, + account.id, + addresses.toArray(new Address[0]), + EntityContact.TYPE_TO, + now); + } + } + + Log.i("Imported contacts"); + + return null; + } + + @Override + protected void onExecuted(Bundle args, Void data) { + ToastEx.makeText(getContext(), R.string.title_completed, Toast.LENGTH_LONG).show(); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragmentManager(), ex); + } + }.execute(this, args, "setup:import"); + } + public static class FragmentDelete extends FragmentDialogBase { @NonNull @Override diff --git a/app/src/main/res/menu/menu_contacts.xml b/app/src/main/res/menu/menu_contacts.xml index 872e1aafc9..699e8cf0ae 100644 --- a/app/src/main/res/menu/menu_contacts.xml +++ b/app/src/main/res/menu/menu_contacts.xml @@ -14,15 +14,20 @@ android:title="@string/title_setup_help" app:showAsAction="always" /> - - + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4d00e2fe4b..522367e013 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -902,6 +902,7 @@ Delete notification channel Add contact Edit contact + Import vCards Create sub folder Delete all trashed messages permanently? diff --git a/metadata/en-US/changelogs/1739.txt b/metadata/en-US/changelogs/1739.txt index 04abf0ed5f..f76515ebc6 100644 --- a/metadata/en-US/changelogs/1739.txt +++ b/metadata/en-US/changelogs/1739.txt @@ -4,6 +4,11 @@ ### [Zanabazar](https://en.wikipedia.org/wiki/Zanabazar_junior) +### Next version + +* Added importing of vCards into local contact database +* Reduced memory usage + ### 1.1739 * Showing search index state