From f77063ff1a39a1103b4d624e05fe93654460899b Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sat, 5 Jan 2019 11:21:26 +0000 Subject: [PATCH] Kotlin: more kotlin migration. --- .../java/com/etesync/syncadapter/App.java | 2 +- .../syncadapter/NotificationHelper.java | 8 +- .../etesync/syncadapter/ui/AboutActivity.java | 228 --------- .../etesync/syncadapter/ui/AboutActivity.kt | 193 +++++++ .../syncadapter/ui/AccountActivity.java | 481 ------------------ .../etesync/syncadapter/ui/AccountActivity.kt | 410 +++++++++++++++ .../syncadapter/ui/AccountListFragment.java | 136 ----- .../syncadapter/ui/AccountListFragment.kt | 119 +++++ .../ui/AccountSettingsActivity.java | 234 --------- .../syncadapter/ui/AccountSettingsActivity.kt | 197 +++++++ .../syncadapter/ui/AccountsActivity.java | 169 ------ .../syncadapter/ui/AccountsActivity.kt | 138 +++++ .../syncadapter/ui/AddMemberFragment.java | 190 ------- .../syncadapter/ui/AddMemberFragment.kt | 146 ++++++ .../syncadapter/ui/AppSettingsActivity.java | 236 --------- .../syncadapter/ui/AppSettingsActivity.kt | 204 ++++++++ .../etesync/syncadapter/ui/BaseActivity.java | 37 -- .../etesync/syncadapter/ui/BaseActivity.kt | 34 ++ .../ui/CollectionMembersActivity.java | 127 ----- .../ui/CollectionMembersActivity.kt | 113 ++++ .../ui/CollectionMembersListFragment.java | 174 ------- .../ui/CollectionMembersListFragment.kt | 139 +++++ .../ui/CreateCollectionActivity.java | 129 ----- .../ui/CreateCollectionActivity.kt | 123 +++++ .../ui/CreateCollectionFragment.java | 183 ------- .../ui/CreateCollectionFragment.kt | 160 ++++++ .../syncadapter/ui/DebugInfoActivity.java | 278 ---------- .../syncadapter/ui/DebugInfoActivity.kt | 242 +++++++++ .../ui/DeleteCollectionFragment.java | 178 ------- .../ui/DeleteCollectionFragment.kt | 150 ++++++ .../ui/EditCollectionActivity.java | 90 ---- .../syncadapter/ui/EditCollectionActivity.kt | 86 ++++ .../syncadapter/ui/ExceptionInfoFragment.java | 76 --- .../syncadapter/ui/ExceptionInfoFragment.kt | 65 +++ .../syncadapter/ui/JournalItemActivity.java | 475 ----------------- .../syncadapter/ui/JournalItemActivity.kt | 424 +++++++++++++++ .../syncadapter/ui/PermissionsActivity.java | 109 ---- .../syncadapter/ui/PermissionsActivity.kt | 86 ++++ .../etesync/syncadapter/ui/Refreshable.java | 5 - .../com/etesync/syncadapter/ui/Refreshable.kt | 5 + .../syncadapter/ui/RemoveMemberFragment.java | 116 ----- .../syncadapter/ui/RemoveMemberFragment.kt | 96 ++++ .../syncadapter/ui/StartupDialogFragment.java | 166 ------ .../syncadapter/ui/StartupDialogFragment.kt | 123 +++++ .../ui/ViewCollectionActivity.java | 261 ---------- .../syncadapter/ui/ViewCollectionActivity.kt | 240 +++++++++ .../syncadapter/ui/WebViewActivity.java | 212 -------- .../etesync/syncadapter/ui/WebViewActivity.kt | 189 +++++++ .../ui/importlocal/AccountResolver.java | 60 --- .../ui/importlocal/AccountResolver.kt | 48 ++ .../ui/importlocal/CalendarAccount.java | 128 ----- .../ui/importlocal/CalendarAccount.kt | 113 ++++ .../ui/importlocal/ImportActivity.java | 183 ------- .../ui/importlocal/ImportActivity.kt | 160 ++++++ .../ui/importlocal/ImportFragment.java | 332 ------------ .../ui/importlocal/ImportFragment.kt | 315 ++++++++++++ .../LocalCalendarImportFragment.java | 267 ---------- .../LocalCalendarImportFragment.kt | 241 +++++++++ .../LocalContactImportFragment.java | 297 ----------- .../importlocal/LocalContactImportFragment.kt | 274 ++++++++++ .../ui/importlocal/ResultFragment.java | 100 ---- .../ui/importlocal/ResultFragment.kt | 87 ++++ .../ui/importlocal/SelectImportMethod.java | 11 - .../ui/importlocal/SelectImportMethod.kt | 11 + .../ui/journalviewer/ListEntriesFragment.java | 187 ------- .../ui/journalviewer/ListEntriesFragment.kt | 162 ++++++ .../ui/setup/BaseConfigurationFinder.java | 136 ----- .../ui/setup/BaseConfigurationFinder.kt | 102 ++++ .../ui/setup/DetectConfigurationFragment.java | 145 ------ .../ui/setup/DetectConfigurationFragment.kt | 123 +++++ .../ui/setup/EncryptionDetailsFragment.java | 87 ---- .../ui/setup/EncryptionDetailsFragment.kt | 81 +++ .../syncadapter/ui/setup/LoginActivity.java | 57 --- .../syncadapter/ui/setup/LoginActivity.kt | 58 +++ .../ui/setup/LoginCredentials.java | 64 --- .../syncadapter/ui/setup/LoginCredentials.kt | 62 +++ .../setup/LoginCredentialsChangeFragment.java | 160 ------ .../setup/LoginCredentialsChangeFragment.kt | 138 +++++ .../ui/setup/LoginCredentialsFragment.java | 144 ------ .../ui/setup/LoginCredentialsFragment.kt | 120 +++++ .../ui/setup/SetupEncryptionFragment.java | 231 --------- .../ui/setup/SetupEncryptionFragment.kt | 204 ++++++++ .../ui/setup/SetupUserInfoFragment.java | 147 ------ .../ui/setup/SetupUserInfoFragment.kt | 125 +++++ .../syncadapter/ui/widget/EditPassword.java | 63 --- .../syncadapter/ui/widget/EditPassword.kt | 60 +++ .../ui/widget/MaximizedListView.java | 55 -- .../ui/widget/MaximizedListView.kt | 50 ++ 88 files changed, 6221 insertions(+), 7149 deletions(-) delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AccountsActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AccountsActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AppSettingsActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AppSettingsActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/BaseActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/BaseActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/ExceptionInfoFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/ExceptionInfoFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/Refreshable.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/Refreshable.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/StartupDialogFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/StartupDialogFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/WebViewActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/WebViewActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/AccountResolver.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/AccountResolver.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/EncryptionDetailsFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/EncryptionDetailsFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/LoginActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/LoginActivity.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsChangeFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsChangeFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/widget/EditPassword.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/widget/EditPassword.kt delete mode 100644 app/src/main/java/com/etesync/syncadapter/ui/widget/MaximizedListView.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/widget/MaximizedListView.kt diff --git a/app/src/main/java/com/etesync/syncadapter/App.java b/app/src/main/java/com/etesync/syncadapter/App.java index 5bee0ae5..e4233576 100644 --- a/app/src/main/java/com/etesync/syncadapter/App.java +++ b/app/src/main/java/com/etesync/syncadapter/App.java @@ -395,7 +395,7 @@ public class App extends Application { } if (fromVersion < 10) { - HintManager.setHintSeen(this, AccountsActivity.HINT_ACCOUNT_ADD, true); + HintManager.setHintSeen(this, AccountsActivity.Companion.getHINT_ACCOUNT_ADD(), true); } if (fromVersion < 11) { diff --git a/app/src/main/java/com/etesync/syncadapter/NotificationHelper.java b/app/src/main/java/com/etesync/syncadapter/NotificationHelper.java index 3b423170..32a91e1b 100644 --- a/app/src/main/java/com/etesync/syncadapter/NotificationHelper.java +++ b/app/src/main/java/com/etesync/syncadapter/NotificationHelper.java @@ -72,7 +72,7 @@ public class NotificationHelper { } detailsIntent = new Intent(context, NotificationHandlerActivity.class); - detailsIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e); + detailsIntent.putExtra(DebugInfoActivity.Companion.getKEY_THROWABLE(), e); detailsIntent.setData(Uri.parse("uri://" + getClass().getName() + "/" + notificationTag)); } @@ -127,16 +127,16 @@ public class NotificationHelper { public void onCreate(Bundle savedBundle) { super.onCreate(savedBundle); Bundle extras = getIntent().getExtras(); - Exception e = (Exception) extras.get(DebugInfoActivity.KEY_THROWABLE); + Exception e = (Exception) extras.get(DebugInfoActivity.Companion.getKEY_THROWABLE()); Intent detailsIntent; if (e instanceof Exceptions.UnauthorizedException) { detailsIntent = new Intent(this, AccountSettingsActivity.class); } else if (e instanceof Exceptions.UserInactiveException) { - WebViewActivity.openUrl(this, Constants.dashboard); + WebViewActivity.Companion.openUrl(this, Constants.dashboard); return; } else if (e instanceof AccountSettings.AccountMigrationException) { - WebViewActivity.openUrl(this, Constants.faqUri.buildUpon().encodedFragment("account-migration-error").build()); + WebViewActivity.Companion.openUrl(this, Constants.faqUri.buildUpon().encodedFragment("account-migration-error").build()); return; } else { detailsIntent = new Intent(this, DebugInfoActivity.class); diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.java deleted file mode 100644 index 09d19612..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TabLayout; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; -import android.support.v4.view.ViewPager; -import android.support.v7.widget.Toolbar; -import android.text.Html; -import android.text.Spanned; -import android.text.util.Linkify; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.BuildConfig; -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.time.DateFormatUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.util.logging.Level; - -import ezvcard.Ezvcard; - -public class AboutActivity extends BaseActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_about); - - setSupportActionBar((Toolbar)findViewById(R.id.toolbar)); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - ViewPager viewPager = (ViewPager)findViewById(R.id.viewpager); - viewPager.setAdapter(new TabsAdapter(getSupportFragmentManager())); - - TabLayout tabLayout = (TabLayout)findViewById(R.id.tabs); - tabLayout.setupWithViewPager(viewPager); - } - - private static class ComponentInfo { - final String title, version, website, copyright; - final int licenseInfo; - final String licenseTextFile; - - ComponentInfo(final String title, final String version, final String website, final String copyright, final int licenseInfo, final String licenseTextFile) { - this.title = title; - this.version = version; - this.website = website; - this.copyright = copyright; - this.licenseInfo = licenseInfo; - this.licenseTextFile = licenseTextFile; - } - } - - private final static ComponentInfo components[] = { - new ComponentInfo( - App.getAppName(), BuildConfig.VERSION_NAME, Constants.webUri.toString(), - DateFormatUtils.format(BuildConfig.buildTime, "yyyy") + " Tom Hacohen", - R.string.about_license_info_no_warranty, "gpl-3.0-standalone.html" - ), new ComponentInfo( - "DAVdroid", "(forked from)", "https://syncadapter.bitfire.at", - "Ricki Hirner, Bernhard Stockmann (bitfire web engineering)", - R.string.about_license_info_no_warranty, "gpl-3.0-standalone.html" - ), new ComponentInfo( - "AmbilWarna", null, "https://github.com/yukuku/ambilwarna", - "Yuku", R.string.about_license_info_no_warranty, "apache2.html" - ), new ComponentInfo( - "Apache Commons", null, "http://commons.apache.org/", - "Apache Software Foundation", R.string.about_license_info_no_warranty, "apache2.html" - ), new ComponentInfo( - "dnsjava", null, "http://dnsjava.org/", - "Brian Wellington", R.string.about_license_info_no_warranty, "bsd.html" - ), new ComponentInfo( - "ez-vcard", Ezvcard.VERSION, "https://github.com/mangstadt/ez-vcard", - "Michael Angstadt", R.string.about_license_info_no_warranty, "bsd.html" - ), new ComponentInfo( - "ical4j", "2.x", "https://ical4j.github.io/", - "Ben Fortuna", R.string.about_license_info_no_warranty, "bsd-3clause.html" - ), new ComponentInfo( - "OkHttp", null, "https://square.github.io/okhttp/", - "Square, Inc.", R.string.about_license_info_no_warranty, "apache2.html" - ), new ComponentInfo( - "Project Lombok", null, "https://projectlombok.org/", - "The Project Lombok Authors", R.string.about_license_info_no_warranty, "mit.html" - ) - }; - - - private static class TabsAdapter extends FragmentPagerAdapter { - public TabsAdapter(FragmentManager fm) { - super(fm); - } - - @Override - public int getCount() { - return components.length; - } - - @Override - public CharSequence getPageTitle(int position) { - return components[position].title; - } - - @Override - public Fragment getItem(int position) { - return ComponentFragment.instantiate(position); - } - } - - public static class ComponentFragment extends Fragment implements LoaderManager.LoaderCallbacks { - private static final String - KEY_POSITION = "position", - KEY_FILE_NAME = "fileName"; - - public static ComponentFragment instantiate(int position) { - ComponentFragment frag = new ComponentFragment(); - Bundle args = new Bundle(1); - args.putInt(KEY_POSITION, position); - frag.setArguments(args); - return frag; - } - - @Nullable - @Override - @SuppressLint("SetTextI18n") - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - ComponentInfo info = components[getArguments().getInt(KEY_POSITION)]; - - View v = inflater.inflate(R.layout.about_component, container, false); - - TextView tv = (TextView)v.findViewById(R.id.title); - tv.setText(info.title + (info.version != null ? (" " + info.version) : "")); - - tv = (TextView)v.findViewById(R.id.website); - tv.setAutoLinkMask(Linkify.WEB_URLS); - tv.setText(info.website); - - tv = (TextView)v.findViewById(R.id.copyright); - tv.setText("© " + info.copyright); - - tv = (TextView)v.findViewById(R.id.license_info); - tv.setText(info.licenseInfo); - - // load and format license text - Bundle args = new Bundle(1); - args.putString(KEY_FILE_NAME, info.licenseTextFile); - getLoaderManager().initLoader(0, args, this); - - return v; - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new LicenseLoader(getContext(), args.getString(KEY_FILE_NAME)); - } - - @Override - public void onLoadFinished(Loader loader, Spanned license) { - if (getView() != null) { - TextView tv = (TextView)getView().findViewById(R.id.license_text); - if (tv != null) { - tv.setAutoLinkMask(Linkify.EMAIL_ADDRESSES | Linkify.WEB_URLS); - tv.setText(license); - } - } - } - - @Override - public void onLoaderReset(Loader loader) { - } - } - - private static class LicenseLoader extends AsyncTaskLoader { - final String fileName; - Spanned content; - - LicenseLoader(Context context, String fileName) { - super(context); - this.fileName = fileName; - } - - @Override - protected void onStartLoading() { - if (content == null) - forceLoad(); - else - deliverResult(content); - } - - @Override - public Spanned loadInBackground() { - App.log.fine("Loading license file " + fileName); - try { - InputStream is = getContext().getResources().getAssets().open(fileName); - byte[] raw = IOUtils.toByteArray(is); - is.close(); - return content = Html.fromHtml(new String(raw)); - } catch (IOException e) { - App.log.log(Level.SEVERE, "Couldn't read license file", e); - return null; - } - } - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt new file mode 100644 index 00000000..d5c062d1 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt @@ -0,0 +1,193 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.support.design.widget.TabLayout +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentPagerAdapter +import android.support.v4.app.LoaderManager +import android.support.v4.content.AsyncTaskLoader +import android.support.v4.content.Loader +import android.support.v4.view.ViewPager +import android.support.v7.widget.Toolbar +import android.text.Html +import android.text.Spanned +import android.text.util.Linkify +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import com.etesync.syncadapter.App +import com.etesync.syncadapter.BuildConfig +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import ezvcard.Ezvcard +import org.apache.commons.io.IOUtils +import org.apache.commons.lang3.time.DateFormatUtils +import java.io.IOException +import java.util.logging.Level + +class AboutActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_about) + + setSupportActionBar(findViewById(R.id.toolbar) as Toolbar) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + val viewPager = findViewById(R.id.viewpager) as ViewPager + viewPager.adapter = TabsAdapter(supportFragmentManager) + + val tabLayout = findViewById(R.id.tabs) as TabLayout + tabLayout.setupWithViewPager(viewPager) + } + + private class ComponentInfo internal constructor(internal val title: String, internal val version: String?, internal val website: String, internal val copyright: String, internal val licenseInfo: Int, internal val licenseTextFile: String) + + + private class TabsAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) { + + override fun getCount(): Int { + return components.size + } + + override fun getPageTitle(position: Int): CharSequence? { + return components[position].title + } + + override fun getItem(position: Int): Fragment { + return ComponentFragment.instantiate(position) + } + } + + class ComponentFragment : Fragment(), LoaderManager.LoaderCallbacks { + + @SuppressLint("SetTextI18n") + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val info = components[arguments!!.getInt(KEY_POSITION)] + + val v = inflater.inflate(R.layout.about_component, container, false) + + var tv = v.findViewById(R.id.title) as TextView + tv.text = info.title + if (info.version != null) " " + info.version else "" + + tv = v.findViewById(R.id.website) as TextView + tv.autoLinkMask = Linkify.WEB_URLS + tv.text = info.website + + tv = v.findViewById(R.id.copyright) as TextView + tv.text = "© " + info.copyright + + tv = v.findViewById(R.id.license_info) as TextView + tv.setText(info.licenseInfo) + + // load and format license text + val args = Bundle(1) + args.putString(KEY_FILE_NAME, info.licenseTextFile) + loaderManager.initLoader(0, args, this) + + return v + } + + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + return LicenseLoader(context!!, args!!.getString(KEY_FILE_NAME)) + } + + override fun onLoadFinished(loader: Loader, license: Spanned) { + if (view != null) { + val tv = view!!.findViewById(R.id.license_text) as TextView + if (tv != null) { + tv.autoLinkMask = Linkify.EMAIL_ADDRESSES or Linkify.WEB_URLS + tv.text = license + } + } + } + + override fun onLoaderReset(loader: Loader) {} + + companion object { + private val KEY_POSITION = "position" + private val KEY_FILE_NAME = "fileName" + + fun instantiate(position: Int): ComponentFragment { + val frag = ComponentFragment() + val args = Bundle(1) + args.putInt(KEY_POSITION, position) + frag.arguments = args + return frag + } + } + } + + private class LicenseLoader internal constructor(context: Context, internal val fileName: String) : AsyncTaskLoader(context) { + internal var content: Spanned? = null + + override fun onStartLoading() { + if (content == null) + forceLoad() + else + deliverResult(content) + } + + override fun loadInBackground(): Spanned? { + App.log.fine("Loading license file $fileName") + try { + val `is` = context.resources.assets.open(fileName) + val raw = IOUtils.toByteArray(`is`) + `is`.close() + content = Html.fromHtml(String(raw)) + return content + } catch (e: IOException) { + App.log.log(Level.SEVERE, "Couldn't read license file", e) + return null + } + + } + } + + companion object { + + private val components = arrayOf(ComponentInfo( + App.getAppName(), BuildConfig.VERSION_NAME, Constants.webUri.toString(), + DateFormatUtils.format(BuildConfig.buildTime, "yyyy") + " Tom Hacohen", + R.string.about_license_info_no_warranty, "gpl-3.0-standalone.html" + ), ComponentInfo( + "DAVdroid", "(forked from)", "https://syncadapter.bitfire.at", + "Ricki Hirner, Bernhard Stockmann (bitfire web engineering)", + R.string.about_license_info_no_warranty, "gpl-3.0-standalone.html" + ), ComponentInfo( + "AmbilWarna", null, "https://github.com/yukuku/ambilwarna", + "Yuku", R.string.about_license_info_no_warranty, "apache2.html" + ), ComponentInfo( + "Apache Commons", null, "http://commons.apache.org/", + "Apache Software Foundation", R.string.about_license_info_no_warranty, "apache2.html" + ), ComponentInfo( + "dnsjava", null, "http://dnsjava.org/", + "Brian Wellington", R.string.about_license_info_no_warranty, "bsd.html" + ), ComponentInfo( + "ez-vcard", Ezvcard.VERSION, "https://github.com/mangstadt/ez-vcard", + "Michael Angstadt", R.string.about_license_info_no_warranty, "bsd.html" + ), ComponentInfo( + "ical4j", "2.x", "https://ical4j.github.io/", + "Ben Fortuna", R.string.about_license_info_no_warranty, "bsd-3clause.html" + ), ComponentInfo( + "OkHttp", null, "https://square.github.io/okhttp/", + "Square, Inc.", R.string.about_license_info_no_warranty, "apache2.html" + ), ComponentInfo( + "Project Lombok", null, "https://projectlombok.org/", + "The Project Lombok Authors", R.string.about_license_info_no_warranty, "mit.html" + )) + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java deleted file mode 100644 index aa5fb179..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java +++ /dev/null @@ -1,481 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.app.LoaderManager; -import android.content.AsyncTaskLoader; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.Loader; -import android.content.ServiceConnection; -import android.content.SyncStatusObserver; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.provider.CalendarContract; -import android.provider.ContactsContract; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.CardView; -import android.support.v7.widget.Toolbar; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.PopupMenu; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.AccountUpdateService; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.journalmanager.Crypto; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.ServiceEntity; -import com.etesync.syncadapter.resource.LocalAddressBook; -import com.etesync.syncadapter.resource.LocalCalendar; -import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment; -import com.etesync.syncadapter.utils.HintManager; -import com.etesync.syncadapter.utils.ShowcaseBuilder; - -import java.io.IOException; -import java.util.List; -import java.util.logging.Level; - -import at.bitfire.ical4android.TaskProvider; -import at.bitfire.vcard4android.ContactsStorageException; -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; -import tourguide.tourguide.ToolTip; - -import static android.content.ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE; - -public class AccountActivity extends BaseActivity implements Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, LoaderManager.LoaderCallbacks, Refreshable { - public static final String EXTRA_ACCOUNT = "account"; - private static final String HINT_VIEW_COLLECTION = "ViewCollection"; - - private Account account; - private AccountInfo accountInfo; - - ListView listCalDAV, listCardDAV; - Toolbar tbCardDAV, tbCalDAV; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - account = getIntent().getParcelableExtra(EXTRA_ACCOUNT); - setTitle(account.name); - - setContentView(R.layout.activity_account); - - Drawable icMenu = ContextCompat.getDrawable(this, R.drawable.ic_menu_light); - - // CardDAV toolbar - tbCardDAV = (Toolbar)findViewById(R.id.carddav_menu); - tbCardDAV.setOverflowIcon(icMenu); - tbCardDAV.inflateMenu(R.menu.carddav_actions); - tbCardDAV.setOnMenuItemClickListener(this); - tbCardDAV.setTitle(R.string.settings_carddav); - - // CalDAV toolbar - tbCalDAV = (Toolbar)findViewById(R.id.caldav_menu); - tbCalDAV.setOverflowIcon(icMenu); - tbCalDAV.inflateMenu(R.menu.caldav_actions); - tbCalDAV.setOnMenuItemClickListener(this); - tbCalDAV.setTitle(R.string.settings_caldav); - - // load CardDAV/CalDAV journals - getLoaderManager().initLoader(0, getIntent().getExtras(), this); - - if (!HintManager.getHintSeen(this, HINT_VIEW_COLLECTION)) { - ShowcaseBuilder.getBuilder(this) - .setToolTip(new ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.account_showcase_view_collection))) - .playOn(tbCardDAV); - HintManager.setHintSeen(this, HINT_VIEW_COLLECTION, true); - } - - if (!SetupUserInfoFragment.hasUserInfo(this, account)) { - SetupUserInfoFragment.newInstance(account).show(getSupportFragmentManager(), null); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.activity_account, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.sync_now: - requestSync(); - break; - case R.id.settings: - Intent intent = new Intent(this, AccountSettingsActivity.class); - intent.putExtra(Constants.KEY_ACCOUNT, account); - startActivity(intent); - break; - case R.id.delete_account: - new AlertDialog.Builder(AccountActivity.this) - .setIcon(R.drawable.ic_error_dark) - .setTitle(R.string.account_delete_confirmation_title) - .setMessage(R.string.account_delete_confirmation_text) - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - deleteAccount(); - } - }) - .show(); - break; - case R.id.show_fingerprint: - View view = getLayoutInflater().inflate(R.layout.fingerprint_alertdialog, null); - view.findViewById(R.id.body).setVisibility(View.GONE); - ((TextView) view.findViewById(R.id.fingerprint)).setText(getFormattedFingerprint()); - AlertDialog dialog = new AlertDialog.Builder(AccountActivity.this) - .setIcon(R.drawable.ic_fingerprint_dark) - .setTitle(R.string.show_fingperprint_title) - .setView(view) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }).create(); - dialog.show(); - break; - default: - return super.onOptionsItemSelected(item); - } - return true; - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - CollectionInfo info; - switch (item.getItemId()) { - case R.id.create_calendar: - info = new CollectionInfo(); - info.type = CollectionInfo.Type.CALENDAR; - startActivity(CreateCollectionActivity.newIntent(AccountActivity.this, account, info)); - break; - case R.id.create_addressbook: - info = new CollectionInfo(); - info.type = CollectionInfo.Type.ADDRESS_BOOK; - startActivity(CreateCollectionActivity.newIntent(AccountActivity.this, account, info)); - break; - } - return false; - } - - private AdapterView.OnItemClickListener onItemClickListener = new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - final ListView list = (ListView)parent; - final ArrayAdapter adapter = (ArrayAdapter)list.getAdapter(); - final JournalEntity journalEntity = adapter.getItem(position); - final CollectionInfo info = journalEntity.getInfo(); - - startActivity(ViewCollectionActivity.newIntent(AccountActivity.this, account, info)); - } - }; - - private String getFormattedFingerprint() { - AccountSettings settings = null; - try { - settings = new AccountSettings(this, account); - return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.getKeyPair().getPublicKey()); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - /* LOADERS AND LOADED DATA */ - - protected static class AccountInfo { - ServiceInfo carddav, caldav; - - public static class ServiceInfo { - long id; - boolean refreshing; - - List journals; - } - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new AccountLoader(this, account); - } - - @Override - public void refresh() { - getLoaderManager().restartLoader(0, getIntent().getExtras(), this); - } - - @Override - public void onLoadFinished(Loader loader, final AccountInfo info) { - accountInfo = info; - - CardView card = (CardView)findViewById(R.id.carddav); - if (info.carddav != null) { - ProgressBar progress = (ProgressBar)findViewById(R.id.carddav_refreshing); - progress.setVisibility(info.carddav.refreshing ? View.VISIBLE : View.GONE); - - listCardDAV = (ListView)findViewById(R.id.address_books); - listCardDAV.setEnabled(!info.carddav.refreshing); - listCardDAV.setAlpha(info.carddav.refreshing ? 0.5f : 1); - - final CollectionListAdapter adapter = new CollectionListAdapter(this, account); - adapter.addAll(info.carddav.journals); - listCardDAV.setAdapter(adapter); - listCardDAV.setOnItemClickListener(onItemClickListener); - } else - card.setVisibility(View.GONE); - - card = (CardView)findViewById(R.id.caldav); - if (info.caldav != null) { - ProgressBar progress = (ProgressBar)findViewById(R.id.caldav_refreshing); - progress.setVisibility(info.caldav.refreshing ? View.VISIBLE : View.GONE); - - listCalDAV = (ListView)findViewById(R.id.calendars); - listCalDAV.setEnabled(!info.caldav.refreshing); - listCalDAV.setAlpha(info.caldav.refreshing ? 0.5f : 1); - - final CollectionListAdapter adapter = new CollectionListAdapter(this, account); - adapter.addAll(info.caldav.journals); - listCalDAV.setAdapter(adapter); - listCalDAV.setOnItemClickListener(onItemClickListener); - } else - card.setVisibility(View.GONE); - } - - @Override - public void onLoaderReset(Loader loader) { - if (listCardDAV != null) - listCardDAV.setAdapter(null); - - if (listCalDAV != null) - listCalDAV.setAdapter(null); - } - - - private static class AccountLoader extends AsyncTaskLoader implements AccountUpdateService.RefreshingStatusListener, ServiceConnection, SyncStatusObserver { - private final Account account; - private AccountUpdateService.InfoBinder davService; - private Object syncStatusListener; - - public AccountLoader(Context context, Account account) { - super(context); - this.account = account; - } - - @Override - protected void onStartLoading() { - syncStatusListener = ContentResolver.addStatusChangeListener(SYNC_OBSERVER_TYPE_ACTIVE, this); - - getContext().bindService(new Intent(getContext(), AccountUpdateService.class), this, Context.BIND_AUTO_CREATE); - } - - @Override - protected void onStopLoading() { - davService.removeRefreshingStatusListener(this); - getContext().unbindService(this); - - if (syncStatusListener != null) - ContentResolver.removeStatusChangeListener(syncStatusListener); - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - davService = (AccountUpdateService.InfoBinder)service; - davService.addRefreshingStatusListener(this, false); - - forceLoad(); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - davService = null; - } - - @Override - public void onDavRefreshStatusChanged(long id, boolean refreshing) { - forceLoad(); - } - - @Override - public void onStatusChanged(int which) { - forceLoad(); - } - - @Override - public AccountInfo loadInBackground() { - AccountInfo info = new AccountInfo(); - - EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); - - for (ServiceEntity serviceEntity : data.select(ServiceEntity.class).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) { - long id = serviceEntity.getId(); - CollectionInfo.Type service = serviceEntity.getType(); - if (service.equals(CollectionInfo.Type.ADDRESS_BOOK)) { - info.carddav = new AccountInfo.ServiceInfo(); - info.carddav.id = id; - info.carddav.refreshing = (davService != null && davService.isRefreshing(id)) || ContentResolver.isSyncActive(account, App.getAddressBooksAuthority()); - info.carddav.journals = JournalEntity.getJournals(data, serviceEntity); - - AccountManager accountManager = AccountManager.get(getContext()); - for (Account addrBookAccount : accountManager.getAccountsByType(App.getAddressBookAccountType())) { - LocalAddressBook addressBook = new LocalAddressBook(getContext(), addrBookAccount, null); - try { - if (account.equals(addressBook.getMainAccount())) - info.carddav.refreshing |= ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY); - } catch(ContactsStorageException e) { - } - } - } else if (service.equals(CollectionInfo.Type.CALENDAR)) { - info.caldav = new AccountInfo.ServiceInfo(); - info.caldav.id = id; - info.caldav.refreshing = (davService != null && davService.isRefreshing(id)) || - ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) || - ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.authority); - info.caldav.journals = JournalEntity.getJournals(data, serviceEntity); - } - } - return info; - } - } - - - /* LIST ADAPTERS */ - - public static class CollectionListAdapter extends ArrayAdapter { - private Account account; - - public CollectionListAdapter(Context context, Account account) { - super(context, R.layout.account_collection_item); - this.account = account; - } - - @Override - public View getView(int position, View v, ViewGroup parent) { - if (v == null) - v = LayoutInflater.from(getContext()).inflate(R.layout.account_collection_item, parent, false); - - final JournalEntity journalEntity = getItem(position); - final CollectionInfo info = journalEntity.getInfo(); - - TextView tv = (TextView)v.findViewById(R.id.title); - tv.setText(TextUtils.isEmpty(info.displayName) ? info.uid : info.displayName); - - tv = (TextView)v.findViewById(R.id.description); - if (TextUtils.isEmpty(info.description)) - tv.setVisibility(View.GONE); - else { - tv.setVisibility(View.VISIBLE); - tv.setText(info.description); - } - - final View vColor = v.findViewById(R.id.color); - if (info.type.equals(CollectionInfo.Type.ADDRESS_BOOK)) { - vColor.setVisibility(View.GONE); - } else { - if (info.color != null) { - vColor.setBackgroundColor(info.color); - } else { - vColor.setBackgroundColor(LocalCalendar.defaultColor); - } - } - - View readOnly = v.findViewById(R.id.read_only); - readOnly.setVisibility(journalEntity.isReadOnly() ? View.VISIBLE : View.GONE); - - final View shared = v.findViewById(R.id.shared); - boolean isOwner = journalEntity.isOwner(account.name); - shared.setVisibility(isOwner ? View.GONE : View.VISIBLE); - - return v; - } - } - - /* USER ACTIONS */ - - private void deleteAccount() { - AccountManager accountManager = AccountManager.get(this); - - if (Build.VERSION.SDK_INT >= 22) - accountManager.removeAccount(account, this, new AccountManagerCallback() { - @Override - public void run(AccountManagerFuture future) { - try { - if (future.getResult().getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) - finish(); - } catch(OperationCanceledException|IOException|AuthenticatorException e) { - App.log.log(Level.SEVERE, "Couldn't remove account", e); - } - } - }, null); - else - accountManager.removeAccount(account, new AccountManagerCallback() { - @Override - public void run(AccountManagerFuture future) { - try { - if (future.getResult()) - finish(); - } catch (OperationCanceledException|IOException|AuthenticatorException e) { - App.log.log(Level.SEVERE, "Couldn't remove account", e); - } - } - }, null); - } - - protected static void requestSync(Account account) { - String authorities[] = { - App.getAddressBooksAuthority(), - CalendarContract.AUTHORITY, - TaskProvider.ProviderName.OpenTasks.authority - }; - - for (String authority : authorities) { - Bundle extras = new Bundle(); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); // manual sync - extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); // run immediately (don't queue) - ContentResolver.requestSync(account, authority, extras); - } - } - - private void requestSync() { - requestSync(account); - Snackbar.make(findViewById(R.id.parent), R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show(); - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt new file mode 100644 index 00000000..b0335134 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -0,0 +1,410 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.accounts.AccountManager +import android.accounts.AuthenticatorException +import android.accounts.OperationCanceledException +import android.app.LoaderManager +import android.content.* +import android.content.ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.provider.CalendarContract +import android.provider.ContactsContract +import android.support.design.widget.Snackbar +import android.support.v4.content.ContextCompat +import android.support.v7.app.AlertDialog +import android.support.v7.widget.CardView +import android.support.v7.widget.Toolbar +import android.text.TextUtils +import android.view.* +import android.widget.* +import at.bitfire.ical4android.TaskProvider +import at.bitfire.vcard4android.ContactsStorageException +import com.etesync.syncadapter.* +import com.etesync.syncadapter.journalmanager.Crypto +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.model.ServiceEntity +import com.etesync.syncadapter.resource.LocalAddressBook +import com.etesync.syncadapter.resource.LocalCalendar +import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment +import com.etesync.syncadapter.utils.HintManager +import com.etesync.syncadapter.utils.ShowcaseBuilder +import tourguide.tourguide.ToolTip +import java.io.IOException +import java.util.logging.Level + +class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, LoaderManager.LoaderCallbacks, Refreshable { + + private var account: Account? = null + private var accountInfo: AccountInfo? = null + + internal var listCalDAV: ListView? = null + internal var listCardDAV: ListView? = null + + private val onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id -> + val list = parent as ListView + val adapter = list.adapter as ArrayAdapter<*> + val journalEntity = adapter.getItem(position) as JournalEntity + val info = journalEntity.getInfo() + + startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account!!, info)) + } + + private val formattedFingerprint: String? + get() { + var settings: AccountSettings? = null + try { + settings = AccountSettings(this, account!!) + return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.keyPair!!.publicKey) + } catch (e: Exception) { + e.printStackTrace() + return null + } + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + account = intent.getParcelableExtra(EXTRA_ACCOUNT) + title = account!!.name + + setContentView(R.layout.activity_account) + + val icMenu = ContextCompat.getDrawable(this, R.drawable.ic_menu_light) + + // CardDAV toolbar + val tbCardDAV = findViewById(R.id.carddav_menu) as Toolbar + tbCardDAV.overflowIcon = icMenu + tbCardDAV.inflateMenu(R.menu.carddav_actions) + tbCardDAV.setOnMenuItemClickListener(this) + tbCardDAV.setTitle(R.string.settings_carddav) + + // CalDAV toolbar + val tbCalDAV = findViewById(R.id.caldav_menu) as Toolbar + tbCalDAV.overflowIcon = icMenu + tbCalDAV.inflateMenu(R.menu.caldav_actions) + tbCalDAV.setOnMenuItemClickListener(this) + tbCalDAV.setTitle(R.string.settings_caldav) + + // load CardDAV/CalDAV journals + loaderManager.initLoader(0, intent.extras, this) + + if (!HintManager.getHintSeen(this, HINT_VIEW_COLLECTION)) { + ShowcaseBuilder.getBuilder(this) + .setToolTip(ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.account_showcase_view_collection))) + .playOn(tbCardDAV) + HintManager.setHintSeen(this, HINT_VIEW_COLLECTION, true) + } + + if (!SetupUserInfoFragment.hasUserInfo(this, account!!)) { + SetupUserInfoFragment.newInstance(account!!).show(supportFragmentManager, null) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.activity_account, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.sync_now -> requestSync() + R.id.settings -> { + val intent = Intent(this, AccountSettingsActivity::class.java) + intent.putExtra(Constants.KEY_ACCOUNT, account) + startActivity(intent) + } + R.id.delete_account -> AlertDialog.Builder(this@AccountActivity) + .setIcon(R.drawable.ic_error_dark) + .setTitle(R.string.account_delete_confirmation_title) + .setMessage(R.string.account_delete_confirmation_text) + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes) { dialog, which -> deleteAccount() } + .show() + R.id.show_fingerprint -> { + val view = layoutInflater.inflate(R.layout.fingerprint_alertdialog, null) + view.findViewById(R.id.body).visibility = View.GONE + (view.findViewById(R.id.fingerprint) as TextView).text = formattedFingerprint + val dialog = AlertDialog.Builder(this@AccountActivity) + .setIcon(R.drawable.ic_fingerprint_dark) + .setTitle(R.string.show_fingperprint_title) + .setView(view) + .setPositiveButton(android.R.string.yes) { dialog, which -> }.create() + dialog.show() + } + else -> return super.onOptionsItemSelected(item) + } + return true + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + val info: CollectionInfo + when (item.itemId) { + R.id.create_calendar -> { + info = CollectionInfo() + info.type = CollectionInfo.Type.CALENDAR + startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account!!, info)) + } + R.id.create_addressbook -> { + info = CollectionInfo() + info.type = CollectionInfo.Type.ADDRESS_BOOK + startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account!!, info)) + } + } + return false + } + + /* LOADERS AND LOADED DATA */ + + class AccountInfo { + internal var carddav: ServiceInfo? = null + internal var caldav: ServiceInfo? = null + + class ServiceInfo { + internal var id: Long = 0 + internal var refreshing: Boolean = false + + internal var journals: List? = null + } + } + + override fun onCreateLoader(id: Int, args: Bundle): Loader { + return AccountLoader(this, account!!) + } + + override fun refresh() { + loaderManager.restartLoader(0, intent.extras, this) + } + + override fun onLoadFinished(loader: Loader, info: AccountInfo) { + accountInfo = info + + var card = findViewById(R.id.carddav) as CardView + if (info.carddav != null) { + val progress = findViewById(R.id.carddav_refreshing) as ProgressBar + progress.visibility = if (info.carddav!!.refreshing) View.VISIBLE else View.GONE + + listCardDAV = findViewById(R.id.address_books) as ListView + listCardDAV!!.isEnabled = !info.carddav!!.refreshing + listCardDAV!!.setAlpha(if (info.carddav!!.refreshing) 0.5f else 1f) + + val adapter = CollectionListAdapter(this, account!!) + adapter.addAll(info.carddav!!.journals!!) + listCardDAV!!.adapter = adapter + listCardDAV!!.onItemClickListener = onItemClickListener + } else + card.visibility = View.GONE + + card = findViewById(R.id.caldav) as CardView + if (info.caldav != null) { + val progress = findViewById(R.id.caldav_refreshing) as ProgressBar + progress.visibility = if (info.caldav!!.refreshing) View.VISIBLE else View.GONE + + listCalDAV = findViewById(R.id.calendars) as ListView + listCalDAV!!.isEnabled = !info.caldav!!.refreshing + listCalDAV!!.setAlpha(if (info.caldav!!.refreshing) 0.5f else 1f) + + val adapter = CollectionListAdapter(this, account!!) + adapter.addAll(info.caldav!!.journals!!) + listCalDAV!!.adapter = adapter + listCalDAV!!.onItemClickListener = onItemClickListener + } else + card.visibility = View.GONE + } + + override fun onLoaderReset(loader: Loader) { + if (listCardDAV != null) + listCardDAV!!.adapter = null + + if (listCalDAV != null) + listCalDAV!!.adapter = null + } + + + private class AccountLoader(context: Context, private val account: Account) : AsyncTaskLoader(context), AccountUpdateService.RefreshingStatusListener, ServiceConnection, SyncStatusObserver { + private var davService: AccountUpdateService.InfoBinder? = null + private var syncStatusListener: Any? = null + + override fun onStartLoading() { + syncStatusListener = ContentResolver.addStatusChangeListener(SYNC_OBSERVER_TYPE_ACTIVE, this) + + context.bindService(Intent(context, AccountUpdateService::class.java), this, Context.BIND_AUTO_CREATE) + } + + override fun onStopLoading() { + davService!!.removeRefreshingStatusListener(this) + context.unbindService(this) + + if (syncStatusListener != null) + ContentResolver.removeStatusChangeListener(syncStatusListener) + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + davService = service as AccountUpdateService.InfoBinder + davService!!.addRefreshingStatusListener(this, false) + + forceLoad() + } + + override fun onServiceDisconnected(name: ComponentName) { + davService = null + } + + override fun onDavRefreshStatusChanged(id: Long, refreshing: Boolean) { + forceLoad() + } + + override fun onStatusChanged(which: Int) { + forceLoad() + } + + override fun loadInBackground(): AccountInfo { + val info = AccountInfo() + + val data = (context.applicationContext as App).data + + for (serviceEntity in data.select(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) { + val id = serviceEntity.id.toLong() + val service = serviceEntity.type + if (service == CollectionInfo.Type.ADDRESS_BOOK) { + info.carddav = AccountInfo.ServiceInfo() + info.carddav!!.id = id + info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.getAddressBooksAuthority()) + info.carddav!!.journals = JournalEntity.getJournals(data, serviceEntity) + + val accountManager = AccountManager.get(context) + for (addrBookAccount in accountManager.getAccountsByType(App.getAddressBookAccountType())) { + val addressBook = LocalAddressBook(context, addrBookAccount, null) + try { + if (account == addressBook.mainAccount) + info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY) + } catch (e: ContactsStorageException) { + } + + } + } else if (service == CollectionInfo.Type.CALENDAR) { + info.caldav = AccountInfo.ServiceInfo() + info.caldav!!.id = id + info.caldav!!.refreshing = davService != null && davService!!.isRefreshing(id) || + ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) || + ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.authority) + info.caldav!!.journals = JournalEntity.getJournals(data, serviceEntity) + } + } + return info + } + } + + + /* LIST ADAPTERS */ + + class CollectionListAdapter(context: Context, private val account: Account) : ArrayAdapter(context, R.layout.account_collection_item) { + + override fun getView(position: Int, v: View?, parent: ViewGroup): View { + var v = v + if (v == null) + v = LayoutInflater.from(context).inflate(R.layout.account_collection_item, parent, false) + + val journalEntity = getItem(position) + val info = journalEntity!!.info + + var tv = v!!.findViewById(R.id.title) as TextView + tv.text = if (TextUtils.isEmpty(info.displayName)) info.uid else info.displayName + + tv = v.findViewById(R.id.description) as TextView + if (TextUtils.isEmpty(info.description)) + tv.visibility = View.GONE + else { + tv.visibility = View.VISIBLE + tv.text = info.description + } + + val vColor = v.findViewById(R.id.color) + if (info.type == CollectionInfo.Type.ADDRESS_BOOK) { + vColor.visibility = View.GONE + } else { + if (info.color != null) { + vColor.setBackgroundColor(info.color) + } else { + vColor.setBackgroundColor(LocalCalendar.defaultColor) + } + } + + val readOnly = v.findViewById(R.id.read_only) + readOnly.visibility = if (journalEntity.isReadOnly) View.VISIBLE else View.GONE + + val shared = v.findViewById(R.id.shared) + val isOwner = journalEntity.isOwner(account.name) + shared.visibility = if (isOwner) View.GONE else View.VISIBLE + + return v + } + } + + /* USER ACTIONS */ + + private fun deleteAccount() { + val accountManager = AccountManager.get(this) + + if (Build.VERSION.SDK_INT >= 22) + accountManager.removeAccount(account, this, { future -> + try { + if (future.result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) + finish() + } catch (e: OperationCanceledException) { + App.log.log(Level.SEVERE, "Couldn't remove account", e) + } catch (e: IOException) { + App.log.log(Level.SEVERE, "Couldn't remove account", e) + } catch (e: AuthenticatorException) { + App.log.log(Level.SEVERE, "Couldn't remove account", e) + } + }, null) + else + accountManager.removeAccount(account, { future -> + try { + if (future.result) + finish() + } catch (e: OperationCanceledException) { + App.log.log(Level.SEVERE, "Couldn't remove account", e) + } catch (e: IOException) { + App.log.log(Level.SEVERE, "Couldn't remove account", e) + } catch (e: AuthenticatorException) { + App.log.log(Level.SEVERE, "Couldn't remove account", e) + } + }, null) + } + + private fun requestSync() { + requestSync(account) + Snackbar.make(findViewById(R.id.parent), R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show() + } + + companion object { + val EXTRA_ACCOUNT = "account" + private val HINT_VIEW_COLLECTION = "ViewCollection" + + protected fun requestSync(account: Account?) { + val authorities = arrayOf(App.getAddressBooksAuthority(), CalendarContract.AUTHORITY, TaskProvider.ProviderName.OpenTasks.authority) + + for (authority in authorities) { + val extras = Bundle() + extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync + extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue) + ContentResolver.requestSync(account, authority, extras) + } + } + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.java deleted file mode 100644 index a60e8d91..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.OnAccountsUpdateListener; -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.ListFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import com.etesync.syncadapter.AccountsChangedReceiver; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; - -public class AccountListFragment extends ListFragment implements LoaderManager.LoaderCallbacks, AdapterView.OnItemClickListener { - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - setListAdapter(new AccountListAdapter(getContext())); - - return inflater.inflate(R.layout.account_list, container, false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - getLoaderManager().initLoader(0, getArguments(), this); - - ListView list = getListView(); - list.setOnItemClickListener(this); - list.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Account account = (Account)getListAdapter().getItem(position); - - Intent intent = new Intent(getContext(), AccountActivity.class); - intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account); - startActivity(intent); - } - - - // loader - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new AccountLoader(getContext()); - } - - @Override - public void onLoadFinished(Loader loader, Account[] accounts) { - AccountListAdapter adapter = (AccountListAdapter)getListAdapter(); - adapter.clear(); - adapter.addAll(accounts); - } - - @Override - public void onLoaderReset(Loader loader) { - ((AccountListAdapter)getListAdapter()).clear(); - } - - private static class AccountLoader extends AsyncTaskLoader implements OnAccountsUpdateListener { - private final AccountManager accountManager; - - public AccountLoader(Context context) { - super(context); - accountManager = AccountManager.get(context); - } - - @Override - protected void onStartLoading() { - AccountsChangedReceiver.registerListener(this, true); - } - - @Override - protected void onStopLoading() { - AccountsChangedReceiver.unregisterListener(this); - } - - @Override - public void onAccountsUpdated(Account[] accounts) { - forceLoad(); - } - - @Override - @SuppressLint("MissingPermission") - public Account[] loadInBackground() { - return accountManager.getAccountsByType(App.getAccountType()); - } - } - - - // list adapter - - static class AccountListAdapter extends ArrayAdapter { - public AccountListAdapter(Context context) { - super(context, R.layout.account_list_item); - } - - @Override - public View getView(int position, View v, ViewGroup parent) { - if (v == null) - v = LayoutInflater.from(getContext()).inflate(R.layout.account_list_item, parent, false); - - Account account = getItem(position); - - TextView tv = (TextView)v.findViewById(R.id.account_name); - tv.setText(account.name); - - return v; - } - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.kt new file mode 100644 index 00000000..1777a15e --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.kt @@ -0,0 +1,119 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.accounts.AccountManager +import android.accounts.OnAccountsUpdateListener +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v4.app.ListFragment +import android.support.v4.app.LoaderManager +import android.support.v4.content.AsyncTaskLoader +import android.support.v4.content.Loader +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AbsListView +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.TextView +import com.etesync.syncadapter.AccountsChangedReceiver +import com.etesync.syncadapter.App +import com.etesync.syncadapter.R + +class AccountListFragment : ListFragment(), LoaderManager.LoaderCallbacks>, AdapterView.OnItemClickListener { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + listAdapter = AccountListAdapter(context!!) + + return inflater.inflate(R.layout.account_list, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + loaderManager.initLoader(0, arguments, this) + + val list = listView + list.onItemClickListener = this + list.choiceMode = AbsListView.CHOICE_MODE_SINGLE + } + + override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val account = listAdapter.getItem(position) as Account + + val intent = Intent(context, AccountActivity::class.java) + intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account) + startActivity(intent) + } + + + // loader + + override fun onCreateLoader(id: Int, args: Bundle?): Loader> { + return AccountLoader(context!!) + } + + override fun onLoadFinished(loader: Loader>, accounts: Array) { + val adapter = listAdapter as AccountListAdapter + adapter.clear() + adapter.addAll(*accounts) + } + + override fun onLoaderReset(loader: Loader>) { + (listAdapter as AccountListAdapter).clear() + } + + private class AccountLoader(context: Context) : AsyncTaskLoader>(context), OnAccountsUpdateListener { + private val accountManager: AccountManager + + init { + accountManager = AccountManager.get(context) + } + + override fun onStartLoading() { + AccountsChangedReceiver.registerListener(this, true) + } + + override fun onStopLoading() { + AccountsChangedReceiver.unregisterListener(this) + } + + override fun onAccountsUpdated(accounts: Array) { + forceLoad() + } + + @SuppressLint("MissingPermission") + override fun loadInBackground(): Array? { + return accountManager.getAccountsByType(App.getAccountType()) + } + } + + + // list adapter + + internal class AccountListAdapter(context: Context) : ArrayAdapter(context, R.layout.account_list_item) { + + override fun getView(position: Int, v: View?, parent: ViewGroup): View { + var v = v + if (v == null) + v = LayoutInflater.from(context).inflate(R.layout.account_list_item, parent, false) + + val account = getItem(position) + + val tv = v!!.findViewById(R.id.account_name) as TextView + tv.text = account!!.name + + return v + } + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.java deleted file mode 100644 index e8b82cf1..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.SyncStatusObserver; -import android.os.Bundle; -import android.provider.CalendarContract; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.NavUtils; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; -import android.support.v7.preference.EditTextPreference; -import android.support.v7.preference.ListPreference; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceFragmentCompat; -import android.support.v7.preference.SwitchPreferenceCompat; -import android.text.TextUtils; -import android.view.MenuItem; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.InvalidAccountException; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.ui.setup.LoginCredentials; -import com.etesync.syncadapter.ui.setup.LoginCredentialsChangeFragment; - -import static com.etesync.syncadapter.Constants.KEY_ACCOUNT; - -public class AccountSettingsActivity extends BaseActivity { - private Account account; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - account = getIntent().getParcelableExtra(KEY_ACCOUNT); - setTitle(getString(R.string.settings_title, account.name)); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - if (savedInstanceState == null) - getSupportFragmentManager().beginTransaction() - .replace(android.R.id.content, AccountSettingsFragment.instantiate(this, AccountSettingsFragment.class.getName(), getIntent().getExtras())) - .commit(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - Intent intent = new Intent(this, AccountActivity.class); - intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account); - NavUtils.navigateUpTo(this, intent); - return true; - } else - return false; - } - - - public static class AccountSettingsFragment extends PreferenceFragmentCompat implements LoaderManager.LoaderCallbacks { - Account account; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - account = getArguments().getParcelable(KEY_ACCOUNT); - - getLoaderManager().initLoader(0, getArguments(), this); - } - - @Override - public void onCreatePreferences(Bundle bundle, String s) { - addPreferencesFromResource(R.xml.settings_account); - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new AccountSettingsLoader(getContext(), (Account)args.getParcelable(KEY_ACCOUNT)); - } - - @Override - public void onLoadFinished(Loader loader, final AccountSettings settings) { - if (settings == null) { - getActivity().finish(); - return; - } - - // category: authentication - final EditTextPreference prefPassword = (EditTextPreference)findPreference("password"); - prefPassword.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - LoginCredentials credentials = newValue != null ? new LoginCredentials(settings.getUri(), account.name, (String) newValue) : null; - LoginCredentialsChangeFragment.newInstance(account, credentials).show(getFragmentManager(), null); - getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this); - return false; - } - }); - - // category: synchronization - final ListPreference prefSyncContacts = (ListPreference)findPreference("sync_interval_contacts"); - final Long syncIntervalContacts = settings.getSyncInterval(App.getAddressBooksAuthority()); - if (syncIntervalContacts != null) { - prefSyncContacts.setValue(syncIntervalContacts.toString()); - if (syncIntervalContacts == AccountSettings.SYNC_INTERVAL_MANUALLY) - prefSyncContacts.setSummary(R.string.settings_sync_summary_manually); - else - prefSyncContacts.setSummary(getString(R.string.settings_sync_summary_periodically, prefSyncContacts.getEntry())); - prefSyncContacts.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - settings.setSyncInterval(App.getAddressBooksAuthority(), Long.parseLong((String)newValue)); - getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this); - return false; - } - }); - } else { - prefSyncContacts.setEnabled(false); - prefSyncContacts.setSummary(R.string.settings_sync_summary_not_available); - } - - final ListPreference prefSyncCalendars = (ListPreference)findPreference("sync_interval_calendars"); - final Long syncIntervalCalendars = settings.getSyncInterval(CalendarContract.AUTHORITY); - if (syncIntervalCalendars != null) { - prefSyncCalendars.setValue(syncIntervalCalendars.toString()); - if (syncIntervalCalendars == AccountSettings.SYNC_INTERVAL_MANUALLY) - prefSyncCalendars.setSummary(R.string.settings_sync_summary_manually); - else - prefSyncCalendars.setSummary(getString(R.string.settings_sync_summary_periodically, prefSyncCalendars.getEntry())); - prefSyncCalendars.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - settings.setSyncInterval(CalendarContract.AUTHORITY, Long.parseLong((String)newValue)); - getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this); - return false; - } - }); - } else { - prefSyncCalendars.setEnabled(false); - prefSyncCalendars.setSummary(R.string.settings_sync_summary_not_available); - } - - final SwitchPreferenceCompat prefWifiOnly = (SwitchPreferenceCompat)findPreference("sync_wifi_only"); - prefWifiOnly.setChecked(settings.getSyncWifiOnly()); - prefWifiOnly.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object wifiOnly) { - settings.setSyncWiFiOnly((Boolean)wifiOnly); - getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this); - return false; - } - }); - - final EditTextPreference prefWifiOnlySSID = (EditTextPreference)findPreference("sync_wifi_only_ssid"); - final String onlySSID = settings.getSyncWifiOnlySSID(); - prefWifiOnlySSID.setText(onlySSID); - if (onlySSID != null) - prefWifiOnlySSID.setSummary(getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID)); - else - prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off); - prefWifiOnlySSID.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - String ssid = (String)newValue; - settings.setSyncWifiOnlySSID(!TextUtils.isEmpty(ssid) ? ssid : null); - getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this); - return false; - } - }); - } - - @Override - public void onLoaderReset(Loader loader) { - } - - } - - - private static class AccountSettingsLoader extends AsyncTaskLoader implements SyncStatusObserver { - - final Account account; - Object listenerHandle; - - public AccountSettingsLoader(Context context, Account account) { - super(context); - this.account = account; - } - - @Override - protected void onStartLoading() { - forceLoad(); - listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this); - } - - @Override - protected void onStopLoading() { - ContentResolver.removeStatusChangeListener(listenerHandle); - } - - @Override - public void abandon() { - onStopLoading(); - } - - @Override - public AccountSettings loadInBackground() { - AccountSettings settings; - try { - settings = new AccountSettings(getContext(), account); - } catch(InvalidAccountException e) { - return null; - } - return settings; - } - - @Override - public void onStatusChanged(int which) { - App.log.fine("Reloading account settings"); - forceLoad(); - } - - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt new file mode 100644 index 00000000..b582ae8c --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt @@ -0,0 +1,197 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.SyncStatusObserver +import android.os.Bundle +import android.provider.CalendarContract +import android.support.v4.app.LoaderManager +import android.support.v4.app.NavUtils +import android.support.v4.content.AsyncTaskLoader +import android.support.v4.content.Loader +import android.support.v7.preference.* +import android.text.TextUtils +import android.view.MenuItem +import com.etesync.syncadapter.AccountSettings +import com.etesync.syncadapter.App +import com.etesync.syncadapter.Constants.KEY_ACCOUNT +import com.etesync.syncadapter.InvalidAccountException +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.setup.LoginCredentials +import com.etesync.syncadapter.ui.setup.LoginCredentialsChangeFragment + +class AccountSettingsActivity : BaseActivity() { + private var account: Account? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + account = intent.getParcelableExtra(KEY_ACCOUNT) + title = getString(R.string.settings_title, account!!.name) + + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + if (savedInstanceState == null) { + val frag = AccountSettingsFragment() + frag.arguments = intent.extras + supportFragmentManager.beginTransaction() + .replace(android.R.id.content, frag) + .commit() + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + val intent = Intent(this, AccountActivity::class.java) + intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account) + NavUtils.navigateUpTo(this, intent) + return true + } else + return false + } + + + class AccountSettingsFragment : PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks { + internal lateinit var account: Account + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + account = arguments!!.getParcelable(KEY_ACCOUNT) + + loaderManager.initLoader(0, arguments, this) + } + + override fun onCreatePreferences(bundle: Bundle, s: String) { + addPreferencesFromResource(R.xml.settings_account) + } + + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account) + } + + override fun onLoadFinished(loader: Loader, settings: AccountSettings?) { + if (settings == null) { + activity!!.finish() + return + } + + // category: authentication + val prefPassword = findPreference("password") as EditTextPreference + prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> + val credentials = if (newValue != null) LoginCredentials(settings.uri, account!!.name, newValue as String) else null + LoginCredentialsChangeFragment.newInstance(account!!, credentials!!).show(fragmentManager!!, null) + loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) + false + } + + // category: synchronization + val prefSyncContacts = findPreference("sync_interval_contacts") as ListPreference + val syncIntervalContacts = settings.getSyncInterval(App.getAddressBooksAuthority()) + if (syncIntervalContacts != null) { + prefSyncContacts.value = syncIntervalContacts.toString() + if (syncIntervalContacts == AccountSettings.SYNC_INTERVAL_MANUALLY) + prefSyncContacts.setSummary(R.string.settings_sync_summary_manually) + else + prefSyncContacts.summary = getString(R.string.settings_sync_summary_periodically, prefSyncContacts.entry) + prefSyncContacts.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> + settings.setSyncInterval(App.getAddressBooksAuthority(), java.lang.Long.parseLong(newValue as String)) + loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) + false + } + } else { + prefSyncContacts.isEnabled = false + prefSyncContacts.setSummary(R.string.settings_sync_summary_not_available) + } + + val prefSyncCalendars = findPreference("sync_interval_calendars") as ListPreference + val syncIntervalCalendars = settings.getSyncInterval(CalendarContract.AUTHORITY) + if (syncIntervalCalendars != null) { + prefSyncCalendars.value = syncIntervalCalendars.toString() + if (syncIntervalCalendars == AccountSettings.SYNC_INTERVAL_MANUALLY) + prefSyncCalendars.setSummary(R.string.settings_sync_summary_manually) + else + prefSyncCalendars.summary = getString(R.string.settings_sync_summary_periodically, prefSyncCalendars.entry) + prefSyncCalendars.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> + settings.setSyncInterval(CalendarContract.AUTHORITY, java.lang.Long.parseLong(newValue as String)) + loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) + false + } + } else { + prefSyncCalendars.isEnabled = false + prefSyncCalendars.setSummary(R.string.settings_sync_summary_not_available) + } + + val prefWifiOnly = findPreference("sync_wifi_only") as SwitchPreferenceCompat + prefWifiOnly.isChecked = settings.syncWifiOnly + prefWifiOnly.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, wifiOnly -> + settings.setSyncWiFiOnly(wifiOnly as Boolean) + loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) + false + } + + val prefWifiOnlySSID = findPreference("sync_wifi_only_ssid") as EditTextPreference + val onlySSID = settings.syncWifiOnlySSID + prefWifiOnlySSID.text = onlySSID + if (onlySSID != null) + prefWifiOnlySSID.summary = getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID) + else + prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off) + prefWifiOnlySSID.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> + val ssid = newValue as String + settings.syncWifiOnlySSID = if (!TextUtils.isEmpty(ssid)) ssid else null + loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) + false + } + } + + override fun onLoaderReset(loader: Loader) {} + + } + + + private class AccountSettingsLoader(context: Context, internal val account: Account) : AsyncTaskLoader(context), SyncStatusObserver { + internal lateinit var listenerHandle: Any + + override fun onStartLoading() { + forceLoad() + listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this) + } + + override fun onStopLoading() { + ContentResolver.removeStatusChangeListener(listenerHandle) + } + + override fun abandon() { + onStopLoading() + } + + override fun loadInBackground(): AccountSettings? { + val settings: AccountSettings + try { + settings = AccountSettings(context, account) + } catch (e: InvalidAccountException) { + return null + } + + return settings + } + + override fun onStatusChanged(which: Int) { + App.log.fine("Reloading account settings") + forceLoad() + } + + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountsActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountsActivity.java deleted file mode 100644 index 8071eaf5..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountsActivity.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.content.ContentResolver; -import android.content.Intent; -import android.content.SyncStatusObserver; -import android.os.Bundle; -import android.support.design.widget.FloatingActionButton; -import android.support.design.widget.NavigationView; -import android.support.design.widget.Snackbar; -import android.support.v4.app.FragmentTransaction; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v7.widget.Toolbar; -import android.view.Gravity; -import android.view.MenuItem; -import android.view.View; -import android.widget.Toast; - -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.ui.setup.LoginActivity; -import com.etesync.syncadapter.utils.HintManager; -import com.etesync.syncadapter.utils.ShowcaseBuilder; - -import tourguide.tourguide.ToolTip; - -import static android.content.ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS; -import static com.etesync.syncadapter.BuildConfig.DEBUG; -import static com.etesync.syncadapter.Constants.serviceUrl; - -public class AccountsActivity extends BaseActivity implements NavigationView.OnNavigationItemSelectedListener, SyncStatusObserver { - public static final String HINT_ACCOUNT_ADD = "AddAccount"; - - private Snackbar syncStatusSnackbar; - private Object syncStatusObserver; - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_accounts); - - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); - fab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(AccountsActivity.this, LoginActivity.class)); - } - }); - - DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); - ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( - this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); - drawer.setDrawerListener(toggle); - toggle.syncState(); - - NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); - navigationView.setNavigationItemSelectedListener(this); - navigationView.setItemIconTintList(null); - - if (savedInstanceState == null && !getPackageName().equals(getCallingPackage())) { - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - for (StartupDialogFragment fragment : StartupDialogFragment.getStartupDialogs(this)) - ft.add(fragment, null); - ft.commit(); - - if (DEBUG) { - Toast.makeText(this, "Server: " + serviceUrl.toString(), Toast.LENGTH_SHORT).show(); - } - } - - PermissionsActivity.requestAllPermissions(this); - - if (!HintManager.getHintSeen(this, HINT_ACCOUNT_ADD)) { - ShowcaseBuilder.getBuilder(this) - .setToolTip(new ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.accounts_showcase_add)).setGravity(Gravity.TOP | Gravity.LEFT)) - .playOn(fab); - HintManager.setHintSeen(this, HINT_ACCOUNT_ADD, true); - } - } - - @Override - protected void onResume() { - super.onResume(); - onStatusChanged(SYNC_OBSERVER_TYPE_SETTINGS); - syncStatusObserver = ContentResolver.addStatusChangeListener(SYNC_OBSERVER_TYPE_SETTINGS, this); - } - - @Override - protected void onPause() { - super.onPause(); - if (syncStatusObserver != null) { - ContentResolver.removeStatusChangeListener(syncStatusObserver); - syncStatusObserver = null; - } - } - - @Override - public void onStatusChanged(int which) { - if (syncStatusSnackbar != null) { - syncStatusSnackbar.dismiss(); - syncStatusSnackbar = null; - } - - if (!ContentResolver.getMasterSyncAutomatically()) { - syncStatusSnackbar = Snackbar.make(findViewById(R.id.coordinator), R.string.accounts_global_sync_disabled, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.accounts_global_sync_enable, new View.OnClickListener() { - @Override - public void onClick(View v) { - ContentResolver.setMasterSyncAutomatically(true); - } - }); - syncStatusSnackbar.show(); - } - } - - - @Override - public void onBackPressed() { - DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); - if (drawer.isDrawerOpen(GravityCompat.START)) - drawer.closeDrawer(GravityCompat.START); - else - super.onBackPressed(); - } - - @Override - public boolean onNavigationItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.nav_about: - startActivity(new Intent(this, AboutActivity.class)); - break; - case R.id.nav_app_settings: - startActivity(new Intent(this, AppSettingsActivity.class)); - break; - case R.id.nav_website: - startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri)); - break; - case R.id.nav_guide: - WebViewActivity.openUrl(this, Constants.helpUri); - break; - case R.id.nav_faq: - WebViewActivity.openUrl(this, Constants.faqUri); - break; - case R.id.nav_report_issue: - startActivity(new Intent(Intent.ACTION_VIEW, Constants.reportIssueUri)); - break; - case R.id.nav_contact: - startActivity(new Intent(Intent.ACTION_VIEW, Constants.contactUri)); - break; - } - - DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); - drawer.closeDrawer(GravityCompat.START); - return true; - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountsActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountsActivity.kt new file mode 100644 index 00000000..cc36fa66 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountsActivity.kt @@ -0,0 +1,138 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.content.ContentResolver +import android.content.ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS +import android.content.Intent +import android.content.SyncStatusObserver +import android.os.Bundle +import android.support.design.widget.FloatingActionButton +import android.support.design.widget.NavigationView +import android.support.design.widget.Snackbar +import android.support.v4.view.GravityCompat +import android.support.v4.widget.DrawerLayout +import android.support.v7.app.ActionBarDrawerToggle +import android.support.v7.widget.Toolbar +import android.view.Gravity +import android.view.MenuItem +import android.view.View +import android.widget.Toast +import com.etesync.syncadapter.BuildConfig.DEBUG +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.Constants.serviceUrl +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.setup.LoginActivity +import com.etesync.syncadapter.utils.HintManager +import com.etesync.syncadapter.utils.ShowcaseBuilder +import tourguide.tourguide.ToolTip + +class AccountsActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener, SyncStatusObserver { + + private var syncStatusSnackbar: Snackbar? = null + private var syncStatusObserver: Any? = null + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_accounts) + + val toolbar = findViewById(R.id.toolbar) as Toolbar + setSupportActionBar(toolbar) + + val fab = findViewById(R.id.fab) as FloatingActionButton + fab.setOnClickListener { startActivity(Intent(this@AccountsActivity, LoginActivity::class.java)) } + + val drawer = findViewById(R.id.drawer_layout) as DrawerLayout + val toggle = ActionBarDrawerToggle( + this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) + drawer.setDrawerListener(toggle) + toggle.syncState() + + val navigationView = findViewById(R.id.nav_view) as NavigationView + navigationView.setNavigationItemSelectedListener(this) + navigationView.itemIconTintList = null + + if (savedInstanceState == null && packageName != callingPackage) { + val ft = supportFragmentManager.beginTransaction() + for (fragment in StartupDialogFragment.getStartupDialogs(this)) + ft.add(fragment, null) + ft.commit() + + if (DEBUG) { + Toast.makeText(this, "Server: " + serviceUrl.toString(), Toast.LENGTH_SHORT).show() + } + } + + PermissionsActivity.requestAllPermissions(this) + + if (!HintManager.getHintSeen(this, HINT_ACCOUNT_ADD)) { + ShowcaseBuilder.getBuilder(this) + .setToolTip(ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.accounts_showcase_add)).setGravity(Gravity.TOP or Gravity.LEFT)) + .playOn(fab) + HintManager.setHintSeen(this, HINT_ACCOUNT_ADD, true) + } + } + + override fun onResume() { + super.onResume() + onStatusChanged(SYNC_OBSERVER_TYPE_SETTINGS) + syncStatusObserver = ContentResolver.addStatusChangeListener(SYNC_OBSERVER_TYPE_SETTINGS, this) + } + + override fun onPause() { + super.onPause() + if (syncStatusObserver != null) { + ContentResolver.removeStatusChangeListener(syncStatusObserver) + syncStatusObserver = null + } + } + + override fun onStatusChanged(which: Int) { + if (syncStatusSnackbar != null) { + syncStatusSnackbar!!.dismiss() + syncStatusSnackbar = null + } + + if (!ContentResolver.getMasterSyncAutomatically()) { + syncStatusSnackbar = Snackbar.make(findViewById(R.id.coordinator), R.string.accounts_global_sync_disabled, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.accounts_global_sync_enable) { ContentResolver.setMasterSyncAutomatically(true) } + syncStatusSnackbar!!.show() + } + } + + + override fun onBackPressed() { + val drawer = findViewById(R.id.drawer_layout) as DrawerLayout + if (drawer.isDrawerOpen(GravityCompat.START)) + drawer.closeDrawer(GravityCompat.START) + else + super.onBackPressed() + } + + override fun onNavigationItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.nav_about -> startActivity(Intent(this, AboutActivity::class.java)) + R.id.nav_app_settings -> startActivity(Intent(this, AppSettingsActivity::class.java)) + R.id.nav_website -> startActivity(Intent(Intent.ACTION_VIEW, Constants.webUri)) + R.id.nav_guide -> WebViewActivity.openUrl(this, Constants.helpUri) + R.id.nav_faq -> WebViewActivity.openUrl(this, Constants.faqUri) + R.id.nav_report_issue -> startActivity(Intent(Intent.ACTION_VIEW, Constants.reportIssueUri)) + R.id.nav_contact -> startActivity(Intent(Intent.ACTION_VIEW, Constants.contactUri)) + } + + val drawer = findViewById(R.id.drawer_layout) as DrawerLayout + drawer.closeDrawer(GravityCompat.START) + return true + } + + companion object { + val HINT_ACCOUNT_ADD = "AddAccount" + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java deleted file mode 100644 index 053ea307..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.AsyncTask; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v7.app.AlertDialog; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.HttpClient; -import com.etesync.syncadapter.InvalidAccountException; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.journalmanager.Crypto; -import com.etesync.syncadapter.journalmanager.JournalManager; -import com.etesync.syncadapter.journalmanager.UserInfoManager; -import com.etesync.syncadapter.model.CollectionInfo; - -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; - -public class AddMemberFragment extends DialogFragment { - final static private String KEY_MEMBER = "memberEmail"; - private Account account; - private AccountSettings settings; - private Context ctx; - private HttpUrl remote; - private CollectionInfo info; - private String memberEmail; - private byte[] memberPubKey; - - public static AddMemberFragment newInstance(Account account, CollectionInfo info, String email) { - AddMemberFragment frag = new AddMemberFragment(); - Bundle args = new Bundle(1); - args.putParcelable(Constants.KEY_ACCOUNT, account); - args.putSerializable(Constants.KEY_COLLECTION_INFO, info); - args.putString(KEY_MEMBER, email); - frag.setArguments(args); - return frag; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - account = getArguments().getParcelable(Constants.KEY_ACCOUNT); - info = (CollectionInfo) getArguments().getSerializable(Constants.KEY_COLLECTION_INFO); - memberEmail = getArguments().getString(KEY_MEMBER); - ctx = getContext(); - try { - settings = new AccountSettings(ctx, account); - } catch (InvalidAccountException e) { - e.printStackTrace(); - } - remote = HttpUrl.get(settings.getUri()); - - new MemberAdd().execute(); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - ProgressDialog progress = new ProgressDialog(getContext()); - progress.setTitle(R.string.collection_members_adding); - progress.setMessage(getString(R.string.please_wait)); - progress.setIndeterminate(true); - progress.setCanceledOnTouchOutside(false); - setCancelable(false); - return progress; - } - - private class MemberAdd extends AsyncTask { - @Override - protected AddResult doInBackground(Void... voids) { - try { - OkHttpClient httpClient = HttpClient.create(ctx, settings); - UserInfoManager userInfoManager = new UserInfoManager(httpClient, remote); - - UserInfoManager.UserInfo userInfo = userInfoManager.get(memberEmail); - if (userInfo == null) { - throw new Exception(getString(R.string.collection_members_error_user_not_found, memberEmail)); - } - memberPubKey = userInfo.getPubkey(); - return new AddResult(null); - } catch (Exception e) { - return new AddResult(e); - } - } - - @Override - protected void onPostExecute(AddResult result) { - if (result.throwable == null) { - String fingerprint = Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(memberPubKey); - View view = LayoutInflater.from(getContext()).inflate(R.layout.fingerprint_alertdialog, null); - ((TextView) view.findViewById(R.id.body)).setText(getString(R.string.trust_fingerprint_body, memberEmail)); - ((TextView) view.findViewById(R.id.fingerprint)).setText(fingerprint); - new AlertDialog.Builder(getActivity()) - .setIcon(R.drawable.ic_fingerprint_dark) - .setTitle(R.string.trust_fingerprint_title) - .setView(view) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - new MemberAddSecond().execute(); - } - }) - .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dismiss(); - } - }).show(); - } else { - new AlertDialog.Builder(getActivity()) - .setIcon(R.drawable.ic_error_dark) - .setTitle(R.string.collection_members_add_error) - .setMessage(result.throwable.getMessage()) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }).show(); - dismiss(); - } - } - - class AddResult { - final Throwable throwable; - - AddResult(final Throwable throwable) { - this.throwable = throwable; - } - } - } - - private class MemberAddSecond extends AsyncTask { - @Override - protected AddResultSecond doInBackground(Void... voids) { - try { - OkHttpClient httpClient = HttpClient.create(ctx, settings); - JournalManager journalsManager = new JournalManager(httpClient, remote); - - JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(info.uid); - Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); - - byte[] encryptedKey = crypto.getEncryptedKey(settings.getKeyPair(), memberPubKey); - JournalManager.Member member = new JournalManager.Member(memberEmail, encryptedKey); - journalsManager.addMember(journal, member); - return new AddResultSecond(null); - } catch (Exception e) { - return new AddResultSecond(e); - } - } - - @Override - protected void onPostExecute(AddResultSecond result) { - if (result.throwable == null) { - ((Refreshable) getActivity()).refresh(); - } else { - new AlertDialog.Builder(getActivity()) - .setIcon(R.drawable.ic_error_dark) - .setTitle(R.string.collection_members_add_error) - .setMessage(result.throwable.getMessage()) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }).show(); - } - dismiss(); - } - - class AddResultSecond { - final Throwable throwable; - - AddResultSecond(final Throwable throwable) { - this.throwable = throwable; - } - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.kt new file mode 100644 index 00000000..fb73f1fa --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.kt @@ -0,0 +1,146 @@ +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.app.Dialog +import android.app.ProgressDialog +import android.content.Context +import android.os.AsyncTask +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.support.v7.app.AlertDialog +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import com.etesync.syncadapter.* +import com.etesync.syncadapter.journalmanager.Crypto +import com.etesync.syncadapter.journalmanager.JournalManager +import com.etesync.syncadapter.journalmanager.UserInfoManager +import com.etesync.syncadapter.model.CollectionInfo +import okhttp3.HttpUrl + +class AddMemberFragment : DialogFragment() { + private var account: Account? = null + private var settings: AccountSettings? = null + private var ctx: Context? = null + private var remote: HttpUrl? = null + private var info: CollectionInfo? = null + private var memberEmail: String? = null + private var memberPubKey: ByteArray? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + account = arguments!!.getParcelable(Constants.KEY_ACCOUNT) + info = arguments!!.getSerializable(Constants.KEY_COLLECTION_INFO) as CollectionInfo + memberEmail = arguments!!.getString(KEY_MEMBER) + ctx = context + try { + settings = AccountSettings(ctx!!, account!!) + } catch (e: InvalidAccountException) { + e.printStackTrace() + } + + remote = HttpUrl.get(settings!!.uri!!) + + MemberAdd().execute() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val progress = ProgressDialog(context) + progress.setTitle(R.string.collection_members_adding) + progress.setMessage(getString(R.string.please_wait)) + progress.isIndeterminate = true + progress.setCanceledOnTouchOutside(false) + isCancelable = false + return progress + } + + private inner class MemberAdd : AsyncTask() { + override fun doInBackground(vararg voids: Void): AddResult { + try { + val httpClient = HttpClient.create(ctx!!, settings!!) + val userInfoManager = UserInfoManager(httpClient, remote!!) + + val userInfo = userInfoManager[memberEmail!!] + ?: throw Exception(getString(R.string.collection_members_error_user_not_found, memberEmail)) + memberPubKey = userInfo.pubkey + return AddResult(null) + } catch (e: Exception) { + return AddResult(e) + } + + } + + override fun onPostExecute(result: AddResult) { + if (result.throwable == null) { + val fingerprint = Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(memberPubKey!!) + val view = LayoutInflater.from(context).inflate(R.layout.fingerprint_alertdialog, null) + (view.findViewById(R.id.body) as TextView).text = getString(R.string.trust_fingerprint_body, memberEmail) + (view.findViewById(R.id.fingerprint) as TextView).text = fingerprint + AlertDialog.Builder(activity!!) + .setIcon(R.drawable.ic_fingerprint_dark) + .setTitle(R.string.trust_fingerprint_title) + .setView(view) + .setPositiveButton(android.R.string.ok) { dialog, which -> MemberAddSecond().execute() } + .setNegativeButton(android.R.string.cancel) { dialog, which -> dismiss() }.show() + } else { + AlertDialog.Builder(activity!!) + .setIcon(R.drawable.ic_error_dark) + .setTitle(R.string.collection_members_add_error) + .setMessage(result.throwable.message) + .setPositiveButton(android.R.string.yes) { dialog, which -> }.show() + dismiss() + } + } + + internal inner class AddResult(val throwable: Throwable?) + } + + private inner class MemberAddSecond : AsyncTask() { + override fun doInBackground(vararg voids: Void): AddResultSecond { + try { + val httpClient = HttpClient.create(ctx!!, settings!!) + val journalsManager = JournalManager(httpClient, remote!!) + + val journal = JournalManager.Journal.fakeWithUid(info!!.uid) + val crypto = Crypto.CryptoManager(info!!.version, settings!!.password(), info!!.uid) + + val encryptedKey = crypto.getEncryptedKey(settings!!.keyPair!!, memberPubKey!!) + val member = JournalManager.Member(memberEmail!!, encryptedKey!!) + journalsManager.addMember(journal, member) + return AddResultSecond(null) + } catch (e: Exception) { + return AddResultSecond(e) + } + + } + + override fun onPostExecute(result: AddResultSecond) { + if (result.throwable == null) { + (activity as Refreshable).refresh() + } else { + AlertDialog.Builder(activity!!) + .setIcon(R.drawable.ic_error_dark) + .setTitle(R.string.collection_members_add_error) + .setMessage(result.throwable.message) + .setPositiveButton(android.R.string.yes) { dialog, which -> }.show() + } + dismiss() + } + + internal inner class AddResultSecond(val throwable: Throwable?) + } + + companion object { + private val KEY_MEMBER = "memberEmail" + + fun newInstance(account: Account, info: CollectionInfo, email: String): AddMemberFragment { + val frag = AddMemberFragment() + val args = Bundle(1) + args.putParcelable(Constants.KEY_ACCOUNT, account) + args.putSerializable(Constants.KEY_COLLECTION_INFO, info) + args.putString(KEY_MEMBER, email) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AppSettingsActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AppSettingsActivity.java deleted file mode 100644 index 0162ecbd..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/AppSettingsActivity.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v7.preference.EditTextPreference; -import android.support.v7.preference.ListPreference; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceFragmentCompat; -import android.support.v7.preference.SwitchPreferenceCompat; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.ServiceDB; -import com.etesync.syncadapter.model.Settings; -import com.etesync.syncadapter.utils.HintManager; -import com.etesync.syncadapter.utils.LanguageUtils; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Arrays; - -public class AppSettingsActivity extends BaseActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState == null) { - getSupportFragmentManager().beginTransaction() - .replace(android.R.id.content, new SettingsFragment()) - .commit(); - } - } - - - public static class SettingsFragment extends PreferenceFragmentCompat { - ServiceDB.OpenHelper dbHelper; - Settings settings; - - Preference - prefResetHints, - prefResetCertificates; - SwitchPreferenceCompat - prefOverrideProxy, - prefDistrustSystemCerts, - prefLogToExternalStorage; - - EditTextPreference - prefProxyHost, - prefProxyPort; - - @Override - public void onCreate(Bundle savedInstanceState) { - dbHelper = new ServiceDB.OpenHelper(getContext()); - settings = new Settings(dbHelper.getReadableDatabase()); - - super.onCreate(savedInstanceState); - } - - @Override - public void onDestroy() { - super.onDestroy(); - dbHelper.close(); - } - - @Override - public void onCreatePreferences(Bundle bundle, String s) { - addPreferencesFromResource(R.xml.settings_app); - - prefResetHints = findPreference("reset_hints"); - - prefOverrideProxy = (SwitchPreferenceCompat)findPreference("override_proxy"); - prefOverrideProxy.setChecked(settings.getBoolean(App.OVERRIDE_PROXY, false)); - prefOverrideProxy.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - settings.putBoolean(App.OVERRIDE_PROXY, (boolean)newValue); - return true; - } - }); - - prefProxyHost = (EditTextPreference)findPreference("proxy_host"); - String proxyHost = settings.getString(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT); - prefProxyHost.setText(proxyHost); - prefProxyHost.setSummary(proxyHost); - prefProxyHost.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - String host = (String)newValue; - try { - URI uri = new URI(null, host, null, null); - } catch(URISyntaxException e) { - Snackbar.make(getView(), e.getLocalizedMessage(), Snackbar.LENGTH_LONG).show(); - return false; - } - settings.putString(App.OVERRIDE_PROXY_HOST, host); - prefProxyHost.setSummary(host); - return true; - } - }); - - prefProxyPort = (EditTextPreference)findPreference("proxy_port"); - String proxyPort = settings.getString(App.OVERRIDE_PROXY_PORT, String.valueOf(App.OVERRIDE_PROXY_PORT_DEFAULT)); - prefProxyPort.setText(proxyPort); - prefProxyPort.setSummary(proxyPort); - prefProxyPort.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - int port; - try { - port = Integer.parseInt((String)newValue); - } catch(NumberFormatException e) { - port = App.OVERRIDE_PROXY_PORT_DEFAULT; - } - settings.putInt(App.OVERRIDE_PROXY_PORT, port); - prefProxyPort.setText(String.valueOf(port)); - prefProxyPort.setSummary(String.valueOf(port)); - return true; - } - }); - - prefDistrustSystemCerts = (SwitchPreferenceCompat) findPreference("distrust_system_certs"); - prefDistrustSystemCerts.setChecked(settings.getBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, false)); - - prefResetCertificates = findPreference("reset_certificates"); - - prefLogToExternalStorage = (SwitchPreferenceCompat) findPreference("log_to_external_storage"); - prefLogToExternalStorage.setChecked(settings.getBoolean(App.LOG_TO_EXTERNAL_STORAGE, false)); - - initSelectLanguageList(); - } - - private void initSelectLanguageList() { - ListPreference listPreference = (ListPreference) findPreference("select_language"); - new LanguageTask(listPreference).execute(); - } - - @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (preference == prefResetHints) - resetHints(); - else if (preference == prefDistrustSystemCerts) - setDistrustSystemCerts(((SwitchPreferenceCompat)preference).isChecked()); - else if (preference == prefResetCertificates) - resetCertificates(); - else if (preference == prefLogToExternalStorage) - setExternalLogging(((SwitchPreferenceCompat)preference).isChecked()); - else - return false; - return true; - } - - private void resetHints() { - HintManager.resetHints(getContext()); - Snackbar.make(getView(), R.string.app_settings_reset_hints_success, Snackbar.LENGTH_LONG).show(); - } - - private void setDistrustSystemCerts(boolean distrust) { - settings.putBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, distrust); - - // re-initialize certificate manager - App app = (App)getContext().getApplicationContext(); - app.reinitCertManager(); - - // reinitialize certificate manager of :sync process - getContext().sendBroadcast(new Intent(App.ReinitSettingsReceiver.ACTION_REINIT_SETTINGS)); - } - - private void resetCertificates() { - ((App)getContext().getApplicationContext()).getCertManager().resetCertificates(); - Snackbar.make(getView(), getString(R.string.app_settings_reset_certificates_success), Snackbar.LENGTH_LONG).show(); - } - - private void setExternalLogging(boolean externalLogging) { - settings.putBoolean(App.LOG_TO_EXTERNAL_STORAGE, externalLogging); - - // reinitialize logger of default process - App app = (App) getContext().getApplicationContext(); - app.reinitLogger(); - - // reinitialize logger of :sync process - getContext().sendBroadcast(new Intent(App.ReinitSettingsReceiver.ACTION_REINIT_SETTINGS)); - } - - private class LanguageTask extends AsyncTask { - private ListPreference mListPreference; - - LanguageTask(ListPreference listPreference) { - mListPreference = listPreference; - } - - @Override - protected LanguageUtils.LocaleList doInBackground(Void... voids) { - return LanguageUtils.getAppLanguages(getContext()); - - } - - @Override - protected void onPostExecute(LanguageUtils.LocaleList locales) { - - mListPreference.setEntries(locales.getDisplayNames()); - mListPreference.setEntryValues(locales.getLocaleData()); - - mListPreference.setValue(settings.getString(App.FORCE_LANGUAGE, - App.DEFAULT_LANGUAGE)); - mListPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - String value = newValue.toString(); - if (value.equals(((ListPreference) preference).getValue())) return true; - - LanguageUtils.setLanguage(getContext(), value); - - settings.putString(App.FORCE_LANGUAGE, newValue.toString()); - - Intent intent = new Intent(getContext(), AccountsActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - return false; - } - }); - } - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AppSettingsActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AppSettingsActivity.kt new file mode 100644 index 00000000..e3e2a202 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/AppSettingsActivity.kt @@ -0,0 +1,204 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v7.preference.* +import com.etesync.syncadapter.App +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.ServiceDB +import com.etesync.syncadapter.model.Settings +import com.etesync.syncadapter.utils.HintManager +import com.etesync.syncadapter.utils.LanguageUtils +import java.net.URI +import java.net.URISyntaxException + +class AppSettingsActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .replace(android.R.id.content, SettingsFragment()) + .commit() + } + } + + + class SettingsFragment : PreferenceFragmentCompat() { + internal lateinit var dbHelper: ServiceDB.OpenHelper + internal lateinit var settings: Settings + + internal lateinit var prefResetHints: Preference + internal lateinit var prefResetCertificates: Preference + internal lateinit var prefOverrideProxy: SwitchPreferenceCompat + internal lateinit var prefDistrustSystemCerts: SwitchPreferenceCompat + internal lateinit var prefLogToExternalStorage: SwitchPreferenceCompat + + internal lateinit var prefProxyHost: EditTextPreference + internal lateinit var prefProxyPort: EditTextPreference + + override fun onCreate(savedInstanceState: Bundle?) { + dbHelper = ServiceDB.OpenHelper(context) + settings = Settings(dbHelper.readableDatabase) + + super.onCreate(savedInstanceState) + } + + override fun onDestroy() { + super.onDestroy() + dbHelper.close() + } + + override fun onCreatePreferences(bundle: Bundle, s: String) { + addPreferencesFromResource(R.xml.settings_app) + + prefResetHints = findPreference("reset_hints") + + prefOverrideProxy = findPreference("override_proxy") as SwitchPreferenceCompat + prefOverrideProxy.isChecked = settings.getBoolean(App.OVERRIDE_PROXY, false) + prefOverrideProxy.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> + settings.putBoolean(App.OVERRIDE_PROXY, newValue as Boolean) + true + } + + prefProxyHost = findPreference("proxy_host") as EditTextPreference + val proxyHost = settings.getString(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT) + prefProxyHost.text = proxyHost + prefProxyHost.summary = proxyHost + prefProxyHost.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> + val host = newValue as String + try { + val uri = URI(null, host, null, null) + } catch (e: URISyntaxException) { + Snackbar.make(view!!, e.localizedMessage, Snackbar.LENGTH_LONG).show() + return@OnPreferenceChangeListener false + } + + settings.putString(App.OVERRIDE_PROXY_HOST, host) + prefProxyHost.summary = host + true + } + + prefProxyPort = findPreference("proxy_port") as EditTextPreference + val proxyPort = settings.getString(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT.toString()) + prefProxyPort.text = proxyPort + prefProxyPort.summary = proxyPort + prefProxyPort.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> + var port: Int + try { + port = Integer.parseInt(newValue as String) + } catch (e: NumberFormatException) { + port = App.OVERRIDE_PROXY_PORT_DEFAULT + } + + settings.putInt(App.OVERRIDE_PROXY_PORT, port) + prefProxyPort.text = port.toString() + prefProxyPort.summary = port.toString() + true + } + + prefDistrustSystemCerts = findPreference("distrust_system_certs") as SwitchPreferenceCompat + prefDistrustSystemCerts.isChecked = settings.getBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, false) + + prefResetCertificates = findPreference("reset_certificates") + + prefLogToExternalStorage = findPreference("log_to_external_storage") as SwitchPreferenceCompat + prefLogToExternalStorage.isChecked = settings.getBoolean(App.LOG_TO_EXTERNAL_STORAGE, false) + + initSelectLanguageList() + } + + private fun initSelectLanguageList() { + val listPreference = findPreference("select_language") as ListPreference + LanguageTask(listPreference).execute() + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + if (preference === prefResetHints) + resetHints() + else if (preference === prefDistrustSystemCerts) + setDistrustSystemCerts(preference.isChecked) + else if (preference === prefResetCertificates) + resetCertificates() + else if (preference === prefLogToExternalStorage) + setExternalLogging(preference.isChecked) + else + return false + return true + } + + private fun resetHints() { + HintManager.resetHints(context) + Snackbar.make(view!!, R.string.app_settings_reset_hints_success, Snackbar.LENGTH_LONG).show() + } + + private fun setDistrustSystemCerts(distrust: Boolean) { + settings.putBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, distrust) + + // re-initialize certificate manager + val app = context!!.applicationContext as App + app.reinitCertManager() + + // reinitialize certificate manager of :sync process + context!!.sendBroadcast(Intent(App.ReinitSettingsReceiver.ACTION_REINIT_SETTINGS)) + } + + private fun resetCertificates() { + (context!!.applicationContext as App).certManager.resetCertificates() + Snackbar.make(view!!, getString(R.string.app_settings_reset_certificates_success), Snackbar.LENGTH_LONG).show() + } + + private fun setExternalLogging(externalLogging: Boolean) { + settings.putBoolean(App.LOG_TO_EXTERNAL_STORAGE, externalLogging) + + // reinitialize logger of default process + val app = context!!.applicationContext as App + app.reinitLogger() + + // reinitialize logger of :sync process + context!!.sendBroadcast(Intent(App.ReinitSettingsReceiver.ACTION_REINIT_SETTINGS)) + } + + private inner class LanguageTask internal constructor(private val mListPreference: ListPreference) : AsyncTask() { + + override fun doInBackground(vararg voids: Void): LanguageUtils.LocaleList { + return LanguageUtils.getAppLanguages(context!!) + + } + + override fun onPostExecute(locales: LanguageUtils.LocaleList) { + + mListPreference.entries = locales.displayNames + mListPreference.entryValues = locales.localeData + + mListPreference.value = settings.getString(App.FORCE_LANGUAGE, + App.DEFAULT_LANGUAGE) + mListPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> + val value = newValue.toString() + if (value == (preference as ListPreference).value) return@OnPreferenceChangeListener true + + LanguageUtils.setLanguage(context, value) + + settings.putString(App.FORCE_LANGUAGE, newValue.toString()) + + val intent = Intent(context, AccountsActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + false + } + } + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/BaseActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/BaseActivity.java deleted file mode 100644 index 2bc1d6b1..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/BaseActivity.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.etesync.syncadapter.ui; - -import android.support.v7.app.AppCompatActivity; -import android.view.MenuItem; - -import com.etesync.syncadapter.App; - -public class BaseActivity extends AppCompatActivity { - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - if (!getSupportFragmentManager().popBackStackImmediate()) { - finish(); - } - return true; - } - return false; - } - - @Override - protected void onResume() { - super.onResume(); - - App app = (App) getApplicationContext(); - if (app.getCertManager() != null) - app.getCertManager().appInForeground = true; - } - - @Override - protected void onPause() { - super.onPause(); - - App app = (App) getApplicationContext(); - if (app.getCertManager() != null) - app.getCertManager().appInForeground = false; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/BaseActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/BaseActivity.kt new file mode 100644 index 00000000..bb03add9 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/BaseActivity.kt @@ -0,0 +1,34 @@ +package com.etesync.syncadapter.ui + +import android.support.v7.app.AppCompatActivity +import android.view.MenuItem + +import com.etesync.syncadapter.App + +open class BaseActivity : AppCompatActivity() { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + if (!supportFragmentManager.popBackStackImmediate()) { + finish() + } + return true + } + return false + } + + override fun onResume() { + super.onResume() + + val app = applicationContext as App + if (app.certManager != null) + app.certManager.appInForeground = true + } + + override fun onPause() { + super.onPause() + + val app = applicationContext as App + if (app.certManager != null) + app.certManager.appInForeground = false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java deleted file mode 100644 index 42661b12..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.DialogFragment; -import android.support.v7.app.AlertDialog; -import android.text.InputType; -import android.view.MenuItem; -import android.view.View; -import android.widget.EditText; -import android.widget.TextView; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.resource.LocalCalendar; - -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; - -public class CollectionMembersActivity extends BaseActivity implements Refreshable { - public final static String EXTRA_ACCOUNT = "account", - EXTRA_COLLECTION_INFO = "collectionInfo"; - - private Account account; - private JournalEntity journalEntity; - private CollectionMembersListFragment listFragment; - protected CollectionInfo info; - - public static Intent newIntent(Context context, Account account, CollectionInfo info) { - Intent intent = new Intent(context, CollectionMembersActivity.class); - intent.putExtra(CollectionMembersActivity.EXTRA_ACCOUNT, account); - intent.putExtra(CollectionMembersActivity.EXTRA_COLLECTION_INFO, info); - return intent; - } - - @Override - public void refresh() { - EntityDataStore data = ((App) getApplicationContext()).getData(); - - journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid); - if ((journalEntity == null) || journalEntity.isDeleted()) { - finish(); - return; - } - - info = journalEntity.getInfo(); - - setTitle(R.string.collection_members_title); - - final View colorSquare = findViewById(R.id.color); - if (info.type == CollectionInfo.Type.CALENDAR) { - if (info.color != null) { - colorSquare.setBackgroundColor(info.color); - } else { - colorSquare.setBackgroundColor(LocalCalendar.defaultColor); - } - } else { - colorSquare.setVisibility(View.GONE); - } - findViewById(R.id.progressBar).setVisibility(View.GONE); - - final TextView title = (TextView) findViewById(R.id.display_name); - title.setText(info.displayName); - - final TextView desc = (TextView) findViewById(R.id.description); - desc.setText(info.description); - - if (listFragment != null) { - listFragment.refresh(); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.view_collection_members); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - account = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT); - info = (CollectionInfo) getIntent().getExtras().getSerializable(EXTRA_COLLECTION_INFO); - - refresh(); - - // We refresh before this, so we don't refresh the list before it was fully created. - if (savedInstanceState == null) { - listFragment = CollectionMembersListFragment.newInstance(account, info); - getSupportFragmentManager().beginTransaction() - .add(R.id.list_entries_container, listFragment) - .commit(); - } - } - - public void onAddMemberClicked(View v) { - final EditText input = new EditText(this); - input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); - AlertDialog.Builder dialog = new AlertDialog.Builder(this) - .setTitle(R.string.collection_members_add) - .setIcon(R.drawable.ic_account_add_dark) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - DialogFragment frag = AddMemberFragment.newInstance(account, info, input.getText().toString()); - frag.show(getSupportFragmentManager(), null); - } - }) - .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }); - dialog.setView(input); - dialog.show(); - } - - @Override - protected void onResume() { - super.onResume(); - refresh(); - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.kt new file mode 100644 index 00000000..a3200719 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.kt @@ -0,0 +1,113 @@ +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AlertDialog +import android.text.InputType +import android.view.View +import android.widget.EditText +import android.widget.TextView +import com.etesync.syncadapter.App +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.resource.LocalCalendar + +class CollectionMembersActivity : BaseActivity(), Refreshable { + + private lateinit var account: Account + private var journalEntity: JournalEntity? = null + private var listFragment: CollectionMembersListFragment? = null + protected lateinit var info: CollectionInfo + + override fun refresh() { + val data = (applicationContext as App).data + + journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid) + if (journalEntity == null || journalEntity!!.isDeleted) { + finish() + return + } + + info = journalEntity!!.info + + setTitle(R.string.collection_members_title) + + val colorSquare = findViewById(R.id.color) + if (info.type == CollectionInfo.Type.CALENDAR) { + if (info.color != null) { + colorSquare.setBackgroundColor(info.color) + } else { + colorSquare.setBackgroundColor(LocalCalendar.defaultColor) + } + } else { + colorSquare.visibility = View.GONE + } + findViewById(R.id.progressBar).visibility = View.GONE + + val title = findViewById(R.id.display_name) as TextView + title.text = info.displayName + + val desc = findViewById(R.id.description) as TextView + desc.text = info.description + + if (listFragment != null) { + listFragment!!.refresh() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.view_collection_members) + + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + account = intent.extras!!.getParcelable(EXTRA_ACCOUNT) + info = intent.extras!!.getSerializable(EXTRA_COLLECTION_INFO) as CollectionInfo + + refresh() + + // We refresh before this, so we don't refresh the list before it was fully created. + if (savedInstanceState == null) { + listFragment = CollectionMembersListFragment.newInstance(account, info) + supportFragmentManager.beginTransaction() + .add(R.id.list_entries_container, listFragment) + .commit() + } + } + + fun onAddMemberClicked(v: View) { + val input = EditText(this) + input.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + val dialog = AlertDialog.Builder(this) + .setTitle(R.string.collection_members_add) + .setIcon(R.drawable.ic_account_add_dark) + .setPositiveButton(android.R.string.yes) { dialog, which -> + val frag = AddMemberFragment.newInstance(account, info, input.text.toString()) + frag.show(supportFragmentManager, null) + } + .setNegativeButton(android.R.string.no) { dialog, which -> } + dialog.setView(input) + dialog.show() + } + + override fun onResume() { + super.onResume() + refresh() + } + + companion object { + val EXTRA_ACCOUNT = "account" + val EXTRA_COLLECTION_INFO = "collectionInfo" + + fun newIntent(context: Context, account: Account, info: CollectionInfo): Intent { + val intent = Intent(context, CollectionMembersActivity::class.java) + intent.putExtra(CollectionMembersActivity.EXTRA_ACCOUNT, account) + intent.putExtra(CollectionMembersActivity.EXTRA_COLLECTION_INFO, info) + return intent + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java deleted file mode 100644 index 79c34ff6..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.content.Context; -import android.content.DialogInterface; -import android.os.AsyncTask; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.ListFragment; -import android.support.v7.app.AlertDialog; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.TextView; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.HttpClient; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.journalmanager.JournalManager; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.JournalModel; - -import java.util.List; - -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; - -public class CollectionMembersListFragment extends ListFragment implements AdapterView.OnItemClickListener, Refreshable { - private EntityDataStore data; - private Account account; - private CollectionInfo info; - private JournalEntity journalEntity; - private AsyncTask asyncTask; - - private TextView emptyTextView; - - public static CollectionMembersListFragment newInstance(Account account, CollectionInfo info) { - CollectionMembersListFragment frag = new CollectionMembersListFragment(); - Bundle args = new Bundle(1); - args.putParcelable(Constants.KEY_ACCOUNT, account); - args.putSerializable(Constants.KEY_COLLECTION_INFO, info); - frag.setArguments(args); - return frag; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - data = ((App) getContext().getApplicationContext()).getData(); - account = getArguments().getParcelable(Constants.KEY_ACCOUNT); - info = (CollectionInfo) getArguments().getSerializable(Constants.KEY_COLLECTION_INFO); - journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), info.uid); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.collection_members_list, container, false); - - //This is instead of setEmptyText() function because of Google bug - //See: https://code.google.com/p/android/issues/detail?id=21742 - emptyTextView = (TextView) view.findViewById(android.R.id.empty); - return view; - } - - @Override - public void refresh() { - asyncTask = new JournalMembersFetch().execute(); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - refresh(); - - getListView().setOnItemClickListener(this); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - if (asyncTask != null) - asyncTask.cancel(true); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - final JournalManager.Member member = (JournalManager.Member) getListAdapter().getItem(position); - - new AlertDialog.Builder(getActivity()) - .setIcon(R.drawable.ic_info_dark) - .setTitle(R.string.collection_members_remove_title) - .setMessage(getString(R.string.collection_members_remove, member.getUser())) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - DialogFragment frag = RemoveMemberFragment.newInstance(account, info, member.getUser()); - frag.show(getFragmentManager(), null); - } - }) - .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }).show(); - } - - class MembersListAdapter extends ArrayAdapter { - MembersListAdapter(Context context) { - super(context, R.layout.collection_members_list_item); - } - - @Override - @NonNull - public View getView(int position, View v, @NonNull ViewGroup parent) { - if (v == null) - v = LayoutInflater.from(getContext()).inflate(R.layout.collection_members_list_item, parent, false); - - JournalManager.Member member = getItem(position); - - TextView tv = (TextView) v.findViewById(R.id.title); - tv.setText(member.getUser()); - return v; - } - } - - private class JournalMembersFetch extends AsyncTask { - @Override - protected MembersResult doInBackground(Void... voids) { - try { - AccountSettings settings = new AccountSettings(getContext(), account); - OkHttpClient httpClient = HttpClient.create(getContext(), settings); - JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri())); - - JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(journalEntity.getUid()); - return new MembersResult(journalsManager.listMembers(journal), null); - } catch (Exception e) { - return new MembersResult(null, e); - } - } - - @Override - protected void onPostExecute(MembersResult result) { - if (result.throwable == null) { - MembersListAdapter listAdapter = new MembersListAdapter(getContext()); - setListAdapter(listAdapter); - - listAdapter.addAll(result.members); - - emptyTextView.setText(R.string.collection_members_list_empty); - } else { - emptyTextView.setText(result.throwable.getLocalizedMessage()); - } - } - - class MembersResult { - final List members; - final Throwable throwable; - - MembersResult(final List members, final Throwable throwable) { - this.members = members; - this.throwable = throwable; - } - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.kt new file mode 100644 index 00000000..a208b020 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.kt @@ -0,0 +1,139 @@ +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.content.Context +import android.os.AsyncTask +import android.os.Bundle +import android.support.v4.app.ListFragment +import android.support.v7.app.AlertDialog +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.TextView +import com.etesync.syncadapter.* +import com.etesync.syncadapter.journalmanager.JournalManager +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.model.JournalModel +import io.requery.Persistable +import io.requery.sql.EntityDataStore +import okhttp3.HttpUrl + +class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickListener, Refreshable { + private lateinit var data: EntityDataStore + private lateinit var account: Account + private lateinit var info: CollectionInfo + private lateinit var journalEntity: JournalEntity + private var asyncTask: AsyncTask<*, *, *>? = null + + private var emptyTextView: TextView? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + data = (context!!.applicationContext as App).data + account = arguments!!.getParcelable(Constants.KEY_ACCOUNT) + info = arguments!!.getSerializable(Constants.KEY_COLLECTION_INFO) as CollectionInfo + journalEntity = JournalModel.Journal.fetch(data!!, info!!.getServiceEntity(data), info!!.uid) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.collection_members_list, container, false) + + //This is instead of setEmptyText() function because of Google bug + //See: https://code.google.com/p/android/issues/detail?id=21742 + emptyTextView = view.findViewById(android.R.id.empty) as TextView + return view + } + + override fun refresh() { + asyncTask = JournalMembersFetch().execute() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + refresh() + + listView.onItemClickListener = this + } + + override fun onDestroyView() { + super.onDestroyView() + if (asyncTask != null) + asyncTask!!.cancel(true) + } + + override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val member = listAdapter.getItem(position) as JournalManager.Member + + AlertDialog.Builder(activity!!) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.collection_members_remove_title) + .setMessage(getString(R.string.collection_members_remove, member.user)) + .setPositiveButton(android.R.string.yes) { dialog, which -> + val frag = RemoveMemberFragment.newInstance(account, info, member.user!!) + frag.show(fragmentManager!!, null) + } + .setNegativeButton(android.R.string.no) { dialog, which -> }.show() + } + + internal inner class MembersListAdapter(context: Context) : ArrayAdapter(context, R.layout.collection_members_list_item) { + + override fun getView(position: Int, v: View?, parent: ViewGroup): View { + var v = v + if (v == null) + v = LayoutInflater.from(context).inflate(R.layout.collection_members_list_item, parent, false) + + val member = getItem(position) + + val tv = v!!.findViewById(R.id.title) as TextView + tv.text = member!!.user + return v + } + } + + private inner class JournalMembersFetch : AsyncTask() { + override fun doInBackground(vararg voids: Void): MembersResult { + try { + val settings = AccountSettings(context!!, account!!) + val httpClient = HttpClient.create(context!!, settings) + val journalsManager = JournalManager(httpClient, HttpUrl.get(settings.uri!!)!!) + + val journal = JournalManager.Journal.fakeWithUid(journalEntity!!.uid) + return MembersResult(journalsManager.listMembers(journal), null) + } catch (e: Exception) { + return MembersResult(null, e) + } + + } + + override fun onPostExecute(result: MembersResult) { + if (result.throwable == null) { + val listAdapter = MembersListAdapter(context!!) + setListAdapter(listAdapter) + + listAdapter.addAll(result.members) + + emptyTextView!!.setText(R.string.collection_members_list_empty) + } else { + emptyTextView!!.text = result.throwable.localizedMessage + } + } + + internal inner class MembersResult(val members: List?, val throwable: Throwable?) + } + + companion object { + + fun newInstance(account: Account, info: CollectionInfo): CollectionMembersListFragment { + val frag = CollectionMembersListFragment() + val args = Bundle(1) + args.putParcelable(Constants.KEY_ACCOUNT, account) + args.putSerializable(Constants.KEY_COLLECTION_INFO, info) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.java deleted file mode 100644 index 808fc26f..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.content.Context; -import android.content.Intent; -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.support.v4.app.NavUtils; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.EditText; - -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.CollectionInfo; - -import org.apache.commons.lang3.StringUtils; - -import yuku.ambilwarna.AmbilWarnaDialog; - -public class CreateCollectionActivity extends BaseActivity { - static final String EXTRA_ACCOUNT = "account", - EXTRA_COLLECTION_INFO = "collectionInfo"; - - protected Account account; - protected CollectionInfo info; - - public static Intent newIntent(Context context, Account account, CollectionInfo info) { - Intent intent = new Intent(context, CreateCollectionActivity.class); - intent.putExtra(CreateCollectionActivity.EXTRA_ACCOUNT, account); - intent.putExtra(CreateCollectionActivity.EXTRA_COLLECTION_INFO, info); - return intent; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - account = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT); - info = (CollectionInfo) getIntent().getExtras().getSerializable(EXTRA_COLLECTION_INFO); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setContentView(R.layout.activity_create_collection); - - final EditText displayName = (EditText) findViewById(R.id.display_name); - if (info.type == CollectionInfo.Type.CALENDAR) { - setTitle(R.string.create_calendar); - displayName.setHint(R.string.create_calendar_display_name_hint); - - final View colorSquare = findViewById(R.id.color); - colorSquare.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - new AmbilWarnaDialog(CreateCollectionActivity.this, ((ColorDrawable) colorSquare.getBackground()).getColor(), true, new AmbilWarnaDialog.OnAmbilWarnaListener() { - @Override - public void onCancel(AmbilWarnaDialog dialog) { - } - - @Override - public void onOk(AmbilWarnaDialog dialog, int color) { - colorSquare.setBackgroundColor(color); - } - }).show(); - } - }); - } else { - setTitle(R.string.create_addressbook); - displayName.setHint(R.string.create_addressbook_display_name_hint); - - final View colorGroup = findViewById(R.id.color_group); - colorGroup.setVisibility(View.GONE); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.activity_create_collection, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - Intent intent = new Intent(this, AccountActivity.class); - intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account); - NavUtils.navigateUpTo(this, intent); - return true; - } - return false; - } - - public void onCreateCollection(MenuItem item) { - boolean ok = true; - if (info == null) { - info = new CollectionInfo(); - } - - EditText edit = (EditText) findViewById(R.id.display_name); - info.displayName = edit.getText().toString(); - if (TextUtils.isEmpty(info.displayName)) { - edit.setError(getString(R.string.create_collection_display_name_required)); - ok = false; - } - - edit = (EditText) findViewById(R.id.description); - info.description = StringUtils.trimToNull(edit.getText().toString()); - - if (ok) { - if (info.type == CollectionInfo.Type.CALENDAR) { - View view = findViewById(R.id.color); - info.color = ((ColorDrawable) view.getBackground()).getColor(); - } - - info.selected = true; - - CreateCollectionFragment.newInstance(account, info).show(getSupportFragmentManager(), null); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.kt new file mode 100644 index 00000000..a85e7fdd --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.kt @@ -0,0 +1,123 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.content.Context +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.support.v4.app.NavUtils +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.EditText + +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.CollectionInfo + +import org.apache.commons.lang3.StringUtils + +import yuku.ambilwarna.AmbilWarnaDialog + +open class CreateCollectionActivity : BaseActivity() { + + protected lateinit var account: Account + protected lateinit var info: CollectionInfo + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + account = intent.extras!!.getParcelable(EXTRA_ACCOUNT) + info = intent.extras!!.getSerializable(EXTRA_COLLECTION_INFO) as CollectionInfo + + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + setContentView(R.layout.activity_create_collection) + + val displayName = findViewById(R.id.display_name) as EditText + if (info!!.type == CollectionInfo.Type.CALENDAR) { + setTitle(R.string.create_calendar) + displayName.setHint(R.string.create_calendar_display_name_hint) + + val colorSquare = findViewById(R.id.color) + colorSquare.setOnClickListener { + AmbilWarnaDialog(this@CreateCollectionActivity, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener { + override fun onCancel(dialog: AmbilWarnaDialog) {} + + override fun onOk(dialog: AmbilWarnaDialog, color: Int) { + colorSquare.setBackgroundColor(color) + } + }).show() + } + } else { + setTitle(R.string.create_addressbook) + displayName.setHint(R.string.create_addressbook_display_name_hint) + + val colorGroup = findViewById(R.id.color_group) + colorGroup.visibility = View.GONE + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.activity_create_collection, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + val intent = Intent(this, AccountActivity::class.java) + intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account) + NavUtils.navigateUpTo(this, intent) + return true + } + return false + } + + fun onCreateCollection(item: MenuItem) { + var ok = true + if (info == null) { + info = CollectionInfo() + } + + var edit = findViewById(R.id.display_name) as EditText + info!!.displayName = edit.text.toString() + if (TextUtils.isEmpty(info!!.displayName)) { + edit.error = getString(R.string.create_collection_display_name_required) + ok = false + } + + edit = findViewById(R.id.description) as EditText + info!!.description = StringUtils.trimToNull(edit.text.toString()) + + if (ok) { + if (info!!.type == CollectionInfo.Type.CALENDAR) { + val view = findViewById(R.id.color) + info!!.color = (view.background as ColorDrawable).color + } + + info!!.selected = true + + CreateCollectionFragment.newInstance(account, info).show(supportFragmentManager, null) + } + } + + companion object { + internal val EXTRA_ACCOUNT = "account" + internal val EXTRA_COLLECTION_INFO = "collectionInfo" + + fun newIntent(context: Context, account: Account, info: CollectionInfo): Intent { + val intent = Intent(context, CreateCollectionActivity::class.java) + intent.putExtra(CreateCollectionActivity.EXTRA_ACCOUNT, account) + intent.putExtra(CreateCollectionActivity.EXTRA_COLLECTION_INFO, info) + return intent + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.java deleted file mode 100644 index d23fc146..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.app.Activity; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.ContentResolver; -import android.content.Context; -import android.os.Bundle; -import android.provider.CalendarContract; -import android.provider.ContactsContract; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.HttpClient; -import com.etesync.syncadapter.InvalidAccountException; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.journalmanager.Crypto; -import com.etesync.syncadapter.journalmanager.Exceptions; -import com.etesync.syncadapter.journalmanager.JournalManager; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.JournalModel; -import com.etesync.syncadapter.model.ServiceEntity; - -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; -import okhttp3.HttpUrl; - -public class CreateCollectionFragment extends DialogFragment implements LoaderManager.LoaderCallbacks { - private static final String - ARG_ACCOUNT = "account", - ARG_COLLECTION_INFO = "collectionInfo"; - - protected Account account; - protected CollectionInfo info; - - public static CreateCollectionFragment newInstance(Account account, CollectionInfo info) { - CreateCollectionFragment frag = new CreateCollectionFragment(); - Bundle args = new Bundle(2); - args.putParcelable(ARG_ACCOUNT, account); - args.putSerializable(ARG_COLLECTION_INFO, info); - frag.setArguments(args); - return frag; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - account = getArguments().getParcelable(ARG_ACCOUNT); - info = (CollectionInfo) getArguments().getSerializable(ARG_COLLECTION_INFO); - - getLoaderManager().initLoader(0, null, this); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - ProgressDialog progress = new ProgressDialog(getContext()); - progress.setTitle(R.string.create_collection_creating); - progress.setMessage(getString(R.string.please_wait)); - progress.setIndeterminate(true); - progress.setCanceledOnTouchOutside(false); - setCancelable(false); - return progress; - } - - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new CreateCollectionLoader(getContext(), account, info); - } - - @Override - public void onLoadFinished(Loader loader, Exception exception) { - dismissAllowingStateLoss(); - - Activity parent = getActivity(); - if (parent != null) { - if (exception != null) - getFragmentManager().beginTransaction() - .add(ExceptionInfoFragment.newInstance(exception, account), null) - .commitAllowingStateLoss(); - else - parent.finish(); - } - - } - - @Override - public void onLoaderReset(Loader loader) { - } - - - protected static class CreateCollectionLoader extends AsyncTaskLoader { - final Account account; - final CollectionInfo info; - - public CreateCollectionLoader(Context context, Account account, CollectionInfo info) { - super(context); - this.account = account; - this.info = info; - } - - @Override - protected void onStartLoading() { - forceLoad(); - } - - @Override - public Exception loadInBackground() { - try { - String authority = null; - - EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); - - // 1. find service ID - if (info.type == CollectionInfo.Type.ADDRESS_BOOK) { - authority = App.getAddressBooksAuthority(); - } else if (info.type == CollectionInfo.Type.CALENDAR) { - authority = CalendarContract.AUTHORITY; - } else { - throw new IllegalArgumentException("Collection must be an address book or calendar"); - } - - ServiceEntity serviceEntity = JournalModel.Service.fetch(data, account.name, info.type); - - AccountSettings settings = new AccountSettings(getContext(), account); - HttpUrl principal = HttpUrl.get(settings.getUri()); - - JournalManager journalManager = new JournalManager(HttpClient.create(getContext(), settings), principal); - if (info.uid == null) { - info.uid = JournalManager.Journal.genUid(); - Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); - JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.uid); - journalManager.create(journal); - } else { - Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); - JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.uid); - journalManager.update(journal); - } - - // 2. add collection to service - info.serviceID = serviceEntity.getId(); - JournalEntity journalEntity = JournalEntity.fetchOrCreate(data, info); - data.upsert(journalEntity); - - - requestSync(authority); - } catch (IllegalStateException | Exceptions.HttpException e) { - return e; - } catch (InvalidAccountException e) { - return e; - } catch (Exceptions.IntegrityException|Exceptions.GenericCryptoException e) { - return e; - } - - return null; - } - - private void requestSync(String authority) { - Bundle extras = new Bundle(); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); // manual sync - extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); // run immediately (don't queue) - ContentResolver.requestSync(account, authority, extras); - } - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt new file mode 100644 index 00000000..18109eb1 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt @@ -0,0 +1,160 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.app.Dialog +import android.app.ProgressDialog +import android.content.ContentResolver +import android.content.Context +import android.os.Bundle +import android.provider.CalendarContract +import android.support.v4.app.DialogFragment +import android.support.v4.app.LoaderManager +import android.support.v4.content.AsyncTaskLoader +import android.support.v4.content.Loader +import com.etesync.syncadapter.* +import com.etesync.syncadapter.journalmanager.Crypto +import com.etesync.syncadapter.journalmanager.Exceptions +import com.etesync.syncadapter.journalmanager.JournalManager +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.model.JournalModel +import okhttp3.HttpUrl + +class CreateCollectionFragment : DialogFragment(), LoaderManager.LoaderCallbacks { + + protected lateinit var account: Account + protected lateinit var info: CollectionInfo + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + account = arguments!!.getParcelable(ARG_ACCOUNT) + info = arguments!!.getSerializable(ARG_COLLECTION_INFO) as CollectionInfo + + loaderManager.initLoader(0, null, this) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val progress = ProgressDialog(context) + progress.setTitle(R.string.create_collection_creating) + progress.setMessage(getString(R.string.please_wait)) + progress.isIndeterminate = true + progress.setCanceledOnTouchOutside(false) + isCancelable = false + return progress + } + + + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + return CreateCollectionLoader(context!!, account, info) + } + + override fun onLoadFinished(loader: Loader, exception: Exception?) { + dismissAllowingStateLoss() + + val parent = activity + if (parent != null) { + if (exception != null) + fragmentManager!!.beginTransaction() + .add(ExceptionInfoFragment.newInstance(exception, account), null) + .commitAllowingStateLoss() + else + parent.finish() + } + + } + + override fun onLoaderReset(loader: Loader) {} + + + protected class CreateCollectionLoader(context: Context, internal val account: Account, internal val info: CollectionInfo) : AsyncTaskLoader(context) { + + override fun onStartLoading() { + forceLoad() + } + + override fun loadInBackground(): Exception? { + try { + var authority: String + + val data = (context.applicationContext as App).data + + // 1. find service ID + if (info.type == CollectionInfo.Type.ADDRESS_BOOK) { + authority = App.getAddressBooksAuthority() + } else if (info.type == CollectionInfo.Type.CALENDAR) { + authority = CalendarContract.AUTHORITY + } else { + throw IllegalArgumentException("Collection must be an address book or calendar") + } + + val serviceEntity = JournalModel.Service.fetch(data, account.name, info.type) + + val settings = AccountSettings(context, account) + val principal = HttpUrl.get(settings.uri!!) + + val journalManager = JournalManager(HttpClient.create(context, settings), principal!!) + if (info.uid == null) { + info.uid = JournalManager.Journal.genUid() + val crypto = Crypto.CryptoManager(info.version, settings.password(), info.uid) + val journal = JournalManager.Journal(crypto, info.toJson(), info.uid) + journalManager.create(journal) + } else { + val crypto = Crypto.CryptoManager(info.version, settings.password(), info.uid) + val journal = JournalManager.Journal(crypto, info.toJson(), info.uid) + journalManager.update(journal) + } + + // 2. add collection to service + info.serviceID = serviceEntity.id + val journalEntity = JournalEntity.fetchOrCreate(data, info) + data.upsert(journalEntity) + + + requestSync(authority) + } catch (e: IllegalStateException) { + return e + } catch (e: Exceptions.HttpException) { + return e + } catch (e: InvalidAccountException) { + return e + } catch (e: Exceptions.IntegrityException) { + return e + } catch (e: Exceptions.GenericCryptoException) { + return e + } + + return null + } + + private fun requestSync(authority: String) { + val extras = Bundle() + extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync + extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue) + ContentResolver.requestSync(account, authority, extras) + } + } + + companion object { + private val ARG_ACCOUNT = "account" + private val ARG_COLLECTION_INFO = "collectionInfo" + + fun newInstance(account: Account, info: CollectionInfo): CreateCollectionFragment { + val frag = CreateCollectionFragment() + val args = Bundle(2) + args.putParcelable(ARG_ACCOUNT, account) + args.putSerializable(ARG_COLLECTION_INFO, info) + frag.arguments = args + return frag + } + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.java deleted file mode 100644 index 49db012c..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.Manifest; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.annotation.SuppressLint; -import android.app.LoaderManager; -import android.content.AsyncTaskLoader; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.Loader; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.os.PowerManager; -import android.provider.CalendarContract; -import android.provider.ContactsContract; -import android.support.v4.content.ContextCompat; -import android.support.v4.content.FileProvider; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.TextView; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.BuildConfig; -import com.etesync.syncadapter.InvalidAccountException; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.journalmanager.Exceptions.HttpException; -import com.etesync.syncadapter.model.EntryEntity; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.ServiceDB; -import com.etesync.syncadapter.model.ServiceEntity; -import com.etesync.syncadapter.resource.LocalAddressBook; - -import org.acra.ACRA; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.commons.lang3.text.WordUtils; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Date; -import java.util.List; -import java.util.logging.Level; - -import at.bitfire.vcard4android.ContactsStorageException; -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; - -import static com.etesync.syncadapter.Constants.KEY_ACCOUNT; - -public class DebugInfoActivity extends BaseActivity implements LoaderManager.LoaderCallbacks { - public static final String - KEY_THROWABLE = "throwable", - KEY_LOGS = "logs", - KEY_AUTHORITY = "authority", - KEY_PHASE = "phase"; - - TextView tvReport; - String report; - - File reportFile; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_debug_info); - tvReport = (TextView)findViewById(R.id.text_report); - - getLoaderManager().initLoader(0, getIntent().getExtras(), this); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.activity_debug_info, menu); - return true; - } - - - public void onShare(MenuItem item) { - ACRA.getErrorReporter().putCustomData("debug_info", report); - ACRA.getErrorReporter().handleSilentException(null); - ACRA.getErrorReporter().removeCustomData("debug_info"); - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new ReportLoader(this, args); - } - - @Override - public void onLoadFinished(Loader loader, String data) { - if (data != null) - tvReport.setText(report = data); - } - - @Override - public void onLoaderReset(Loader loader) { - } - - - static class ReportLoader extends AsyncTaskLoader { - - final Bundle extras; - - public ReportLoader(Context context, Bundle extras) { - super(context); - this.extras = extras; - } - - @Override - protected void onStartLoading() { - forceLoad(); - } - - @Override - @SuppressLint("MissingPermission") - public String loadInBackground() { - Throwable throwable = null; - String logs = null, - authority = null; - Account account = null; - String phase = null; - - if (extras != null) { - throwable = (Throwable)extras.getSerializable(KEY_THROWABLE); - logs = extras.getString(KEY_LOGS); - account = extras.getParcelable(KEY_ACCOUNT); - authority = extras.getString(KEY_AUTHORITY); - phase = extras.getString(KEY_PHASE, null); - } - - StringBuilder report = new StringBuilder("--- BEGIN DEBUG INFO ---\n"); - - // begin with most specific information - - if (phase != null) - report.append("SYNCHRONIZATION INFO\nSynchronization phase: ").append(phase).append("\n"); - if (account != null) - report.append("Account name: ").append(account.name).append("\n"); - if (authority != null) - report.append("Authority: ").append(authority).append("\n"); - - if (throwable instanceof HttpException) { - HttpException http = (HttpException)throwable; - if (http.getRequest() != null) - report.append("\nHTTP REQUEST:\n").append(http.getRequest()).append("\n\n"); - if (http.getRequest() != null) - report.append("HTTP RESPONSE:\n").append(http.getRequest()).append("\n"); - } - - if (throwable != null) - report .append("\nEXCEPTION:\n") - .append(ExceptionUtils.getStackTrace(throwable)); - - if (logs != null) - report.append("\nLOGS:\n").append(logs).append("\n"); - - final Context context = getContext(); - - try { - PackageManager pm = context.getPackageManager(); - String installedFrom = pm.getInstallerPackageName(BuildConfig.APPLICATION_ID); - if (TextUtils.isEmpty(installedFrom)) - installedFrom = "APK (directly)"; - report.append("\nSOFTWARE INFORMATION\n" + - "EteSync version: ").append(BuildConfig.VERSION_NAME).append(" (").append(BuildConfig.VERSION_CODE).append(") ").append(new Date(BuildConfig.buildTime)).append("\n") - .append("Installed from: ").append(installedFrom).append("\n"); - } catch(Exception ex) { - App.log.log(Level.SEVERE, "Couldn't get software information", ex); - } - - report.append("CONFIGURATION\n"); - // power saving - PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE); - if (powerManager != null && Build.VERSION.SDK_INT >= 23) - report.append("Power saving disabled: ") - .append(powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) ? "yes" : "no") - .append("\n"); - // permissions - for (String permission : new String[] { Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, - Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR, - PermissionsActivity.PERMISSION_READ_TASKS, PermissionsActivity.PERMISSION_WRITE_TASKS }) - report.append(permission).append(" permission: ") - .append(ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED ? "granted" : "denied") - .append("\n"); - // system-wide sync settings - report.append("System-wide synchronization: ") - .append(ContentResolver.getMasterSyncAutomatically() ? "automatically" : "manually") - .append("\n"); - // main accounts - AccountManager accountManager = AccountManager.get(context); - for (Account acct : accountManager.getAccountsByType(context.getString(R.string.account_type))) - try { - AccountSettings settings = new AccountSettings(context, acct); - report.append("Account: ").append(acct.name).append("\n" + - " Address book sync. interval: ").append(syncStatus(settings, App.getAddressBooksAuthority())).append("\n" + - " Calendar sync. interval: ").append(syncStatus(settings, CalendarContract.AUTHORITY)).append("\n" + - " OpenTasks sync. interval: ").append(syncStatus(settings, "org.dmfs.tasks")).append("\n" + - " WiFi only: ").append(settings.getSyncWifiOnly()); - if (settings.getSyncWifiOnlySSID() != null) - report.append(", SSID: ").append(settings.getSyncWifiOnlySSID()); - report.append("\n [CardDAV] Contact group method: ").append(settings.getGroupMethod()) - .append("\n Manage calendar colors: ").append(settings.getManageCalendarColors()) - .append("\n"); - } catch(InvalidAccountException e) { - report.append(acct).append(" is invalid (unsupported settings version) or does not exist\n"); - } - // address book accounts - for (Account acct : accountManager.getAccountsByType(App.getAddressBookAccountType())) - try { - LocalAddressBook addressBook = new LocalAddressBook(context, acct, null); - report.append("Address book account: ").append(acct.name).append("\n" + - " Main account: ").append(addressBook.getMainAccount()).append("\n" + - " URL: ").append(addressBook.getURL()).append("\n" + - " Sync automatically: ").append(ContentResolver.getSyncAutomatically(acct, ContactsContract.AUTHORITY)).append("\n"); - } catch(ContactsStorageException e) { - report.append(acct).append(" is invalid: ").append(e.getMessage()).append("\n"); - } - report.append("\n"); - - report.append("SQLITE DUMP\n"); - ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(context); - dbHelper.dump(report); - dbHelper.close(); - report.append("\n"); - - report.append("SERVICES DUMP\n"); - EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); - for (ServiceEntity serviceEntity : data.select(ServiceEntity.class).get()) { - report.append(serviceEntity.toString() + "\n"); - } - report.append("\n"); - - report.append("JOURNALS DUMP\n"); - List journals = data.select(JournalEntity.class).where(JournalEntity.DELETED.eq(false)).get().toList(); - for (JournalEntity journal : journals) { - report.append(journal.toString() + "\n"); - int entryCount = data.count(EntryEntity.class).where(EntryEntity.JOURNAL.eq(journal)).get().value(); - report.append("\tEntries: " + String.valueOf(entryCount) + "\n\n"); - } - report.append("\n"); - - try { - report.append( - "SYSTEM INFORMATION\n" + - "Android version: ").append(Build.VERSION.RELEASE).append(" (").append(Build.DISPLAY).append(")\n" + - "Device: ").append(WordUtils.capitalize(Build.MANUFACTURER)).append(" ").append(Build.MODEL).append(" (").append(Build.DEVICE).append(")\n\n" - ); - } catch(Exception ex) { - App.log.log(Level.SEVERE, "Couldn't get system details", ex); - } - - report.append("--- END DEBUG INFO ---\n"); - return report.toString(); - } - - protected String syncStatus(AccountSettings settings, String authority) { - Long interval = settings.getSyncInterval(authority); - return interval != null ? - (interval == AccountSettings.SYNC_INTERVAL_MANUALLY ? "manually" : interval/60 + " min") : - "—"; - } - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt new file mode 100644 index 00000000..fea488e1 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt @@ -0,0 +1,242 @@ +/* + * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.Manifest +import android.accounts.Account +import android.accounts.AccountManager +import android.annotation.SuppressLint +import android.app.LoaderManager +import android.content.AsyncTaskLoader +import android.content.ContentResolver +import android.content.Context +import android.content.Loader +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.PowerManager +import android.provider.CalendarContract +import android.provider.ContactsContract +import android.support.v4.content.ContextCompat +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import android.widget.TextView +import at.bitfire.vcard4android.ContactsStorageException +import com.etesync.syncadapter.* +import com.etesync.syncadapter.Constants.KEY_ACCOUNT +import com.etesync.syncadapter.journalmanager.Exceptions.HttpException +import com.etesync.syncadapter.model.EntryEntity +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.model.ServiceDB +import com.etesync.syncadapter.model.ServiceEntity +import com.etesync.syncadapter.resource.LocalAddressBook +import org.acra.ACRA +import org.apache.commons.lang3.exception.ExceptionUtils +import org.apache.commons.lang3.text.WordUtils +import java.io.File +import java.util.* +import java.util.logging.Level + +class DebugInfoActivity : BaseActivity(), LoaderManager.LoaderCallbacks { + + internal lateinit var tvReport: TextView + internal lateinit var report: String + + internal var reportFile: File? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_debug_info) + tvReport = findViewById(R.id.text_report) + + loaderManager.initLoader(0, intent.extras, this) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.activity_debug_info, menu) + return true + } + + + fun onShare(item: MenuItem) { + ACRA.getErrorReporter().putCustomData("debug_info", report) + ACRA.getErrorReporter().handleSilentException(null) + ACRA.getErrorReporter().removeCustomData("debug_info") + } + + override fun onCreateLoader(id: Int, args: Bundle): Loader { + return ReportLoader(this, args) + } + + override fun onLoadFinished(loader: Loader, data: String?) { + if (data != null) { + report = data + tvReport.setText(report) + } + } + + override fun onLoaderReset(loader: Loader) {} + + + internal class ReportLoader(context: Context, val extras: Bundle?) : AsyncTaskLoader(context) { + + override fun onStartLoading() { + forceLoad() + } + + @SuppressLint("MissingPermission") + override fun loadInBackground(): String { + var throwable: Throwable? = null + var logs: String? = null + var authority: String? = null + var account: Account? = null + var phase: String? = null + + if (extras != null) { + throwable = extras.getSerializable(KEY_THROWABLE) as Throwable + logs = extras.getString(KEY_LOGS) + account = extras.getParcelable(KEY_ACCOUNT) + authority = extras.getString(KEY_AUTHORITY) + phase = extras.getString(KEY_PHASE, null) + } + + val report = StringBuilder("--- BEGIN DEBUG INFO ---\n") + + // begin with most specific information + + if (phase != null) + report.append("SYNCHRONIZATION INFO\nSynchronization phase: ").append(phase).append("\n") + if (account != null) + report.append("Account name: ").append(account.name).append("\n") + if (authority != null) + report.append("Authority: ").append(authority).append("\n") + + if (throwable is HttpException) { + val http = throwable as HttpException? + if (http!!.request != null) + report.append("\nHTTP REQUEST:\n").append(http.request).append("\n\n") + if (http.request != null) + report.append("HTTP RESPONSE:\n").append(http.request).append("\n") + } + + if (throwable != null) + report.append("\nEXCEPTION:\n") + .append(ExceptionUtils.getStackTrace(throwable)) + + if (logs != null) + report.append("\nLOGS:\n").append(logs).append("\n") + + val context = context + + try { + val pm = context.packageManager + var installedFrom = pm.getInstallerPackageName(BuildConfig.APPLICATION_ID) + if (TextUtils.isEmpty(installedFrom)) + installedFrom = "APK (directly)" + report.append("\nSOFTWARE INFORMATION\n" + "EteSync version: ").append(BuildConfig.VERSION_NAME).append(" (").append(BuildConfig.VERSION_CODE).append(") ").append(Date(BuildConfig.buildTime)).append("\n") + .append("Installed from: ").append(installedFrom).append("\n") + } catch (ex: Exception) { + App.log.log(Level.SEVERE, "Couldn't get software information", ex) + } + + report.append("CONFIGURATION\n") + // power saving + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + if (powerManager != null && Build.VERSION.SDK_INT >= 23) + report.append("Power saving disabled: ") + .append(if (powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) "yes" else "no") + .append("\n") + // permissions + for (permission in arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR, PermissionsActivity.PERMISSION_READ_TASKS, PermissionsActivity.PERMISSION_WRITE_TASKS)) + report.append(permission).append(" permission: ") + .append(if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) "granted" else "denied") + .append("\n") + // system-wide sync settings + report.append("System-wide synchronization: ") + .append(if (ContentResolver.getMasterSyncAutomatically()) "automatically" else "manually") + .append("\n") + // main accounts + val accountManager = AccountManager.get(context) + for (acct in accountManager.getAccountsByType(context.getString(R.string.account_type))) + try { + val settings = AccountSettings(context, acct) + report.append("Account: ").append(acct.name).append("\n" + " Address book sync. interval: ").append(syncStatus(settings, App.getAddressBooksAuthority())).append("\n" + " Calendar sync. interval: ").append(syncStatus(settings, CalendarContract.AUTHORITY)).append("\n" + " OpenTasks sync. interval: ").append(syncStatus(settings, "org.dmfs.tasks")).append("\n" + " WiFi only: ").append(settings.syncWifiOnly) + if (settings.syncWifiOnlySSID != null) + report.append(", SSID: ").append(settings.syncWifiOnlySSID) + report.append("\n [CardDAV] Contact group method: ").append(settings.groupMethod) + .append("\n Manage calendar colors: ").append(settings.manageCalendarColors) + .append("\n") + } catch (e: InvalidAccountException) { + report.append(acct).append(" is invalid (unsupported settings version) or does not exist\n") + } + + // address book accounts + for (acct in accountManager.getAccountsByType(App.getAddressBookAccountType())) + try { + val addressBook = LocalAddressBook(context, acct, null) + report.append("Address book account: ").append(acct.name).append("\n" + " Main account: ").append(addressBook.mainAccount).append("\n" + " URL: ").append(addressBook.url).append("\n" + " Sync automatically: ").append(ContentResolver.getSyncAutomatically(acct, ContactsContract.AUTHORITY)).append("\n") + } catch (e: ContactsStorageException) { + report.append(acct).append(" is invalid: ").append(e.message).append("\n") + } + + report.append("\n") + + report.append("SQLITE DUMP\n") + val dbHelper = ServiceDB.OpenHelper(context) + dbHelper.dump(report) + dbHelper.close() + report.append("\n") + + report.append("SERVICES DUMP\n") + val data = (getContext().applicationContext as App).data + for (serviceEntity in data.select(ServiceEntity::class.java).get()) { + report.append(serviceEntity.toString() + "\n") + } + report.append("\n") + + report.append("JOURNALS DUMP\n") + val journals = data.select(JournalEntity::class.java).where(JournalEntity.DELETED.eq(false)).get().toList() + for (journal in journals) { + report.append(journal.toString() + "\n") + val entryCount = data.count(EntryEntity::class.java).where(EntryEntity.JOURNAL.eq(journal)).get().value() + report.append("\tEntries: " + entryCount.toString() + "\n\n") + } + report.append("\n") + + try { + report.append( + "SYSTEM INFORMATION\n" + "Android version: ").append(Build.VERSION.RELEASE).append(" (").append(Build.DISPLAY).append(")\n" + "Device: ").append(WordUtils.capitalize(Build.MANUFACTURER)).append(" ").append(Build.MODEL).append(" (").append(Build.DEVICE).append(")\n\n" + ) + } catch (ex: Exception) { + App.log.log(Level.SEVERE, "Couldn't get system details", ex) + } + + report.append("--- END DEBUG INFO ---\n") + return report.toString() + } + + protected fun syncStatus(settings: AccountSettings, authority: String): String { + val interval = settings.getSyncInterval(authority) + return if (interval != null) + if (interval == AccountSettings.SYNC_INTERVAL_MANUALLY) "manually" else (interval / 60).toString() + " min" + else + "—" + } + } + + companion object { + val KEY_THROWABLE = "throwable" + val KEY_LOGS = "logs" + val KEY_AUTHORITY = "authority" + val KEY_PHASE = "phase" + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.java deleted file mode 100644 index 20606422..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.app.Activity; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; -import android.support.v7.app.AlertDialog; -import android.text.TextUtils; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.HttpClient; -import com.etesync.syncadapter.InvalidAccountException; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.journalmanager.Crypto; -import com.etesync.syncadapter.journalmanager.Exceptions; -import com.etesync.syncadapter.journalmanager.JournalManager; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.JournalEntity; - -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; -import okhttp3.HttpUrl; - -public class DeleteCollectionFragment extends DialogFragment implements LoaderManager.LoaderCallbacks { - protected static final String - ARG_ACCOUNT = "account", - ARG_COLLECTION_INFO = "collectionInfo"; - - protected Account account; - protected CollectionInfo collectionInfo; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getLoaderManager().initLoader(0, getArguments(), this); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - ProgressDialog progress = new ProgressDialog(getContext()); - progress.setTitle(R.string.delete_collection_deleting_collection); - progress.setMessage(getString(R.string.please_wait)); - progress.setIndeterminate(true); - progress.setCanceledOnTouchOutside(false); - setCancelable(false); - return progress; - } - - - @Override - public Loader onCreateLoader(int id, Bundle args) { - account = args.getParcelable(ARG_ACCOUNT); - collectionInfo = (CollectionInfo) args.getSerializable(ARG_COLLECTION_INFO); - return new DeleteCollectionLoader(getContext(), account, collectionInfo); - } - - @Override - public void onLoadFinished(Loader loader, Exception exception) { - dismissAllowingStateLoss(); - - if (exception != null) - getFragmentManager().beginTransaction() - .add(ExceptionInfoFragment.newInstance(exception, account), null) - .commitAllowingStateLoss(); - else { - Activity activity = getActivity(); - if (activity instanceof Refreshable) - ((Refreshable) activity).refresh(); - else if (activity instanceof EditCollectionActivity) - activity.finish(); - } - } - - @Override - public void onLoaderReset(Loader loader) { - } - - - protected static class DeleteCollectionLoader extends AsyncTaskLoader { - final Account account; - final CollectionInfo collectionInfo; - - public DeleteCollectionLoader(Context context, Account account, CollectionInfo collectionInfo) { - super(context); - this.account = account; - this.collectionInfo = collectionInfo; - } - - @Override - protected void onStartLoading() { - forceLoad(); - } - - @Override - public Exception loadInBackground() { - try { - // delete collection locally - EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); - - AccountSettings settings = new AccountSettings(getContext(), account); - HttpUrl principal = HttpUrl.get(settings.getUri()); - - JournalManager journalManager = new JournalManager(HttpClient.create(getContext(), settings), principal); - Crypto.CryptoManager crypto = new Crypto.CryptoManager(collectionInfo.version, settings.password(), collectionInfo.uid); - - journalManager.delete(new JournalManager.Journal(crypto, collectionInfo.toJson(), collectionInfo.uid)); - JournalEntity journalEntity = JournalEntity.fetch(data, collectionInfo.getServiceEntity(data), collectionInfo.uid); - journalEntity.setDeleted(true); - data.update(journalEntity); - - return null; - } catch (Exceptions.HttpException|Exceptions.IntegrityException|Exceptions.GenericCryptoException e) { - return e; - } catch (InvalidAccountException e) { - return e; - } - } - } - - - public static class ConfirmDeleteCollectionFragment extends DialogFragment { - - public static ConfirmDeleteCollectionFragment newInstance(Account account, CollectionInfo collectionInfo) { - ConfirmDeleteCollectionFragment frag = new ConfirmDeleteCollectionFragment(); - Bundle args = new Bundle(2); - args.putParcelable(ARG_ACCOUNT, account); - args.putSerializable(ARG_COLLECTION_INFO, collectionInfo); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - CollectionInfo collectionInfo = (CollectionInfo) getArguments().getSerializable(ARG_COLLECTION_INFO); - String name = TextUtils.isEmpty(collectionInfo.displayName) ? collectionInfo.uid : collectionInfo.displayName; - - return new AlertDialog.Builder(getContext()) - .setTitle(R.string.delete_collection_confirm_title) - .setMessage(getString(R.string.delete_collection_confirm_warning, name)) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - DialogFragment frag = new DeleteCollectionFragment(); - frag.setArguments(getArguments()); - frag.show(getFragmentManager(), null); - } - }) - .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dismiss(); - } - }) - .create(); - } - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.kt new file mode 100644 index 00000000..423b9bb0 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.kt @@ -0,0 +1,150 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.app.Dialog +import android.app.ProgressDialog +import android.content.Context +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.support.v4.app.LoaderManager +import android.support.v4.content.AsyncTaskLoader +import android.support.v4.content.Loader +import android.support.v7.app.AlertDialog +import android.text.TextUtils +import com.etesync.syncadapter.* +import com.etesync.syncadapter.journalmanager.Crypto +import com.etesync.syncadapter.journalmanager.Exceptions +import com.etesync.syncadapter.journalmanager.JournalManager +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import okhttp3.HttpUrl + +class DeleteCollectionFragment : DialogFragment(), LoaderManager.LoaderCallbacks { + + protected lateinit var account: Account + protected lateinit var collectionInfo: CollectionInfo + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + loaderManager.initLoader(0, arguments, this) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val progress = ProgressDialog(context) + progress.setTitle(R.string.delete_collection_deleting_collection) + progress.setMessage(getString(R.string.please_wait)) + progress.isIndeterminate = true + progress.setCanceledOnTouchOutside(false) + isCancelable = false + return progress + } + + + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + account = args!!.getParcelable(ARG_ACCOUNT) + collectionInfo = args.getSerializable(ARG_COLLECTION_INFO) as CollectionInfo + return DeleteCollectionLoader(context!!, account, collectionInfo) + } + + override fun onLoadFinished(loader: Loader, exception: Exception?) { + dismissAllowingStateLoss() + + if (exception != null) + fragmentManager!!.beginTransaction() + .add(ExceptionInfoFragment.newInstance(exception, account), null) + .commitAllowingStateLoss() + else { + val activity = activity + if (activity is Refreshable) + (activity as Refreshable).refresh() + else if (activity is EditCollectionActivity) + activity.finish() + } + } + + override fun onLoaderReset(loader: Loader) {} + + + protected class DeleteCollectionLoader(context: Context, internal val account: Account, internal val collectionInfo: CollectionInfo) : AsyncTaskLoader(context) { + + override fun onStartLoading() { + forceLoad() + } + + override fun loadInBackground(): Exception? { + try { + // delete collection locally + val data = (context.applicationContext as App).data + + val settings = AccountSettings(context, account) + val principal = HttpUrl.get(settings.uri!!) + + val journalManager = JournalManager(HttpClient.create(context, settings), principal!!) + val crypto = Crypto.CryptoManager(collectionInfo.version, settings.password(), collectionInfo.uid) + + journalManager.delete(JournalManager.Journal(crypto, collectionInfo.toJson(), collectionInfo.uid)) + val journalEntity = JournalEntity.fetch(data, collectionInfo.getServiceEntity(data), collectionInfo.uid) + journalEntity!!.isDeleted = true + data.update(journalEntity) + + return null + } catch (e: Exceptions.HttpException) { + return e + } catch (e: Exceptions.IntegrityException) { + return e + } catch (e: Exceptions.GenericCryptoException) { + return e + } catch (e: InvalidAccountException) { + return e + } + + } + } + + + class ConfirmDeleteCollectionFragment : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val collectionInfo = arguments!!.getSerializable(ARG_COLLECTION_INFO) as CollectionInfo + val name = if (TextUtils.isEmpty(collectionInfo.displayName)) collectionInfo.uid else collectionInfo.displayName + + return AlertDialog.Builder(context!!) + .setTitle(R.string.delete_collection_confirm_title) + .setMessage(getString(R.string.delete_collection_confirm_warning, name)) + .setPositiveButton(android.R.string.yes) { dialog, which -> + val frag = DeleteCollectionFragment() + frag.arguments = arguments + frag.show(fragmentManager!!, null) + } + .setNegativeButton(android.R.string.no) { dialog, which -> dismiss() } + .create() + } + + companion object { + + fun newInstance(account: Account, collectionInfo: CollectionInfo): ConfirmDeleteCollectionFragment { + val frag = ConfirmDeleteCollectionFragment() + val args = Bundle(2) + args.putParcelable(ARG_ACCOUNT, account) + args.putSerializable(ARG_COLLECTION_INFO, collectionInfo) + frag.arguments = args + return frag + } + } + } + + companion object { + protected val ARG_ACCOUNT = "account" + protected val ARG_COLLECTION_INFO = "collectionInfo" + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.java deleted file mode 100644 index 9bc3a4bb..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.EditText; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.resource.LocalCalendar; - -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; - -public class EditCollectionActivity extends CreateCollectionActivity { - public static Intent newIntent(Context context, Account account, CollectionInfo info) { - Intent intent = new Intent(context, EditCollectionActivity.class); - intent.putExtra(CreateCollectionActivity.EXTRA_ACCOUNT, account); - intent.putExtra(CreateCollectionActivity.EXTRA_COLLECTION_INFO, info); - return intent; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setTitle(R.string.edit_collection); - - if (info.type == CollectionInfo.Type.CALENDAR) { - final View colorSquare = findViewById(R.id.color); - if (info.color != null) { - colorSquare.setBackgroundColor(info.color); - } else { - colorSquare.setBackgroundColor(LocalCalendar.defaultColor); - } - } - - final EditText edit = (EditText) findViewById(R.id.display_name); - edit.setText(info.displayName); - - final EditText desc = (EditText) findViewById(R.id.description); - desc.setText(info.description); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.activity_edit_collection, menu); - return true; - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - if (getParent() instanceof Refreshable) { - ((Refreshable) getParent()).refresh(); - } - } - - public void onDeleteCollection(MenuItem item) { - EntityDataStore data = ((App) getApplication()).getData(); - int journalCount = data.count(JournalEntity.class).where(JournalEntity.SERVICE_MODEL.eq(info.getServiceEntity(data))).get().value(); - - if (journalCount < 2) { - new AlertDialog.Builder(this) - .setIcon(R.drawable.ic_error_dark) - .setTitle(R.string.account_delete_collection_last_title) - .setMessage(R.string.account_delete_collection_last_text) - .setPositiveButton(android.R.string.ok, null) - .show(); - } else { - DeleteCollectionFragment.ConfirmDeleteCollectionFragment.newInstance(account, info).show(getSupportFragmentManager(), null); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.kt new file mode 100644 index 00000000..a5f6f013 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.kt @@ -0,0 +1,86 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AlertDialog +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.EditText +import com.etesync.syncadapter.App +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.resource.LocalCalendar + +class EditCollectionActivity : CreateCollectionActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setTitle(R.string.edit_collection) + + if (info!!.type == CollectionInfo.Type.CALENDAR) { + val colorSquare = findViewById(R.id.color) + if (info!!.color != null) { + colorSquare.setBackgroundColor(info!!.color) + } else { + colorSquare.setBackgroundColor(LocalCalendar.defaultColor) + } + } + + val edit = findViewById(R.id.display_name) as EditText + edit.setText(info!!.displayName) + + val desc = findViewById(R.id.description) as EditText + desc.setText(info!!.description) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.activity_edit_collection, menu) + return true + } + + override fun onDestroy() { + super.onDestroy() + + if (parent is Refreshable) { + (parent as Refreshable).refresh() + } + } + + fun onDeleteCollection(item: MenuItem) { + val data = (application as App).data + val journalCount = data.count(JournalEntity::class.java).where(JournalEntity.SERVICE_MODEL.eq(info!!.getServiceEntity(data))).get().value() + + if (journalCount < 2) { + AlertDialog.Builder(this) + .setIcon(R.drawable.ic_error_dark) + .setTitle(R.string.account_delete_collection_last_title) + .setMessage(R.string.account_delete_collection_last_text) + .setPositiveButton(android.R.string.ok, null) + .show() + } else { + DeleteCollectionFragment.ConfirmDeleteCollectionFragment.newInstance(account, info).show(supportFragmentManager, null) + } + } + + companion object { + fun newIntent(context: Context, account: Account, info: CollectionInfo): Intent { + val intent = Intent(context, EditCollectionActivity::class.java) + intent.putExtra(CreateCollectionActivity.EXTRA_ACCOUNT, account) + intent.putExtra(CreateCollectionActivity.EXTRA_COLLECTION_INFO, info) + return intent + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ExceptionInfoFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/ExceptionInfoFragment.java deleted file mode 100644 index d1bb05ac..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/ExceptionInfoFragment.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v7.app.AlertDialog; - -import java.io.IOException; - -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.journalmanager.Exceptions.HttpException; - -public class ExceptionInfoFragment extends DialogFragment { - protected static final String - ARG_ACCOUNT = "account", - ARG_EXCEPTION = "exception"; - - public static ExceptionInfoFragment newInstance(@NonNull Exception exception, Account account) { - ExceptionInfoFragment frag = new ExceptionInfoFragment(); - Bundle args = new Bundle(1); - args.putSerializable(ARG_EXCEPTION, exception); - args.putParcelable(ARG_ACCOUNT, account); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Bundle args = getArguments(); - final Exception exception = (Exception)args.getSerializable(ARG_EXCEPTION); - final Account account = args.getParcelable(ARG_ACCOUNT); - - int title = R.string.exception; - if (exception instanceof HttpException) - title = R.string.exception_httpexception; - else if (exception instanceof IOException) - title = R.string.exception_ioexception; - - Dialog dialog = new AlertDialog.Builder(getContext()) - .setIcon(R.drawable.ic_error_dark) - .setTitle(title) - .setMessage(exception.getClass().getCanonicalName() + "\n" + exception.getLocalizedMessage()) - .setNegativeButton(R.string.exception_show_details, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(getContext(), DebugInfoActivity.class); - intent.putExtra(DebugInfoActivity.KEY_THROWABLE, exception); - if (account != null) - intent.putExtra(Constants.KEY_ACCOUNT, account); - startActivity(intent); - } - }) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }) - .create(); - setCancelable(false); - return dialog; - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ExceptionInfoFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/ExceptionInfoFragment.kt new file mode 100644 index 00000000..084435ee --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/ExceptionInfoFragment.kt @@ -0,0 +1,65 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.app.Dialog +import android.content.Intent +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.support.v7.app.AlertDialog +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.journalmanager.Exceptions.HttpException +import java.io.IOException + +class ExceptionInfoFragment : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val args = arguments + val exception = args!!.getSerializable(ARG_EXCEPTION) as Exception + val account = args.getParcelable(ARG_ACCOUNT) + + var title = R.string.exception + if (exception is HttpException) + title = R.string.exception_httpexception + else if (exception is IOException) + title = R.string.exception_ioexception + + val dialog = AlertDialog.Builder(context!!) + .setIcon(R.drawable.ic_error_dark) + .setTitle(title) + .setMessage(exception.javaClass.canonicalName + "\n" + exception.localizedMessage) + .setNegativeButton(R.string.exception_show_details) { dialog, which -> + val intent = Intent(context, DebugInfoActivity::class.java) + intent.putExtra(DebugInfoActivity.KEY_THROWABLE, exception) + if (account != null) + intent.putExtra(Constants.KEY_ACCOUNT, account) + startActivity(intent) + } + .setPositiveButton(android.R.string.ok) { dialog, which -> } + .create() + isCancelable = false + return dialog + } + + companion object { + protected val ARG_ACCOUNT = "account" + protected val ARG_EXCEPTION = "exception" + + fun newInstance(exception: Exception, account: Account): ExceptionInfoFragment { + val frag = ExceptionInfoFragment() + val args = Bundle(1) + args.putSerializable(ARG_EXCEPTION, exception) + args.putParcelable(ARG_ACCOUNT, account) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.java deleted file mode 100644 index 5c7f2330..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.java +++ /dev/null @@ -1,475 +0,0 @@ -package com.etesync.syncadapter.ui; - -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; -import android.support.design.widget.TabLayout; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.SyncEntry; - -import net.fortuna.ical4j.model.component.VAlarm; -import net.fortuna.ical4j.model.property.Attendee; - -import org.apache.commons.codec.Charsets; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.Formatter; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.Locale; - -import at.bitfire.ical4android.Event; -import at.bitfire.ical4android.InvalidCalendarException; -import at.bitfire.vcard4android.Contact; -import at.bitfire.vcard4android.LabeledProperty; -import ezvcard.parameter.AddressType; -import ezvcard.parameter.EmailType; -import ezvcard.parameter.RelatedType; -import ezvcard.parameter.TelephoneType; -import ezvcard.property.Address; -import ezvcard.property.Email; -import ezvcard.property.Impp; -import ezvcard.property.Related; -import ezvcard.property.Telephone; -import ezvcard.property.Url; -import ezvcard.util.PartialDate; -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; - -import static com.etesync.syncadapter.ui.journalviewer.ListEntriesFragment.setJournalEntryView; - -public class JournalItemActivity extends BaseActivity implements Refreshable { - private static final String KEY_SYNC_ENTRY = "syncEntry"; - private JournalEntity journalEntity; - protected CollectionInfo info; - private SyncEntry syncEntry; - - public static Intent newIntent(Context context, CollectionInfo info, SyncEntry syncEntry) { - Intent intent = new Intent(context, JournalItemActivity.class); - intent.putExtra(Constants.KEY_COLLECTION_INFO, info); - intent.putExtra(KEY_SYNC_ENTRY, syncEntry); - return intent; - } - - @Override - public void refresh() { - EntityDataStore data = ((App) getApplicationContext()).getData(); - - journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid); - if ((journalEntity == null) || journalEntity.isDeleted()) { - finish(); - return; - } - - info = journalEntity.getInfo(); - - setTitle(info.displayName); - - setJournalEntryView(findViewById(R.id.journal_list_item), info, syncEntry); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.journal_item_activity); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - info = (CollectionInfo) getIntent().getExtras().getSerializable(Constants.KEY_COLLECTION_INFO); - syncEntry = (SyncEntry) getIntent().getExtras().getSerializable(KEY_SYNC_ENTRY); - - refresh(); - - ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager); - viewPager.setAdapter(new TabsAdapter(getSupportFragmentManager(), this, info, syncEntry)); - - TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); - tabLayout.setupWithViewPager(viewPager); - } - - private static class TabsAdapter extends FragmentPagerAdapter { - private Context context; - private CollectionInfo info; - private SyncEntry syncEntry; - public TabsAdapter(FragmentManager fm, Context context, CollectionInfo info, SyncEntry syncEntry) { - super(fm); - this.context = context; - this.info = info; - this.syncEntry = syncEntry; - } - - @Override - public int getCount() { - // FIXME: Make it depend on info type (only have non-raw for known types) - return 2; - } - - @Override - public CharSequence getPageTitle(int position) { - if (position == 0) { - return context.getString(R.string.journal_item_tab_main); - } else { - return context.getString(R.string.journal_item_tab_raw); - } - } - - @Override - public Fragment getItem(int position) { - if (position == 0) { - return PrettyFragment.newInstance(info, syncEntry); - } else { - return TextFragment.newInstance(syncEntry); - } - } - } - - public static class TextFragment extends Fragment { - public static TextFragment newInstance(SyncEntry syncEntry) { - TextFragment frag = new TextFragment(); - Bundle args = new Bundle(1); - args.putSerializable(KEY_SYNC_ENTRY, syncEntry); - frag.setArguments(args); - return frag; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.text_fragment, container, false); - - TextView tv = (TextView) v.findViewById(R.id.content); - - SyncEntry syncEntry = (SyncEntry) getArguments().getSerializable(KEY_SYNC_ENTRY); - tv.setText(syncEntry.getContent()); - - return v; - } - } - - public static class PrettyFragment extends Fragment { - CollectionInfo info; - SyncEntry syncEntry; - private AsyncTask asyncTask; - - public static PrettyFragment newInstance(CollectionInfo info, SyncEntry syncEntry) { - PrettyFragment frag = new PrettyFragment(); - Bundle args = new Bundle(1); - args.putSerializable(Constants.KEY_COLLECTION_INFO, info); - args.putSerializable(KEY_SYNC_ENTRY, syncEntry); - frag.setArguments(args); - return frag; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = null; - - info = (CollectionInfo) getArguments().getSerializable(Constants.KEY_COLLECTION_INFO); - syncEntry = (SyncEntry) getArguments().getSerializable(KEY_SYNC_ENTRY); - - switch (info.type) { - case ADDRESS_BOOK: - v = inflater.inflate(R.layout.contact_info, container, false); - asyncTask = new LoadContactTask(v).execute(); - break; - case CALENDAR: - v = inflater.inflate(R.layout.event_info, container, false); - asyncTask = new LoadEventTask(v).execute(); - break; - } - - return v; - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - if (asyncTask != null) - asyncTask.cancel(true); - } - - private class LoadEventTask extends AsyncTask { - View view; - LoadEventTask(View v) { - super(); - view = v; - } - @Override - protected Event doInBackground(Void... aVoids) { - InputStream is = new ByteArrayInputStream(syncEntry.getContent().getBytes(Charsets.UTF_8)); - - try { - return Event.fromStream(is, Charsets.UTF_8, null)[0]; - } catch (InvalidCalendarException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - return null; - } - - @Override - protected void onPostExecute(Event event) { - final View loader = view.findViewById(R.id.event_info_loading_msg); - loader.setVisibility(View.GONE); - final View contentContainer = view.findViewById(R.id.event_info_scroll_view); - contentContainer.setVisibility(View.VISIBLE); - - setTextViewText(view, R.id.title, event.summary); - - setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(event.dtStart.getDate().getTime(), event.dtEnd.getDate().getTime(), event.isAllDay(), getContext())); - - setTextViewText(view, R.id.where, event.location); - - if (event.organizer != null) { - TextView tv = (TextView) view.findViewById(R.id.organizer); - tv.setText(event.organizer.getCalAddress().toString().replaceFirst("mailto:", "")); - } else { - View organizer = view.findViewById(R.id.organizer_container); - organizer.setVisibility(View.GONE); - } - - setTextViewText(view, R.id.description, event.description); - - boolean first = true; - StringBuilder sb = new StringBuilder(); - for (Attendee attendee : event.attendees) { - if (first) { - first = false; - sb.append(getString(R.string.journal_item_attendees)).append(": "); - } else { - sb.append(", "); - } - sb.append(attendee.getCalAddress().toString().replaceFirst("mailto:", "")); - } - setTextViewText(view, R.id.attendees, sb.toString()); - - first = true; - sb = new StringBuilder(); - for (VAlarm alarm : event.alarms) { - if (first) { - first = false; - sb.append(getString(R.string.journal_item_reminders)).append(": "); - } else { - sb.append(", "); - } - sb.append(alarm.getTrigger().getValue()); - } - setTextViewText(view, R.id.reminders, sb.toString()); - } - } - - private class LoadContactTask extends AsyncTask { - View view; - - LoadContactTask(View v) { - super(); - view = v; - } - - @Override - protected Contact doInBackground(Void... aVoids) { - InputStream is = new ByteArrayInputStream(syncEntry.getContent().getBytes(Charsets.UTF_8)); - - try { - return Contact.fromStream(is, Charsets.UTF_8, null)[0]; - } catch (IOException e) { - e.printStackTrace(); - } - - return null; - } - - @Override - protected void onPostExecute(Contact contact) { - final View loader = view.findViewById(R.id.loading_msg); - loader.setVisibility(View.GONE); - final View contentContainer = view.findViewById(R.id.content_container); - contentContainer.setVisibility(View.VISIBLE); - - TextView tv = (TextView) view.findViewById(R.id.display_name); - tv.setText(contact.displayName); - - if (contact.group) { - showGroup(contact); - } else { - showContact(contact); - } - } - - private void showGroup(Contact contact) { - final ViewGroup mainCard = (ViewGroup) view.findViewById(R.id.main_card); - - addInfoItem(view.getContext(), mainCard, getString(R.string.journal_item_member_count), null, String.valueOf(contact.members.size())); - - for (String member : contact.members) { - addInfoItem(view.getContext(), mainCard, getString(R.string.journal_item_member), null, member); - } - } - - - private void showContact(Contact contact) { - final ViewGroup mainCard = (ViewGroup) view.findViewById(R.id.main_card); - final ViewGroup aboutCard = (ViewGroup) view.findViewById(R.id.about_card); - aboutCard.findViewById(R.id.title_container).setVisibility(View.VISIBLE); - - // TEL - for (LabeledProperty labeledPhone : contact.phoneNumbers) { - List types = labeledPhone.property.getTypes(); - String type = (types.size() > 0) ? types.get(0).getValue() : null; - addInfoItem(view.getContext(), mainCard, getString(R.string.journal_item_phone), type, labeledPhone.property.getText()); - } - - // EMAIL - for (LabeledProperty labeledEmail : contact.emails) { - List types = labeledEmail.property.getTypes(); - String type = (types.size() > 0) ? types.get(0).getValue() : null; - addInfoItem(view.getContext(), mainCard, getString(R.string.journal_item_email), type, labeledEmail.property.getValue()); - } - - // ORG, TITLE, ROLE - if (contact.organization != null) { - addInfoItem(view.getContext(), aboutCard, getString(R.string.journal_item_organization), contact.jobTitle, contact.organization.getValues().get(0)); - } - if (contact.jobDescription != null) { - addInfoItem(view.getContext(), aboutCard, getString(R.string.journal_item_job_description), null, contact.jobTitle); - } - - // IMPP - for (LabeledProperty labeledImpp : contact.impps) { - addInfoItem(view.getContext(), mainCard, getString(R.string.journal_item_impp), labeledImpp.property.getProtocol(), labeledImpp.property.getHandle()); - } - - // NICKNAME - if (contact.nickName != null && contact.nickName.getValues().size() > 0) { - addInfoItem(view.getContext(), aboutCard, getString(R.string.journal_item_nickname), null, contact.nickName.getValues().get(0)); - } - - // ADR - for (LabeledProperty
labeledAddress : contact.addresses) { - List types = labeledAddress.property.getTypes(); - String type = (types.size() > 0) ? types.get(0).getValue() : null; - addInfoItem(view.getContext(), mainCard, getString(R.string.journal_item_address), type, labeledAddress.property.getLabel()); - } - - // NOTE - if (contact.note != null) { - addInfoItem(view.getContext(), aboutCard, getString(R.string.journal_item_note), null, contact.note); - } - - // URL - for (LabeledProperty labeledUrl : contact.urls) { - addInfoItem(view.getContext(), aboutCard, getString(R.string.journal_item_website), null, labeledUrl.property.getValue()); - } - - // ANNIVERSARY - if (contact.anniversary != null) { - addInfoItem(view.getContext(), aboutCard, getString(R.string.journal_item_anniversary), null, getDisplayedDate(contact.anniversary.getDate(), contact.anniversary.getPartialDate())); - } - // BDAY - if (contact.birthDay != null) { - addInfoItem(view.getContext(), aboutCard, getString(R.string.journal_item_birthday), null, getDisplayedDate(contact.birthDay.getDate(), contact.birthDay.getPartialDate())); - } - - // RELATED - for (Related related : contact.relations) { - List types = related.getTypes(); - String type = (types.size() > 0) ? types.get(0).getValue() : null; - addInfoItem(view.getContext(), aboutCard, getString(R.string.journal_item_relation), type, related.getText()); - } - - // PHOTO - // if (contact.photo != null) - } - } - - private String getDisplayedDate(Date date, PartialDate partialDate) { - if (date != null) { - long epochDate = date.getTime(); - return getDisplayedDatetime(epochDate, epochDate, true, getContext()); - } else { - SimpleDateFormat formatter = new SimpleDateFormat("d MMMM", Locale.getDefault()); - GregorianCalendar calendar = new GregorianCalendar(); - calendar.set(Calendar.DAY_OF_MONTH, partialDate.getDate()); - calendar.set(Calendar.MONTH, partialDate.getMonth() - 1); - return formatter.format(calendar.getTime()); - } - } - - private static View addInfoItem(Context context, ViewGroup parent, String type, String label, String value) { - ViewGroup layout = (ViewGroup) parent.findViewById(R.id.container); - View infoItem = LayoutInflater.from(context).inflate(R.layout.contact_info_item, layout, false); - layout.addView(infoItem); - setTextViewText(infoItem, R.id.type, type); - setTextViewText(infoItem, R.id.title, label); - setTextViewText(infoItem, R.id.content, value); - parent.setVisibility(View.VISIBLE); - - return infoItem; - } - - private static void setTextViewText(View parent, int id, String text) { - TextView tv = (TextView) parent.findViewById(id); - if (text == null) { - tv.setVisibility(View.GONE); - } else { - tv.setText(text); - } - } - - public static String getDisplayedDatetime(long startMillis, long endMillis, boolean allDay, Context context) { - // Configure date/time formatting. - int flagsDate = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY; - int flagsTime = DateUtils.FORMAT_SHOW_TIME; - if (DateFormat.is24HourFormat(context)) { - flagsTime |= DateUtils.FORMAT_24HOUR; - } - - String datetimeString = null; - if (allDay) { - // For multi-day allday events or single-day all-day events that are not - // today or tomorrow, use framework formatter. - Formatter f = new Formatter(new StringBuilder(50), Locale.getDefault()); - datetimeString = DateUtils.formatDateRange(context, f, startMillis, - endMillis, flagsDate, Time.TIMEZONE_UTC).toString(); - } else { - // For multiday events, shorten day/month names. - // Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm" - int flagsDatetime = flagsDate | flagsTime | DateUtils.FORMAT_ABBREV_MONTH | - DateUtils.FORMAT_ABBREV_WEEKDAY; - datetimeString = DateUtils.formatDateRange(context, startMillis, endMillis, - flagsDatetime); - } - return datetimeString; - } - } - - @Override - protected void onResume() { - super.onResume(); - refresh(); - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt new file mode 100644 index 00000000..300dde4c --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt @@ -0,0 +1,424 @@ +package com.etesync.syncadapter.ui + +import android.content.Context +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.support.design.widget.TabLayout +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentPagerAdapter +import android.support.v4.view.ViewPager +import android.text.format.DateFormat +import android.text.format.DateUtils +import android.text.format.Time +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.InvalidCalendarException +import at.bitfire.vcard4android.Contact +import com.etesync.syncadapter.App +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.model.SyncEntry +import com.etesync.syncadapter.ui.journalviewer.ListEntriesFragment.Companion.setJournalEntryView +import ezvcard.util.PartialDate +import org.apache.commons.codec.Charsets +import java.io.ByteArrayInputStream +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* + +class JournalItemActivity : BaseActivity(), Refreshable { + private var journalEntity: JournalEntity? = null + protected lateinit var info: CollectionInfo + private lateinit var syncEntry: SyncEntry + + override fun refresh() { + val data = (applicationContext as App).data + + journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid) + if (journalEntity == null || journalEntity!!.isDeleted) { + finish() + return + } + + info = journalEntity!!.info + + title = info.displayName + + setJournalEntryView(findViewById(R.id.journal_list_item), info, syncEntry) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.journal_item_activity) + + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + info = intent.extras!!.getSerializable(Constants.KEY_COLLECTION_INFO) as CollectionInfo + syncEntry = intent.extras!!.getSerializable(KEY_SYNC_ENTRY) as SyncEntry + + refresh() + + val viewPager = findViewById(R.id.viewpager) as ViewPager + viewPager.adapter = TabsAdapter(supportFragmentManager, this, info, syncEntry) + + val tabLayout = findViewById(R.id.tabs) as TabLayout + tabLayout.setupWithViewPager(viewPager) + } + + private class TabsAdapter(fm: FragmentManager, private val context: Context, private val info: CollectionInfo, private val syncEntry: SyncEntry) : FragmentPagerAdapter(fm) { + + override fun getCount(): Int { + // FIXME: Make it depend on info type (only have non-raw for known types) + return 2 + } + + override fun getPageTitle(position: Int): CharSequence? { + return if (position == 0) { + context.getString(R.string.journal_item_tab_main) + } else { + context.getString(R.string.journal_item_tab_raw) + } + } + + override fun getItem(position: Int): Fragment { + return if (position == 0) { + PrettyFragment.newInstance(info, syncEntry) + } else { + TextFragment.newInstance(syncEntry) + } + } + } + + class TextFragment : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val v = inflater.inflate(R.layout.text_fragment, container, false) + + val tv = v.findViewById(R.id.content) as TextView + + val syncEntry = arguments!!.getSerializable(KEY_SYNC_ENTRY) as SyncEntry + tv.text = syncEntry.content + + return v + } + + companion object { + fun newInstance(syncEntry: SyncEntry): TextFragment { + val frag = TextFragment() + val args = Bundle(1) + args.putSerializable(KEY_SYNC_ENTRY, syncEntry) + frag.arguments = args + return frag + } + } + } + + class PrettyFragment : Fragment() { + internal lateinit var info: CollectionInfo + internal lateinit var syncEntry: SyncEntry + private var asyncTask: AsyncTask<*, *, *>? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + var v: View? = null + + info = arguments!!.getSerializable(Constants.KEY_COLLECTION_INFO) as CollectionInfo + syncEntry = arguments!!.getSerializable(KEY_SYNC_ENTRY) as SyncEntry + + when (info.type) { + CollectionInfo.Type.ADDRESS_BOOK -> { + v = inflater.inflate(R.layout.contact_info, container, false) + asyncTask = LoadContactTask(v).execute() + } + CollectionInfo.Type.CALENDAR -> { + v = inflater.inflate(R.layout.event_info, container, false) + asyncTask = LoadEventTask(v).execute() + } + } + + return v + } + + override fun onDestroyView() { + super.onDestroyView() + if (asyncTask != null) + asyncTask!!.cancel(true) + } + + private inner class LoadEventTask internal constructor(internal var view: View) : AsyncTask() { + override fun doInBackground(vararg aVoids: Void): Event? { + val `is` = ByteArrayInputStream(syncEntry.content.toByteArray(Charsets.UTF_8)) + + try { + return Event.fromStream(`is`, Charsets.UTF_8, null)[0] + } catch (e: InvalidCalendarException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + + return null + } + + override fun onPostExecute(event: Event) { + val loader = view.findViewById(R.id.event_info_loading_msg) + loader.visibility = View.GONE + val contentContainer = view.findViewById(R.id.event_info_scroll_view) + contentContainer.visibility = View.VISIBLE + + setTextViewText(view, R.id.title, event.summary) + + setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(event.dtStart.date.time, event.dtEnd.date.time, event.isAllDay, context)) + + setTextViewText(view, R.id.where, event.location) + + if (event.organizer != null) { + val tv = view.findViewById(R.id.organizer) as TextView + tv.text = event.organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "") + } else { + val organizer = view.findViewById(R.id.organizer_container) + organizer.visibility = View.GONE + } + + setTextViewText(view, R.id.description, event.description) + + var first = true + var sb = StringBuilder() + for (attendee in event.attendees) { + if (first) { + first = false + sb.append(getString(R.string.journal_item_attendees)).append(": ") + } else { + sb.append(", ") + } + sb.append(attendee.calAddress.toString().replaceFirst("mailto:".toRegex(), "")) + } + setTextViewText(view, R.id.attendees, sb.toString()) + + first = true + sb = StringBuilder() + for (alarm in event.alarms) { + if (first) { + first = false + sb.append(getString(R.string.journal_item_reminders)).append(": ") + } else { + sb.append(", ") + } + sb.append(alarm.trigger.value) + } + setTextViewText(view, R.id.reminders, sb.toString()) + } + } + + private inner class LoadContactTask internal constructor(internal var view: View) : AsyncTask() { + + override fun doInBackground(vararg aVoids: Void): Contact? { + val `is` = ByteArrayInputStream(syncEntry.content.toByteArray(Charsets.UTF_8)) + + try { + return Contact.fromStream(`is`, Charsets.UTF_8, null)[0] + } catch (e: IOException) { + e.printStackTrace() + } + + return null + } + + override fun onPostExecute(contact: Contact) { + val loader = view.findViewById(R.id.loading_msg) + loader.visibility = View.GONE + val contentContainer = view.findViewById(R.id.content_container) + contentContainer.visibility = View.VISIBLE + + val tv = view.findViewById(R.id.display_name) as TextView + tv.text = contact.displayName + + if (contact.group) { + showGroup(contact) + } else { + showContact(contact) + } + } + + private fun showGroup(contact: Contact) { + val mainCard = view.findViewById(R.id.main_card) as ViewGroup + + addInfoItem(view.context, mainCard, getString(R.string.journal_item_member_count), null, contact.members.size.toString()) + + for (member in contact.members) { + addInfoItem(view.context, mainCard, getString(R.string.journal_item_member), null, member) + } + } + + + private fun showContact(contact: Contact) { + val mainCard = view.findViewById(R.id.main_card) as ViewGroup + val aboutCard = view.findViewById(R.id.about_card) as ViewGroup + aboutCard.findViewById(R.id.title_container).visibility = View.VISIBLE + + // TEL + for (labeledPhone in contact.phoneNumbers) { + val types = labeledPhone.property.types + val type = if (types.size > 0) types[0].value else null + addInfoItem(view.context, mainCard, getString(R.string.journal_item_phone), type, labeledPhone.property.text) + } + + // EMAIL + for (labeledEmail in contact.emails) { + val types = labeledEmail.property.types + val type = if (types.size > 0) types[0].value else null + addInfoItem(view.context, mainCard, getString(R.string.journal_item_email), type, labeledEmail.property.value) + } + + // ORG, TITLE, ROLE + if (contact.organization != null) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_organization), contact.jobTitle, contact.organization.values[0]) + } + if (contact.jobDescription != null) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_job_description), null, contact.jobTitle) + } + + // IMPP + for (labeledImpp in contact.impps) { + addInfoItem(view.context, mainCard, getString(R.string.journal_item_impp), labeledImpp.property.protocol, labeledImpp.property.handle) + } + + // NICKNAME + if (contact.nickName != null && contact.nickName.values.size > 0) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_nickname), null, contact.nickName.values[0]) + } + + // ADR + for (labeledAddress in contact.addresses) { + val types = labeledAddress.property.types + val type = if (types.size > 0) types[0].value else null + addInfoItem(view.context, mainCard, getString(R.string.journal_item_address), type, labeledAddress.property.label) + } + + // NOTE + if (contact.note != null) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_note), null, contact.note) + } + + // URL + for (labeledUrl in contact.urls) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_website), null, labeledUrl.property.value) + } + + // ANNIVERSARY + if (contact.anniversary != null) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_anniversary), null, getDisplayedDate(contact.anniversary.date, contact.anniversary.partialDate)) + } + // BDAY + if (contact.birthDay != null) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_birthday), null, getDisplayedDate(contact.birthDay.date, contact.birthDay.partialDate)) + } + + // RELATED + for (related in contact.relations) { + val types = related.types + val type = if (types.size > 0) types[0].value else null + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_relation), type, related.text) + } + + // PHOTO + // if (contact.photo != null) + } + } + + private fun getDisplayedDate(date: Date?, partialDate: PartialDate): String? { + if (date != null) { + val epochDate = date.time + return getDisplayedDatetime(epochDate, epochDate, true, context) + } else { + val formatter = SimpleDateFormat("d MMMM", Locale.getDefault()) + val calendar = GregorianCalendar() + calendar.set(Calendar.DAY_OF_MONTH, partialDate.date!!) + calendar.set(Calendar.MONTH, partialDate.month!! - 1) + return formatter.format(calendar.time) + } + } + + companion object { + + fun newInstance(info: CollectionInfo, syncEntry: SyncEntry): PrettyFragment { + val frag = PrettyFragment() + val args = Bundle(1) + args.putSerializable(Constants.KEY_COLLECTION_INFO, info) + args.putSerializable(KEY_SYNC_ENTRY, syncEntry) + frag.arguments = args + return frag + } + + private fun addInfoItem(context: Context, parent: ViewGroup, type: String, label: String?, value: String?): View { + val layout = parent.findViewById(R.id.container) as ViewGroup + val infoItem = LayoutInflater.from(context).inflate(R.layout.contact_info_item, layout, false) + layout.addView(infoItem) + setTextViewText(infoItem, R.id.type, type) + setTextViewText(infoItem, R.id.title, label) + setTextViewText(infoItem, R.id.content, value) + parent.visibility = View.VISIBLE + + return infoItem + } + + private fun setTextViewText(parent: View, id: Int, text: String?) { + val tv = parent.findViewById(id) as TextView + if (text == null) { + tv.visibility = View.GONE + } else { + tv.text = text + } + } + + fun getDisplayedDatetime(startMillis: Long, endMillis: Long, allDay: Boolean, context: Context?): String? { + // Configure date/time formatting. + val flagsDate = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY + var flagsTime = DateUtils.FORMAT_SHOW_TIME + if (DateFormat.is24HourFormat(context)) { + flagsTime = flagsTime or DateUtils.FORMAT_24HOUR + } + + var datetimeString: String? = null + if (allDay) { + // For multi-day allday events or single-day all-day events that are not + // today or tomorrow, use framework formatter. + val f = Formatter(StringBuilder(50), Locale.getDefault()) + datetimeString = DateUtils.formatDateRange(context, f, startMillis, + endMillis, flagsDate, Time.TIMEZONE_UTC).toString() + } else { + // For multiday events, shorten day/month names. + // Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm" + val flagsDatetime = flagsDate or flagsTime or DateUtils.FORMAT_ABBREV_MONTH or + DateUtils.FORMAT_ABBREV_WEEKDAY + datetimeString = DateUtils.formatDateRange(context, startMillis, endMillis, + flagsDatetime) + } + return datetimeString + } + } + } + + override fun onResume() { + super.onResume() + refresh() + } + + companion object { + private val KEY_SYNC_ENTRY = "syncEntry" + + fun newIntent(context: Context, info: CollectionInfo, syncEntry: SyncEntry): Intent { + val intent = Intent(context, JournalItemActivity::class.java) + intent.putExtra(Constants.KEY_COLLECTION_INFO, info) + intent.putExtra(KEY_SYNC_ENTRY, syncEntry) + return intent + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.java deleted file mode 100644 index aa9b4a43..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.Manifest; -import android.app.Activity; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.NotificationManagerCompat; -import android.view.View; - -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.resource.LocalTaskList; - -public class PermissionsActivity extends BaseActivity { - final static private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124; - - public static final String - PERMISSION_READ_TASKS = "org.dmfs.permission.READ_TASKS", - PERMISSION_WRITE_TASKS = "org.dmfs.permission.WRITE_TASKS"; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_permissions); -} - - @Override - protected void onResume() { - super.onResume(); - refresh(); - } - - protected void refresh() { - boolean noCalendarPermissions = - ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED || - ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED; - findViewById(R.id.calendar_permissions).setVisibility(noCalendarPermissions ? View.VISIBLE : View.GONE); - - boolean noContactsPermissions = - ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED || - ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED; - findViewById(R.id.contacts_permissions).setVisibility(noContactsPermissions ? View.VISIBLE : View.GONE); - - boolean noTaskPermissions; - if (LocalTaskList.tasksProviderAvailable(this)) { - noTaskPermissions = - ActivityCompat.checkSelfPermission(this, PERMISSION_READ_TASKS) != PackageManager.PERMISSION_GRANTED || - ActivityCompat.checkSelfPermission(this, PERMISSION_WRITE_TASKS) != PackageManager.PERMISSION_GRANTED; - findViewById(R.id.opentasks_permissions).setVisibility(noTaskPermissions ? View.VISIBLE : View.GONE); - } else { - findViewById(R.id.opentasks_permissions).setVisibility(View.GONE); - noTaskPermissions = false; - } - - if (!noCalendarPermissions && !noContactsPermissions && !noTaskPermissions) { - NotificationManagerCompat nm = NotificationManagerCompat.from(this); - nm.cancel(Constants.NOTIFICATION_PERMISSIONS); - - finish(); - } - } - - public void requestCalendarPermissions(View v) { - ActivityCompat.requestPermissions(this, new String[] { - Manifest.permission.READ_CALENDAR, - Manifest.permission.WRITE_CALENDAR - }, 0); - } - - public void requestContactsPermissions(View v) { - ActivityCompat.requestPermissions(this, new String[] { - Manifest.permission.READ_CONTACTS, - Manifest.permission.WRITE_CONTACTS - }, 0); - } - - public void requestOpenTasksPermissions(View v) { - ActivityCompat.requestPermissions(this, new String[] { - PERMISSION_READ_TASKS, - PERMISSION_WRITE_TASKS - }, 0); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - refresh(); - } - - public static void requestAllPermissions(Activity activity) { - ActivityCompat.requestPermissions(activity, new String[] { - Manifest.permission.READ_CALENDAR, - Manifest.permission.WRITE_CALENDAR, - Manifest.permission.READ_CONTACTS, - Manifest.permission.WRITE_CONTACTS - }, REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS); - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.kt new file mode 100644 index 00000000..d616f85e --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.kt @@ -0,0 +1,86 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import android.os.Bundle +import android.support.v4.app.ActivityCompat +import android.support.v4.app.NotificationManagerCompat +import android.view.View + +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.resource.LocalTaskList + +class PermissionsActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_permissions) + } + + override fun onResume() { + super.onResume() + refresh() + } + + protected fun refresh() { + val noCalendarPermissions = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED + findViewById(R.id.calendar_permissions).visibility = if (noCalendarPermissions) View.VISIBLE else View.GONE + + val noContactsPermissions = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED + findViewById(R.id.contacts_permissions).visibility = if (noContactsPermissions) View.VISIBLE else View.GONE + + val noTaskPermissions: Boolean + if (LocalTaskList.tasksProviderAvailable(this)) { + noTaskPermissions = ActivityCompat.checkSelfPermission(this, PERMISSION_READ_TASKS) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, PERMISSION_WRITE_TASKS) != PackageManager.PERMISSION_GRANTED + findViewById(R.id.opentasks_permissions).visibility = if (noTaskPermissions) View.VISIBLE else View.GONE + } else { + findViewById(R.id.opentasks_permissions).visibility = View.GONE + noTaskPermissions = false + } + + if (!noCalendarPermissions && !noContactsPermissions && !noTaskPermissions) { + val nm = NotificationManagerCompat.from(this) + nm.cancel(Constants.NOTIFICATION_PERMISSIONS) + + finish() + } + } + + fun requestCalendarPermissions(v: View) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR), 0) + } + + fun requestContactsPermissions(v: View) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS), 0) + } + + fun requestOpenTasksPermissions(v: View) { + ActivityCompat.requestPermissions(this, arrayOf(PERMISSION_READ_TASKS, PERMISSION_WRITE_TASKS), 0) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + refresh() + } + + companion object { + private val REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124 + + val PERMISSION_READ_TASKS = "org.dmfs.permission.READ_TASKS" + val PERMISSION_WRITE_TASKS = "org.dmfs.permission.WRITE_TASKS" + + fun requestAllPermissions(activity: Activity) { + ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS), REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS) + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/Refreshable.java b/app/src/main/java/com/etesync/syncadapter/ui/Refreshable.java deleted file mode 100644 index 1e48e2fe..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/Refreshable.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.etesync.syncadapter.ui; - -public interface Refreshable { - public void refresh(); -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/Refreshable.kt b/app/src/main/java/com/etesync/syncadapter/ui/Refreshable.kt new file mode 100644 index 00000000..0553bf56 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/Refreshable.kt @@ -0,0 +1,5 @@ +package com.etesync.syncadapter.ui + +interface Refreshable { + fun refresh() +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java deleted file mode 100644 index e152379d..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.DialogInterface; -import android.os.AsyncTask; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v7.app.AlertDialog; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.HttpClient; -import com.etesync.syncadapter.InvalidAccountException; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.journalmanager.JournalManager; -import com.etesync.syncadapter.model.CollectionInfo; - -import org.apache.commons.codec.Charsets; - -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; - -public class RemoveMemberFragment extends DialogFragment { - final static private String KEY_MEMBER = "memberEmail"; - private AccountSettings settings; - private OkHttpClient httpClient; - private HttpUrl remote; - private CollectionInfo info; - private String memberEmail; - - public static RemoveMemberFragment newInstance(Account account, CollectionInfo info, String email) { - RemoveMemberFragment frag = new RemoveMemberFragment(); - Bundle args = new Bundle(1); - args.putParcelable(Constants.KEY_ACCOUNT, account); - args.putSerializable(Constants.KEY_COLLECTION_INFO, info); - args.putString(KEY_MEMBER, email); - frag.setArguments(args); - return frag; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Account account = getArguments().getParcelable(Constants.KEY_ACCOUNT); - info = (CollectionInfo) getArguments().getSerializable(Constants.KEY_COLLECTION_INFO); - memberEmail = getArguments().getString(KEY_MEMBER); - try { - settings = new AccountSettings(getContext(), account); - httpClient = HttpClient.create(getContext(), settings); - } catch (InvalidAccountException e) { - e.printStackTrace(); - } - remote = HttpUrl.get(settings.getUri()); - - new MemberRemove().execute(); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - ProgressDialog progress = new ProgressDialog(getContext()); - progress.setTitle(R.string.collection_members_removing); - progress.setMessage(getString(R.string.please_wait)); - progress.setIndeterminate(true); - progress.setCanceledOnTouchOutside(false); - setCancelable(false); - return progress; - } - - private class MemberRemove extends AsyncTask { - @Override - protected RemoveResult doInBackground(Void... voids) { - try { - JournalManager journalsManager = new JournalManager(httpClient, remote); - JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(info.uid); - - JournalManager.Member member = new JournalManager.Member(memberEmail, "placeholder".getBytes(Charsets.UTF_8)); - journalsManager.deleteMember(journal, member); - - return new RemoveResult(null); - } catch (Exception e) { - return new RemoveResult(e); - } - } - - @Override - protected void onPostExecute(RemoveResult result) { - if (result.throwable == null) { - ((Refreshable) getActivity()).refresh(); - } else { - new AlertDialog.Builder(getActivity()) - .setIcon(R.drawable.ic_error_dark) - .setTitle(R.string.collection_members_remove_error) - .setMessage(result.throwable.getMessage()) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }).show(); - } - dismiss(); - } - - class RemoveResult { - final Throwable throwable; - - RemoveResult(final Throwable throwable) { - this.throwable = throwable; - } - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.kt new file mode 100644 index 00000000..617774f1 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.kt @@ -0,0 +1,96 @@ +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.app.Dialog +import android.app.ProgressDialog +import android.os.AsyncTask +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.support.v7.app.AlertDialog +import com.etesync.syncadapter.* +import com.etesync.syncadapter.journalmanager.JournalManager +import com.etesync.syncadapter.model.CollectionInfo +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import org.apache.commons.codec.Charsets + +class RemoveMemberFragment : DialogFragment() { + private var settings: AccountSettings? = null + private var httpClient: OkHttpClient? = null + private var remote: HttpUrl? = null + private var info: CollectionInfo? = null + private var memberEmail: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val account = arguments!!.getParcelable(Constants.KEY_ACCOUNT) + info = arguments!!.getSerializable(Constants.KEY_COLLECTION_INFO) as CollectionInfo + memberEmail = arguments!!.getString(KEY_MEMBER) + try { + settings = AccountSettings(context!!, account!!) + httpClient = HttpClient.create(context!!, settings!!) + } catch (e: InvalidAccountException) { + e.printStackTrace() + } + + remote = HttpUrl.get(settings!!.uri!!) + + MemberRemove().execute() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val progress = ProgressDialog(context) + progress.setTitle(R.string.collection_members_removing) + progress.setMessage(getString(R.string.please_wait)) + progress.isIndeterminate = true + progress.setCanceledOnTouchOutside(false) + isCancelable = false + return progress + } + + private inner class MemberRemove : AsyncTask() { + override fun doInBackground(vararg voids: Void): RemoveResult { + try { + val journalsManager = JournalManager(httpClient!!, remote!!) + val journal = JournalManager.Journal.fakeWithUid(info!!.uid) + + val member = JournalManager.Member(memberEmail!!, "placeholder".toByteArray(Charsets.UTF_8)) + journalsManager.deleteMember(journal, member) + + return RemoveResult(null) + } catch (e: Exception) { + return RemoveResult(e) + } + + } + + override fun onPostExecute(result: RemoveResult) { + if (result.throwable == null) { + (activity as Refreshable).refresh() + } else { + AlertDialog.Builder(activity!!) + .setIcon(R.drawable.ic_error_dark) + .setTitle(R.string.collection_members_remove_error) + .setMessage(result.throwable.message) + .setPositiveButton(android.R.string.yes) { dialog, which -> }.show() + } + dismiss() + } + + internal inner class RemoveResult(val throwable: Throwable?) + } + + companion object { + private val KEY_MEMBER = "memberEmail" + + fun newInstance(account: Account, info: CollectionInfo, email: String): RemoveMemberFragment { + val frag = RemoveMemberFragment() + val args = Bundle(1) + args.putParcelable(Constants.KEY_ACCOUNT, account) + args.putSerializable(Constants.KEY_COLLECTION_INFO, info) + args.putString(KEY_MEMBER, email) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/StartupDialogFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/StartupDialogFragment.java deleted file mode 100644 index c1c2b811..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/StartupDialogFragment.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.PowerManager; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v7.app.AlertDialog; - -import com.etesync.syncadapter.BuildConfig; -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.utils.HintManager; - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -public class StartupDialogFragment extends DialogFragment { - private static final String - HINT_BATTERY_OPTIMIZATIONS = "BatteryOptimizations", - HINT_VENDOR_SPECIFIC_BUGS = "VendorSpecificBugs"; - - private static final String ARGS_MODE = "mode"; - - enum Mode { - BATTERY_OPTIMIZATIONS, - DEVELOPMENT_VERSION, - GOOGLE_PLAY_ACCOUNTS_REMOVED, - VENDOR_SPECIFIC_BUGS, - } - - public static StartupDialogFragment[] getStartupDialogs(Context context) { - List dialogs = new LinkedList<>(); - - if (BuildConfig.VERSION_NAME.contains("-alpha") || BuildConfig.VERSION_NAME.contains("-beta") || BuildConfig.VERSION_NAME.contains("-rc")) - dialogs.add(StartupDialogFragment.instantiate(Mode.DEVELOPMENT_VERSION)); - - // battery optimization whitelisting - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !HintManager.getHintSeen(context, HINT_BATTERY_OPTIMIZATIONS)) { - PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE); - if (powerManager != null && !powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) - dialogs.add(StartupDialogFragment.instantiate(Mode.BATTERY_OPTIMIZATIONS)); - } - - // Vendor specific bugs - String manu = Build.MANUFACTURER; - if (!HintManager.getHintSeen(context, HINT_BATTERY_OPTIMIZATIONS) && (manu.equalsIgnoreCase("Xiaomi") || manu.equalsIgnoreCase("Huawei")) && !Build.DISPLAY.contains("lineage")) { - dialogs.add(StartupDialogFragment.instantiate(Mode.VENDOR_SPECIFIC_BUGS)); - } - - Collections.reverse(dialogs); - return dialogs.toArray(new StartupDialogFragment[dialogs.size()]); - } - - public static StartupDialogFragment instantiate(Mode mode) { - StartupDialogFragment frag = new StartupDialogFragment(); - Bundle args = new Bundle(1); - args.putString(ARGS_MODE, mode.name()); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - @TargetApi(Build.VERSION_CODES.M) - @SuppressLint("BatteryLife") - public Dialog onCreateDialog(Bundle savedInstanceState) { - setCancelable(false); - - Mode mode = Mode.valueOf(getArguments().getString(ARGS_MODE)); - switch (mode) { - case BATTERY_OPTIMIZATIONS: - return new AlertDialog.Builder(getActivity()) - .setTitle(R.string.startup_battery_optimization) - .setMessage(R.string.startup_battery_optimization_message) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }) - .setNeutralButton(R.string.startup_battery_optimization_disable, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, - Uri.parse("package:" + BuildConfig.APPLICATION_ID)); - if (intent.resolveActivity(getContext().getPackageManager()) != null) - getContext().startActivity(intent); - } - }) - .setNegativeButton(R.string.startup_dont_show_again, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - HintManager.setHintSeen(getContext(), HINT_BATTERY_OPTIMIZATIONS, true); - } - }) - .create(); - - case DEVELOPMENT_VERSION: - return new AlertDialog.Builder(getActivity()) - .setIcon(R.mipmap.ic_launcher) - .setTitle(R.string.startup_development_version) - .setMessage(R.string.startup_development_version_message) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }) - .setNeutralButton(R.string.startup_development_version_give_feedback, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - startActivity(new Intent(Intent.ACTION_VIEW, Constants.feedbackUri)); - } - }) - .create(); - case VENDOR_SPECIFIC_BUGS: - return new AlertDialog.Builder(getActivity()) - .setTitle(R.string.startup_vendor_specific_bugs) - .setMessage(R.string.startup_vendor_specific_bugs_message) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }) - .setNeutralButton(R.string.startup_vendor_specific_bugs_open_faq, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - WebViewActivity.openUrl(getContext(), Constants.faqUri.buildUpon().encodedFragment("vendor-issues").build()); - } - }) - .setNegativeButton(R.string.startup_dont_show_again, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - HintManager.setHintSeen(getContext(), HINT_VENDOR_SPECIFIC_BUGS, true); - } - }) - .create(); - } - - throw new IllegalArgumentException(/* illegal mode argument */); - } - - private static String installedFrom(Context context) { - try { - return context.getPackageManager().getInstallerPackageName(context.getPackageName()); - } catch(IllegalArgumentException e) { - return null; - } - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/StartupDialogFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/StartupDialogFragment.kt new file mode 100644 index 00000000..46975d45 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/StartupDialogFragment.kt @@ -0,0 +1,123 @@ +/* + * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.app.Dialog +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.PowerManager +import android.support.v4.app.DialogFragment +import android.support.v7.app.AlertDialog +import com.etesync.syncadapter.BuildConfig +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.utils.HintManager +import java.util.* + +class StartupDialogFragment : DialogFragment() { + + enum class Mode { + BATTERY_OPTIMIZATIONS, + DEVELOPMENT_VERSION, + GOOGLE_PLAY_ACCOUNTS_REMOVED, + VENDOR_SPECIFIC_BUGS + } + + @TargetApi(Build.VERSION_CODES.M) + @SuppressLint("BatteryLife") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + isCancelable = false + + val mode = Mode.valueOf(arguments!!.getString(ARGS_MODE)) + when (mode) { + StartupDialogFragment.Mode.BATTERY_OPTIMIZATIONS -> return AlertDialog.Builder(activity!!) + .setTitle(R.string.startup_battery_optimization) + .setMessage(R.string.startup_battery_optimization_message) + .setPositiveButton(android.R.string.ok) { dialog, which -> } + .setNeutralButton(R.string.startup_battery_optimization_disable) { dialog, which -> + val intent = Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:" + BuildConfig.APPLICATION_ID)) + if (intent.resolveActivity(context!!.packageManager) != null) + context!!.startActivity(intent) + } + .setNegativeButton(R.string.startup_dont_show_again) { dialog, which -> HintManager.setHintSeen(context!!, HINT_BATTERY_OPTIMIZATIONS, true) } + .create() + + StartupDialogFragment.Mode.DEVELOPMENT_VERSION -> return AlertDialog.Builder(activity!!) + .setIcon(R.mipmap.ic_launcher) + .setTitle(R.string.startup_development_version) + .setMessage(R.string.startup_development_version_message) + .setPositiveButton(android.R.string.ok) { dialog, which -> } + .setNeutralButton(R.string.startup_development_version_give_feedback) { dialog, which -> startActivity(Intent(Intent.ACTION_VIEW, Constants.feedbackUri)) } + .create() + StartupDialogFragment.Mode.VENDOR_SPECIFIC_BUGS -> return AlertDialog.Builder(activity!!) + .setTitle(R.string.startup_vendor_specific_bugs) + .setMessage(R.string.startup_vendor_specific_bugs_message) + .setPositiveButton(android.R.string.ok) { dialog, which -> } + .setNeutralButton(R.string.startup_vendor_specific_bugs_open_faq) { dialog, which -> WebViewActivity.openUrl(context!!, Constants.faqUri.buildUpon().encodedFragment("vendor-issues").build()) } + .setNegativeButton(R.string.startup_dont_show_again) { dialog, which -> HintManager.setHintSeen(context!!, HINT_VENDOR_SPECIFIC_BUGS, true) } + .create() + } + + throw IllegalArgumentException(/* illegal mode argument */) + } + + companion object { + private val HINT_BATTERY_OPTIMIZATIONS = "BatteryOptimizations" + private val HINT_VENDOR_SPECIFIC_BUGS = "VendorSpecificBugs" + + private val ARGS_MODE = "mode" + + fun getStartupDialogs(context: Context): Array { + val dialogs = LinkedList() + + if (BuildConfig.VERSION_NAME.contains("-alpha") || BuildConfig.VERSION_NAME.contains("-beta") || BuildConfig.VERSION_NAME.contains("-rc")) + dialogs.add(StartupDialogFragment.instantiate(Mode.DEVELOPMENT_VERSION)) + + // battery optimization whitelisting + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !HintManager.getHintSeen(context, HINT_BATTERY_OPTIMIZATIONS)) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + if (powerManager != null && !powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) + dialogs.add(StartupDialogFragment.instantiate(Mode.BATTERY_OPTIMIZATIONS)) + } + + // Vendor specific bugs + val manu = Build.MANUFACTURER + if (!HintManager.getHintSeen(context, HINT_BATTERY_OPTIMIZATIONS) && (manu.equals("Xiaomi", ignoreCase = true) || manu.equals("Huawei", ignoreCase = true)) && !Build.DISPLAY.contains("lineage")) { + dialogs.add(StartupDialogFragment.instantiate(Mode.VENDOR_SPECIFIC_BUGS)) + } + + Collections.reverse(dialogs) + return dialogs.toTypedArray() + } + + fun instantiate(mode: Mode): StartupDialogFragment { + val frag = StartupDialogFragment() + val args = Bundle(1) + args.putString(ARGS_MODE, mode.name) + frag.arguments = args + return frag + } + + private fun installedFrom(context: Context): String? { + try { + return context.packageManager.getInstallerPackageName(context.packageName) + } catch (e: IllegalArgumentException) { + return null + } + + } + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java deleted file mode 100644 index cb27022f..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui; - -import android.accounts.Account; -import android.content.ContentProviderClient; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; -import android.provider.CalendarContract; -import android.provider.ContactsContract; -import android.support.v7.app.AlertDialog; -import android.view.Gravity; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.TextView; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.EntryEntity; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.resource.LocalAddressBook; -import com.etesync.syncadapter.resource.LocalCalendar; -import com.etesync.syncadapter.ui.importlocal.ImportActivity; -import com.etesync.syncadapter.ui.journalviewer.ListEntriesFragment; -import com.etesync.syncadapter.utils.HintManager; -import com.etesync.syncadapter.utils.ShowcaseBuilder; - -import java.io.FileNotFoundException; -import java.util.Locale; - -import at.bitfire.ical4android.CalendarStorageException; -import at.bitfire.vcard4android.ContactsStorageException; -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; -import tourguide.tourguide.ToolTip; -import tourguide.tourguide.TourGuide; - -public class ViewCollectionActivity extends BaseActivity implements Refreshable { - private final static String HINT_IMPORT = "Import"; - public final static String EXTRA_ACCOUNT = "account", - EXTRA_COLLECTION_INFO = "collectionInfo"; - - private Account account; - private JournalEntity journalEntity; - protected CollectionInfo info; - private boolean isOwner; - - public static Intent newIntent(Context context, Account account, CollectionInfo info) { - Intent intent = new Intent(context, ViewCollectionActivity.class); - intent.putExtra(ViewCollectionActivity.EXTRA_ACCOUNT, account); - intent.putExtra(ViewCollectionActivity.EXTRA_COLLECTION_INFO, info); - return intent; - } - - @Override - public void refresh() { - EntityDataStore data = ((App) getApplicationContext()).getData(); - - journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid); - if ((journalEntity == null) || journalEntity.isDeleted()) { - finish(); - return; - } - - info = journalEntity.getInfo(); - isOwner = journalEntity.isOwner(account.name); - - final View colorSquare = findViewById(R.id.color); - if (info.type == CollectionInfo.Type.CALENDAR) { - if (info.color != null) { - colorSquare.setBackgroundColor(info.color); - } else { - colorSquare.setBackgroundColor(LocalCalendar.defaultColor); - } - } else { - colorSquare.setVisibility(View.GONE); - } - - new LoadCountTask().execute(); - - final TextView title = (TextView) findViewById(R.id.display_name); - title.setText(info.displayName); - - final TextView desc = (TextView) findViewById(R.id.description); - desc.setText(info.description); - - final TextView owner = (TextView) findViewById(R.id.owner); - if (isOwner) { - owner.setVisibility(View.GONE); - } else { - owner.setVisibility(View.VISIBLE); - owner.setText(getString(R.string.account_owner, journalEntity.getOwner())); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.view_collection_activity); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - account = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT); - info = (CollectionInfo) getIntent().getExtras().getSerializable(EXTRA_COLLECTION_INFO); - - if (savedInstanceState == null) { - getSupportFragmentManager().beginTransaction() - .add(R.id.list_entries_container, ListEntriesFragment.newInstance(info)) - .commit(); - } - - refresh(); - - final TextView title = (TextView) findViewById(R.id.display_name); - if (!HintManager.getHintSeen(this, HINT_IMPORT)) { - TourGuide tourGuide = ShowcaseBuilder.getBuilder(this) - .setToolTip(new ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.account_showcase_import)).setGravity(Gravity.BOTTOM)) - .setPointer(null); - tourGuide.mOverlay.setHoleRadius(0); - tourGuide.playOn(title); - HintManager.setHintSeen(this, HINT_IMPORT, true); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.activity_view_collection, menu); - return true; - } - - @Override - protected void onResume() { - super.onResume(); - refresh(); - } - - public void onEditCollection(MenuItem item) { - if (isOwner) { - startActivity(EditCollectionActivity.newIntent(this, account, info)); - } else { - AlertDialog dialog = new AlertDialog.Builder(this) - .setIcon(R.drawable.ic_info_dark) - .setTitle(R.string.not_allowed_title) - .setMessage(getString(R.string.edit_owner_only, journalEntity.getOwner())) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }).create(); - dialog.show(); - } - } - - public void onImport(MenuItem item) { - startActivity(ImportActivity.newIntent(ViewCollectionActivity.this, account, info)); - } - - public void onManageMembers(MenuItem item) { - if (info.version < 2) { - AlertDialog dialog = new AlertDialog.Builder(this) - .setIcon(R.drawable.ic_info_dark) - .setTitle(R.string.not_allowed_title) - .setMessage(R.string.members_old_journals_not_allowed) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }).create(); - dialog.show(); - } else if (isOwner) { - startActivity(CollectionMembersActivity.newIntent(this, account, info)); - } else { - AlertDialog dialog = new AlertDialog.Builder(this) - .setIcon(R.drawable.ic_info_dark) - .setTitle(R.string.not_allowed_title) - .setMessage(getString(R.string.members_owner_only, journalEntity.getOwner())) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }).create(); - dialog.show(); - } - } - - private class LoadCountTask extends AsyncTask { - private int entryCount; - - @Override - protected Long doInBackground(Void... aVoids) { - EntityDataStore data = ((App) getApplicationContext()).getData(); - - final JournalEntity journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid); - - entryCount = data.count(EntryEntity.class).where(EntryEntity.JOURNAL.eq(journalEntity)).get().value(); - long count; - - if (info.type == CollectionInfo.Type.CALENDAR) { - try { - ContentProviderClient providerClient = getContentResolver().acquireContentProviderClient(CalendarContract.CONTENT_URI); - LocalCalendar resource = LocalCalendar.findByName(account, providerClient, LocalCalendar.Factory.INSTANCE, info.uid); - providerClient.release(); - if (resource == null) { - return null; - } - count = resource.count(); - } catch (FileNotFoundException | CalendarStorageException e) { - e.printStackTrace(); - return null; - } - } else { - try { - ContentProviderClient providerClient = getContentResolver().acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI); - LocalAddressBook resource = LocalAddressBook.findByUid(ViewCollectionActivity.this, providerClient, account, info.uid); - providerClient.release(); - if (resource == null) { - return null; - } - count = resource.count(); - } catch (ContactsStorageException e) { - e.printStackTrace(); - return null; - } - } - return count; - } - - @Override - protected void onPostExecute(Long result) { - final TextView stats = (TextView) findViewById(R.id.stats); - findViewById(R.id.progressBar).setVisibility(View.GONE); - - if (result == null) { - stats.setText("Stats loading error."); - } else { - if (info.type == CollectionInfo.Type.CALENDAR) { - stats.setText(String.format(Locale.getDefault(), "Events: %d, Journal entries: %d", - result, entryCount)); - } else { - stats.setText(String.format(Locale.getDefault(), "Contacts: %d, Journal Entries: %d", - result, entryCount)); - } - } - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.kt new file mode 100644 index 00000000..24c5fa10 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.kt @@ -0,0 +1,240 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui + +import android.accounts.Account +import android.content.Context +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.provider.CalendarContract +import android.provider.ContactsContract +import android.support.v7.app.AlertDialog +import android.view.Gravity +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.TextView +import at.bitfire.ical4android.CalendarStorageException +import at.bitfire.vcard4android.ContactsStorageException +import com.etesync.syncadapter.App +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.EntryEntity +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.resource.LocalAddressBook +import com.etesync.syncadapter.resource.LocalCalendar +import com.etesync.syncadapter.ui.importlocal.ImportActivity +import com.etesync.syncadapter.ui.journalviewer.ListEntriesFragment +import com.etesync.syncadapter.utils.HintManager +import com.etesync.syncadapter.utils.ShowcaseBuilder +import tourguide.tourguide.ToolTip +import java.io.FileNotFoundException +import java.util.* + +class ViewCollectionActivity : BaseActivity(), Refreshable { + + private lateinit var account: Account + private var journalEntity: JournalEntity? = null + protected lateinit var info: CollectionInfo + private var isOwner: Boolean = false + + override fun refresh() { + val data = (applicationContext as App).data + + journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid) + if (journalEntity == null || journalEntity!!.isDeleted) { + finish() + return + } + + info = journalEntity!!.info + isOwner = journalEntity!!.isOwner(account.name) + + val colorSquare = findViewById(R.id.color) + if (info.type == CollectionInfo.Type.CALENDAR) { + if (info.color != null) { + colorSquare.setBackgroundColor(info.color) + } else { + colorSquare.setBackgroundColor(LocalCalendar.defaultColor) + } + } else { + colorSquare.visibility = View.GONE + } + + LoadCountTask().execute() + + val title = findViewById(R.id.display_name) as TextView + title.text = info.displayName + + val desc = findViewById(R.id.description) as TextView + desc.text = info.description + + val owner = findViewById(R.id.owner) as TextView + if (isOwner) { + owner.visibility = View.GONE + } else { + owner.visibility = View.VISIBLE + owner.text = getString(R.string.account_owner, journalEntity!!.owner) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.view_collection_activity) + + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + account = intent.extras!!.getParcelable(EXTRA_ACCOUNT) + info = intent.extras!!.getSerializable(EXTRA_COLLECTION_INFO) as CollectionInfo + + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .add(R.id.list_entries_container, ListEntriesFragment.newInstance(info)) + .commit() + } + + refresh() + + val title = findViewById(R.id.display_name) as TextView + if (!HintManager.getHintSeen(this, HINT_IMPORT)) { + val tourGuide = ShowcaseBuilder.getBuilder(this) + .setToolTip(ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.account_showcase_import)).setGravity(Gravity.BOTTOM)) + .setPointer(null) + tourGuide.mOverlay.setHoleRadius(0) + tourGuide.playOn(title) + HintManager.setHintSeen(this, HINT_IMPORT, true) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.activity_view_collection, menu) + return true + } + + override fun onResume() { + super.onResume() + refresh() + } + + fun onEditCollection(item: MenuItem) { + if (isOwner) { + startActivity(EditCollectionActivity.newIntent(this, account, info)) + } else { + val dialog = AlertDialog.Builder(this) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.not_allowed_title) + .setMessage(getString(R.string.edit_owner_only, journalEntity!!.owner)) + .setPositiveButton(android.R.string.yes) { dialog, which -> }.create() + dialog.show() + } + } + + fun onImport(item: MenuItem) { + startActivity(ImportActivity.newIntent(this@ViewCollectionActivity, account, info)) + } + + fun onManageMembers(item: MenuItem) { + if (info.version < 2) { + val dialog = AlertDialog.Builder(this) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.not_allowed_title) + .setMessage(R.string.members_old_journals_not_allowed) + .setPositiveButton(android.R.string.yes) { dialog, which -> }.create() + dialog.show() + } else if (isOwner) { + startActivity(CollectionMembersActivity.newIntent(this, account, info)) + } else { + val dialog = AlertDialog.Builder(this) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.not_allowed_title) + .setMessage(getString(R.string.members_owner_only, journalEntity!!.owner)) + .setPositiveButton(android.R.string.yes) { dialog, which -> }.create() + dialog.show() + } + } + + private inner class LoadCountTask : AsyncTask() { + private var entryCount: Int = 0 + + override fun doInBackground(vararg aVoids: Void): Long? { + val data = (applicationContext as App).data + + val journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid) + + entryCount = data.count(EntryEntity::class.java).where(EntryEntity.JOURNAL.eq(journalEntity)).get().value() + val count: Long + + if (info.type == CollectionInfo.Type.CALENDAR) { + try { + val providerClient = contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI) + val resource = LocalCalendar.findByName(account, providerClient, LocalCalendar.Factory.INSTANCE, info.uid) + providerClient!!.release() + if (resource == null) { + return null + } + count = resource.count() + } catch (e: FileNotFoundException) { + e.printStackTrace() + return null + } catch (e: CalendarStorageException) { + e.printStackTrace() + return null + } + + } else { + try { + val providerClient = contentResolver.acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI) + val resource = LocalAddressBook.findByUid(this@ViewCollectionActivity, providerClient!!, account, info.uid) + providerClient.release() + if (resource == null) { + return null + } + count = resource.count() + } catch (e: ContactsStorageException) { + e.printStackTrace() + return null + } + + } + return count + } + + override fun onPostExecute(result: Long?) { + val stats = findViewById(R.id.stats) as TextView + findViewById(R.id.progressBar).visibility = View.GONE + + if (result == null) { + stats.text = "Stats loading error." + } else { + if (info.type == CollectionInfo.Type.CALENDAR) { + stats.text = String.format(Locale.getDefault(), "Events: %d, Journal entries: %d", + result, entryCount) + } else { + stats.text = String.format(Locale.getDefault(), "Contacts: %d, Journal Entries: %d", + result, entryCount) + } + } + } + } + + companion object { + private val HINT_IMPORT = "Import" + val EXTRA_ACCOUNT = "account" + val EXTRA_COLLECTION_INFO = "collectionInfo" + + fun newIntent(context: Context, account: Account, info: CollectionInfo): Intent { + val intent = Intent(context, ViewCollectionActivity::class.java) + intent.putExtra(ViewCollectionActivity.EXTRA_ACCOUNT, account) + intent.putExtra(ViewCollectionActivity.EXTRA_COLLECTION_INFO, info) + return intent + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/WebViewActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/WebViewActivity.java deleted file mode 100644 index b7978232..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/WebViewActivity.java +++ /dev/null @@ -1,212 +0,0 @@ -package com.etesync.syncadapter.ui; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.view.KeyEvent; -import android.view.MenuItem; -import android.view.View; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.ProgressBar; - -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; - -public class WebViewActivity extends BaseActivity { - - private static final String KEY_URL = "url"; - private static final String QUERY_KEY_EMBEDDED = "embedded"; - - private WebView mWebView; - private ProgressBar mProgressBar; - private ActionBar mToolbar; - - public static void openUrl(Context context, Uri uri) { - if (isAllowedUrl(uri)) { - Intent intent = new Intent(context, WebViewActivity.class); - intent.putExtra(WebViewActivity.KEY_URL, uri); - context.startActivity(intent); - } else { - context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); - } - } - - @Override - @SuppressLint("SetJavaScriptEnabled") - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_webview); - - mToolbar = getSupportActionBar(); - mToolbar.setDisplayHomeAsUpEnabled(true); - - Uri uri = getIntent().getParcelableExtra(KEY_URL); - uri = addQueryParams(uri); - mWebView = (WebView) findViewById(R.id.webView); - mProgressBar = (ProgressBar) findViewById(R.id.progressBar); - - mWebView.getSettings().setJavaScriptEnabled(true); - if (savedInstanceState == null) { - mWebView.loadUrl(uri.toString()); - } - - mWebView.setWebViewClient(new WebViewClient() { - @Override - public void onPageFinished(WebView view, String url) { - setTitle(view.getTitle()); - } - - @SuppressWarnings("deprecation") - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return shouldOverrideUrl(Uri.parse(url)); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return shouldOverrideUrl(request.getUrl()); - } - - @SuppressWarnings("deprecation") - @Override - public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - loadErrorPage(failingUrl); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { - loadErrorPage(request.getUrl().toString()); - } - }); - - mWebView.setWebChromeClient(new WebChromeClient() { - - public void onProgressChanged(WebView view, int progress) { - if (progress == 100) { - mToolbar.setTitle(view.getTitle()); - mProgressBar.setVisibility(View.INVISIBLE); - } else { - mToolbar.setTitle(R.string.loading); - mProgressBar.setVisibility(View.VISIBLE); - mProgressBar.setProgress(progress); - } - } - }); - } - - private Uri addQueryParams(Uri uri) { - return uri.buildUpon().appendQueryParameter(QUERY_KEY_EMBEDDED, "1").build(); - } - - private void loadErrorPage(String failingUrl) { - String htmlData = "" + - getString(R.string.loading_error_title) + - "" + - "" + - "" + - "
" + - "" + getString(R.string.loading_error_content) + - "" + - ""; - - mWebView.loadDataWithBaseURL("about:blank", htmlData, "text/html", "UTF-8", null); - mWebView.invalidate(); - } - - private static boolean uriEqual(Uri uri1, Uri uri2) { - return uri1.getHost().equals(uri2.getHost()) && - uri1.getPath().equals(uri2.getPath()); - } - - private static boolean allowedUris(Uri allowedUris[], Uri uri2) { - for (Uri uri : allowedUris) { - if (uriEqual(uri, uri2)) { - return true; - } - } - return false; - } - - private static boolean isAllowedUrl(Uri uri) { - final Uri allowedUris[] = new Uri[]{ - Constants.faqUri, - Constants.helpUri, - Constants.registrationUrl, - Constants.webUri.buildUpon().appendEncodedPath("tos/").build(), - Constants.webUri.buildUpon().appendEncodedPath("about/").build(), - }; - final Uri accountsUri = Constants.webUri.buildUpon().appendEncodedPath("accounts/").build(); - - return (allowedUris(allowedUris, uri) || - (uri.getHost().equals(accountsUri.getHost()) && - (uri.getPath().startsWith(accountsUri.getPath()))) - ); - } - - private boolean shouldOverrideUrl(Uri uri) { - if (isAllowedUrl(uri)) { - if (uri.getQueryParameter(QUERY_KEY_EMBEDDED) != null) { - return false; - } else { - uri = addQueryParams(uri); - mWebView.loadUrl(uri.toString()); - return true; - } - } else { - startActivity(new Intent(Intent.ACTION_VIEW, uri)); - return true; - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - mWebView.saveState(outState); - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - mWebView.restoreState(savedInstanceState); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - if (mWebView.canGoBack()) { - mWebView.goBack(); - return true; - } - } - return super.onKeyDown(keyCode, event); - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/WebViewActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/WebViewActivity.kt new file mode 100644 index 00000000..091d858c --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/WebViewActivity.kt @@ -0,0 +1,189 @@ +package com.etesync.syncadapter.ui + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.support.v7.app.ActionBar +import android.view.KeyEvent +import android.view.View +import android.webkit.* +import android.widget.ProgressBar +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R + +class WebViewActivity : BaseActivity() { + + private var mWebView: WebView? = null + private var mProgressBar: ProgressBar? = null + private var mToolbar: ActionBar? = null + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_webview) + + mToolbar = supportActionBar + mToolbar!!.setDisplayHomeAsUpEnabled(true) + + var uri = intent.getParcelableExtra(KEY_URL) + uri = addQueryParams(uri) + mWebView = findViewById(R.id.webView) as WebView + mProgressBar = findViewById(R.id.progressBar) as ProgressBar + + mWebView!!.settings.javaScriptEnabled = true + if (savedInstanceState == null) { + mWebView!!.loadUrl(uri.toString()) + } + + mWebView!!.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String) { + title = view.title + } + + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + return shouldOverrideUrl(Uri.parse(url)) + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + return shouldOverrideUrl(request.url) + } + + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + loadErrorPage(failingUrl) + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) { + loadErrorPage(request.url.toString()) + } + } + + mWebView!!.webChromeClient = object : WebChromeClient() { + + override fun onProgressChanged(view: WebView, progress: Int) { + if (progress == 100) { + mToolbar!!.title = view.title + mProgressBar!!.visibility = View.INVISIBLE + } else { + mToolbar!!.setTitle(R.string.loading) + mProgressBar!!.visibility = View.VISIBLE + mProgressBar!!.progress = progress + } + } + } + } + + private fun addQueryParams(uri: Uri): Uri { + return uri.buildUpon().appendQueryParameter(QUERY_KEY_EMBEDDED, "1").build() + } + + private fun loadErrorPage(failingUrl: String) { + val htmlData = "" + + getString(R.string.loading_error_title) + + "" + + "" + + "" + + "
" + + "" + getString(R.string.loading_error_content) + + "" + + "" + + mWebView!!.loadDataWithBaseURL("about:blank", htmlData, "text/html", "UTF-8", null) + mWebView!!.invalidate() + } + + private fun shouldOverrideUrl(uri: Uri): Boolean { + var uri = uri + if (isAllowedUrl(uri)) { + if (uri.getQueryParameter(QUERY_KEY_EMBEDDED) != null) { + return false + } else { + uri = addQueryParams(uri) + mWebView!!.loadUrl(uri.toString()) + return true + } + } else { + startActivity(Intent(Intent.ACTION_VIEW, uri)) + return true + } + } + + override fun onSaveInstanceState(outState: Bundle) { + mWebView!!.saveState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + mWebView!!.restoreState(savedInstanceState) + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (mWebView!!.canGoBack()) { + mWebView!!.goBack() + return true + } + } + return super.onKeyDown(keyCode, event) + } + + companion object { + + private val KEY_URL = "url" + private val QUERY_KEY_EMBEDDED = "embedded" + + fun openUrl(context: Context, uri: Uri) { + if (isAllowedUrl(uri)) { + val intent = Intent(context, WebViewActivity::class.java) + intent.putExtra(WebViewActivity.KEY_URL, uri) + context.startActivity(intent) + } else { + context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + } + } + + private fun uriEqual(uri1: Uri, uri2: Uri): Boolean { + return uri1.host == uri2.host && uri1.path == uri2.path + } + + private fun allowedUris(allowedUris: Array, uri2: Uri): Boolean { + for (uri in allowedUris) { + if (uriEqual(uri, uri2)) { + return true + } + } + return false + } + + private fun isAllowedUrl(uri: Uri): Boolean { + val allowedUris = arrayOf(Constants.faqUri, Constants.helpUri, Constants.registrationUrl, Constants.webUri.buildUpon().appendEncodedPath("tos/").build(), Constants.webUri.buildUpon().appendEncodedPath("about/").build()) + val accountsUri = Constants.webUri.buildUpon().appendEncodedPath("accounts/").build() + + return allowedUris(allowedUris, uri) || uri.host == accountsUri.host && uri.path!!.startsWith(accountsUri.path!!) + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/AccountResolver.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/AccountResolver.java deleted file mode 100644 index ba1a070b..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/AccountResolver.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.etesync.syncadapter.ui.importlocal; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.support.v4.content.ContextCompat; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.R; - -import java.util.HashMap; -import java.util.LinkedHashMap; - -class AccountResolver { - private Context context; - private HashMap cache; - - public AccountResolver(Context context) { - this.context = context; - this.cache = new LinkedHashMap<>(); - } - - public AccountInfo resolve(String accountName) { - // Hardcoded swaps for known accounts: - if (accountName.equals("com.google")) { - accountName = "com.google.android.googlequicksearchbox"; - } else if (accountName.equals(App.getAddressBookAccountType())) { - accountName = App.getAccountType(); - } else if (accountName.equals("at.bitfire.davdroid.address_book")) { - accountName = "at.bitfire.davdroid"; - } - - AccountInfo ret = cache.get(accountName); - if (ret == null) { - try { - PackageManager packageManager = context.getPackageManager(); - ApplicationInfo applicationInfo = packageManager.getApplicationInfo(accountName, 0); - String name = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo).toString() : accountName); - Drawable icon = context.getPackageManager().getApplicationIcon(accountName); - ret = new AccountInfo(name, icon); - } catch (PackageManager.NameNotFoundException e) { - ret = new AccountInfo(accountName, ContextCompat.getDrawable(context, R.drawable.ic_account_dark)); - } - cache.put(accountName, ret); - } - - return ret; - } - - public static class AccountInfo { - final String name; - final Drawable icon; - - AccountInfo(String name, Drawable icon) { - this.name = name; - this.icon = icon; - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/AccountResolver.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/AccountResolver.kt new file mode 100644 index 00000000..6db4bd19 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/AccountResolver.kt @@ -0,0 +1,48 @@ +package com.etesync.syncadapter.ui.importlocal + +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.support.v4.content.ContextCompat +import com.etesync.syncadapter.App +import com.etesync.syncadapter.R +import java.util.* + +internal class AccountResolver(private val context: Context) { + private val cache: HashMap + + init { + this.cache = LinkedHashMap() + } + + fun resolve(accountName: String): AccountInfo { + var accountName = accountName + // Hardcoded swaps for known accounts: + if (accountName == "com.google") { + accountName = "com.google.android.googlequicksearchbox" + } else if (accountName == App.getAddressBookAccountType()) { + accountName = App.getAccountType() + } else if (accountName == "at.bitfire.davdroid.address_book") { + accountName = "at.bitfire.davdroid" + } + + var ret: AccountInfo? = cache[accountName] + if (ret == null) { + try { + val packageManager = context.packageManager + val applicationInfo = packageManager.getApplicationInfo(accountName, 0) + val name = if (applicationInfo != null) packageManager.getApplicationLabel(applicationInfo).toString() else accountName + val icon = context.packageManager.getApplicationIcon(accountName) + ret = AccountInfo(name, icon) + } catch (e: PackageManager.NameNotFoundException) { + ret = AccountInfo(accountName, ContextCompat.getDrawable(context, R.drawable.ic_account_dark)!!) + } + + cache[accountName] = ret!! + } + + return ret + } + + class AccountInfo internal constructor(internal val name: String, internal val icon: Drawable) +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.java deleted file mode 100644 index d4f79da8..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.etesync.syncadapter.ui.importlocal; - -import android.accounts.Account; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.database.Cursor; -import android.net.Uri; -import android.provider.CalendarContract; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.Events; -import android.provider.ContactsContract; -import android.util.Log; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.resource.LocalCalendar; - -import java.util.ArrayList; -import java.util.List; - -/** - * Created by tal on 27/03/17. - */ - -public class CalendarAccount { - private Account account; - private List calendars = new ArrayList<>(); - - private static final String[] CAL_COLS = new String[]{ - Calendars.ACCOUNT_NAME, Calendars.ACCOUNT_TYPE, - Calendars.DELETED, Calendars.NAME}; - - protected CalendarAccount(Account account) { - this.account = account; - } - - // Load all available calendars. - // If an empty list is returned the caller probably needs to enable calendar - // read permissions in App Ops/XPrivacy etc. - public static List loadAll(ContentResolver resolver) { - - if (missing(resolver, Calendars.CONTENT_URI) || missing(resolver, Events.CONTENT_URI)) - return new ArrayList<>(); - - Cursor cur; - try { - cur = resolver.query(Calendars.CONTENT_URI, - CAL_COLS, null, null, - ContactsContract.RawContacts.ACCOUNT_NAME + " ASC, " + ContactsContract.RawContacts.ACCOUNT_TYPE); - } catch (Exception except) { - App.log.warning("Calendar provider is missing columns, continuing anyway"); - cur = resolver.query(Calendars.CONTENT_URI, null, null, null, null); - except.printStackTrace(); - } - List calendarAccounts = new ArrayList<>(cur.getCount()); - - CalendarAccount calendarAccount = null; - - ContentProviderClient contentProviderClient = resolver.acquireContentProviderClient(CalendarContract.CONTENT_URI); - while (cur.moveToNext()) { - if (getLong(cur, Calendars.DELETED) != 0) - continue; - - String accountName = getString(cur, Calendars.ACCOUNT_NAME); - String accountType = getString(cur, Calendars.ACCOUNT_TYPE); - if (calendarAccount == null || - !calendarAccount.getAccountName().equals(accountName) || - !calendarAccount.getAccountType().equals(accountType)) { - calendarAccount = new CalendarAccount(new Account(accountName, accountType)); - calendarAccounts.add(calendarAccount); - } - - try { - LocalCalendar localCalendar = LocalCalendar.findByName(calendarAccount.getAccount(), - contentProviderClient, - LocalCalendar.Factory.INSTANCE, getString(cur, Calendars.NAME)); - if (localCalendar != null) calendarAccount.calendars.add(localCalendar); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - contentProviderClient.release(); - cur.close(); - return calendarAccounts; - } - - private static int getColumnIndex(Cursor cur, String dbName) { - return dbName == null ? -1 : cur.getColumnIndex(dbName); - } - - private static long getLong(Cursor cur, String dbName) { - int i = getColumnIndex(cur, dbName); - return i == -1 ? -1 : cur.getLong(i); - } - - private static String getString(Cursor cur, String dbName) { - int i = getColumnIndex(cur, dbName); - return i == -1 ? null : cur.getString(i); - } - - private static boolean missing(ContentResolver resolver, Uri uri) { - // Determine if a provider is missing - ContentProviderClient provider = resolver.acquireContentProviderClient(uri); - if (provider != null) - provider.release(); - return provider == null; - } - - public String getAccountName() { - return account.name; - } - - public String getAccountType() { - return account.type; - } - - public List getCalendars() { - return calendars; - } - - public Account getAccount() { - return account; - } - - @Override - public String toString() { - return account.toString(); - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.kt new file mode 100644 index 00000000..454fc13e --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.kt @@ -0,0 +1,113 @@ +package com.etesync.syncadapter.ui.importlocal + +import android.accounts.Account +import android.content.ContentResolver +import android.database.Cursor +import android.net.Uri +import android.provider.CalendarContract +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Events +import android.provider.ContactsContract +import com.etesync.syncadapter.App +import com.etesync.syncadapter.resource.LocalCalendar +import java.util.* + +/** + * Created by tal on 27/03/17. + */ + +class CalendarAccount protected constructor(val account: Account) { + private val calendars = ArrayList() + + val accountName: String + get() = account.name + + val accountType: String + get() = account.type + + fun getCalendars(): List { + return calendars + } + + override fun toString(): String { + return account.toString() + } + + companion object { + + private val CAL_COLS = arrayOf(Calendars.ACCOUNT_NAME, Calendars.ACCOUNT_TYPE, Calendars.DELETED, Calendars.NAME) + + // Load all available calendars. + // If an empty list is returned the caller probably needs to enable calendar + // read permissions in App Ops/XPrivacy etc. + fun loadAll(resolver: ContentResolver): List { + + if (missing(resolver, Calendars.CONTENT_URI) || missing(resolver, Events.CONTENT_URI)) + return ArrayList() + + var cur: Cursor? + try { + cur = resolver.query(Calendars.CONTENT_URI, + CAL_COLS, null, null, + ContactsContract.RawContacts.ACCOUNT_NAME + " ASC, " + ContactsContract.RawContacts.ACCOUNT_TYPE) + } catch (except: Exception) { + App.log.warning("Calendar provider is missing columns, continuing anyway") + cur = resolver.query(Calendars.CONTENT_URI, null, null, null, null) + except.printStackTrace() + } + + val calendarAccounts = ArrayList(cur!!.count) + + var calendarAccount: CalendarAccount? = null + + val contentProviderClient = resolver.acquireContentProviderClient(CalendarContract.CONTENT_URI) + while (cur.moveToNext()) { + if (getLong(cur, Calendars.DELETED) != 0L) + continue + + val accountName = getString(cur, Calendars.ACCOUNT_NAME) + val accountType = getString(cur, Calendars.ACCOUNT_TYPE) + if (calendarAccount == null || + calendarAccount.accountName != accountName || + calendarAccount.accountType != accountType) { + calendarAccount = CalendarAccount(Account(accountName, accountType)) + calendarAccounts.add(calendarAccount) + } + + try { + val localCalendar = LocalCalendar.findByName(calendarAccount.account, + contentProviderClient, + LocalCalendar.Factory.INSTANCE, getString(cur, Calendars.NAME)) + if (localCalendar != null) calendarAccount.calendars.add(localCalendar) + } catch (ex: Exception) { + ex.printStackTrace() + } + + } + contentProviderClient!!.release() + cur.close() + return calendarAccounts + } + + private fun getColumnIndex(cur: Cursor?, dbName: String?): Int { + return if (dbName == null) -1 else cur!!.getColumnIndex(dbName) + } + + private fun getLong(cur: Cursor?, dbName: String): Long { + val i = getColumnIndex(cur, dbName) + return if (i == -1) -1 else cur!!.getLong(i) + } + + private fun getString(cur: Cursor?, dbName: String): String? { + val i = getColumnIndex(cur, dbName) + return if (i == -1) null else cur!!.getString(i) + } + + private fun missing(resolver: ContentResolver, uri: Uri): Boolean { + // Determine if a provider is missing + val provider = resolver.acquireContentProviderClient(uri) + provider?.release() + return provider == null + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.java deleted file mode 100644 index c98ffc31..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.etesync.syncadapter.ui.importlocal; - -import android.accounts.Account; -import android.app.Activity; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.ui.BaseActivity; - -public class ImportActivity extends BaseActivity implements SelectImportMethod, ResultFragment.OnImportCallback, DialogInterface { - public final static String EXTRA_ACCOUNT = "account", - EXTRA_COLLECTION_INFO = "collectionInfo"; - - private Account account; - protected CollectionInfo info; - - public static Intent newIntent(Context context, Account account, CollectionInfo info) { - Intent intent = new Intent(context, ImportActivity.class); - intent.putExtra(ImportActivity.EXTRA_ACCOUNT, account); - intent.putExtra(ImportActivity.EXTRA_COLLECTION_INFO, info); - return intent; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setTitle(getString(R.string.import_dialog_title)); - - account = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT); - info = (CollectionInfo) getIntent().getExtras().getSerializable(EXTRA_COLLECTION_INFO); - - if (savedInstanceState == null) - getSupportFragmentManager().beginTransaction() - .add(android.R.id.content, new ImportActivity.SelectImportFragment()) - .commit(); - } - - @Override - public void importFile() { - getSupportFragmentManager().beginTransaction() - .add(ImportFragment.newInstance(account, info), null) - .commit(); - - } - - @Override - public void importAccount() { - if (info.type == CollectionInfo.Type.CALENDAR) { - getSupportFragmentManager().beginTransaction() - .replace(android.R.id.content, - LocalCalendarImportFragment.newInstance(account, info)) - .addToBackStack(LocalCalendarImportFragment.class.getName()) - .commit(); - } else if (info.type == CollectionInfo.Type.ADDRESS_BOOK) { - getSupportFragmentManager().beginTransaction() - .replace(android.R.id.content, - LocalContactImportFragment.newInstance(account, info)) - .addToBackStack(LocalContactImportFragment.class.getName()) - .commit(); - } - setTitle(getString(R.string.import_select_account)); - } - - private void popBackStack() { - if (!getSupportFragmentManager().popBackStackImmediate()) { - finish(); - } else { - setTitle(getString(R.string.import_dialog_title)); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - popBackStack(); - return true; - } - return false; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - popBackStack(); - return true; - } - return super.onKeyDown(keyCode, event); - } - - @Override - public void onImportResult(ResultFragment.ImportResult importResult) { - ResultFragment fragment = ResultFragment.newInstance(importResult); - fragment.show(getSupportFragmentManager(), "importResult"); - } - - @Override - public void cancel() { - finish(); - } - - @Override - public void dismiss() { - finish(); - } - - - public static class SelectImportFragment extends Fragment { - - private SelectImportMethod mSelectImportMethod; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - // This makes sure that the container activity has implemented - // the callback interface. If not, it throws an exception - try { - mSelectImportMethod = (SelectImportMethod) getActivity(); - } catch (ClassCastException e) { - throw new ClassCastException(getActivity().toString() - + " must implement MyInterface "); - } - } - - @SuppressWarnings("deprecation") - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - // This makes sure that the container activity has implemented - // the callback interface. If not, it throws an exception - try { - mSelectImportMethod = (SelectImportMethod) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() - + " must implement MyInterface "); - } - } - - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.import_actions_list, container, false); - - View card = v.findViewById(R.id.import_file); - ImageView img = (ImageView) card.findViewById(R.id.action_icon); - TextView text = (TextView) card.findViewById(R.id.action_text); - img.setImageResource(R.drawable.ic_file_white); - text.setText(R.string.import_button_file); - card.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View aView) { - mSelectImportMethod.importFile(); - } - }); - - card = v.findViewById(R.id.import_account); - img = (ImageView) card.findViewById(R.id.action_icon); - text = (TextView) card.findViewById(R.id.action_text); - img.setImageResource(R.drawable.ic_account_circle_white); - text.setText(R.string.import_button_local); - card.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View aView) { - mSelectImportMethod.importAccount(); - } - }); - - return v; - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt new file mode 100644 index 00000000..a1ce3dee --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt @@ -0,0 +1,160 @@ +package com.etesync.syncadapter.ui.importlocal + +import android.accounts.Account +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.support.v4.app.Fragment +import android.view.* +import android.widget.ImageView +import android.widget.TextView +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.ui.BaseActivity + +class ImportActivity : BaseActivity(), SelectImportMethod, ResultFragment.OnImportCallback, DialogInterface { + + private lateinit var account: Account + protected lateinit var info: CollectionInfo + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + title = getString(R.string.import_dialog_title) + + account = intent.extras!!.getParcelable(EXTRA_ACCOUNT) + info = intent.extras!!.getSerializable(EXTRA_COLLECTION_INFO) as CollectionInfo + + if (savedInstanceState == null) + supportFragmentManager.beginTransaction() + .add(android.R.id.content, ImportActivity.SelectImportFragment()) + .commit() + } + + override fun importFile() { + supportFragmentManager.beginTransaction() + .add(ImportFragment.newInstance(account, info), null) + .commit() + + } + + override fun importAccount() { + if (info.type == CollectionInfo.Type.CALENDAR) { + supportFragmentManager.beginTransaction() + .replace(android.R.id.content, + LocalCalendarImportFragment.newInstance(account, info)) + .addToBackStack(LocalCalendarImportFragment::class.java.name) + .commit() + } else if (info.type == CollectionInfo.Type.ADDRESS_BOOK) { + supportFragmentManager.beginTransaction() + .replace(android.R.id.content, + LocalContactImportFragment.newInstance(account, info)) + .addToBackStack(LocalContactImportFragment::class.java.name) + .commit() + } + title = getString(R.string.import_select_account) + } + + private fun popBackStack() { + if (!supportFragmentManager.popBackStackImmediate()) { + finish() + } else { + title = getString(R.string.import_dialog_title) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + popBackStack() + return true + } + return false + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + popBackStack() + return true + } + return super.onKeyDown(keyCode, event) + } + + override fun onImportResult(importResult: ResultFragment.ImportResult) { + val fragment = ResultFragment.newInstance(importResult) + fragment.show(supportFragmentManager, "importResult") + } + + override fun cancel() { + finish() + } + + override fun dismiss() { + finish() + } + + + class SelectImportFragment : Fragment() { + + private var mSelectImportMethod: SelectImportMethod? = null + + override fun onAttach(context: Context?) { + super.onAttach(context) + // This makes sure that the container activity has implemented + // the callback interface. If not, it throws an exception + try { + mSelectImportMethod = activity as SelectImportMethod? + } catch (e: ClassCastException) { + throw ClassCastException(activity!!.toString() + " must implement MyInterface ") + } + + } + + override fun onAttach(activity: Activity?) { + super.onAttach(activity) + // This makes sure that the container activity has implemented + // the callback interface. If not, it throws an exception + try { + mSelectImportMethod = activity as SelectImportMethod? + } catch (e: ClassCastException) { + throw ClassCastException(activity!!.toString() + " must implement MyInterface ") + } + + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val v = inflater.inflate(R.layout.import_actions_list, container, false) + + var card = v.findViewById(R.id.import_file) + var img = card.findViewById(R.id.action_icon) as ImageView + var text = card.findViewById(R.id.action_text) as TextView + img.setImageResource(R.drawable.ic_file_white) + text.setText(R.string.import_button_file) + card.setOnClickListener { mSelectImportMethod!!.importFile() } + + card = v.findViewById(R.id.import_account) + img = card.findViewById(R.id.action_icon) as ImageView + text = card.findViewById(R.id.action_text) as TextView + img.setImageResource(R.drawable.ic_account_circle_white) + text.setText(R.string.import_button_local) + card.setOnClickListener { mSelectImportMethod!!.importAccount() } + + return v + } + } + + companion object { + val EXTRA_ACCOUNT = "account" + val EXTRA_COLLECTION_INFO = "collectionInfo" + + fun newIntent(context: Context, account: Account, info: CollectionInfo): Intent { + val intent = Intent(context, ImportActivity::class.java) + intent.putExtra(ImportActivity.EXTRA_ACCOUNT, account) + intent.putExtra(ImportActivity.EXTRA_COLLECTION_INFO, info) + return intent + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.java deleted file mode 100644 index 13589970..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.java +++ /dev/null @@ -1,332 +0,0 @@ -package com.etesync.syncadapter.ui.importlocal; - -import android.Manifest; -import android.accounts.Account; -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.ActivityNotFoundException; -import android.content.ContentProviderClient; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.provider.CalendarContract; -import android.provider.ContactsContract; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.resource.LocalAddressBook; -import com.etesync.syncadapter.resource.LocalCalendar; -import com.etesync.syncadapter.resource.LocalContact; -import com.etesync.syncadapter.resource.LocalEvent; -import com.etesync.syncadapter.syncadapter.ContactsSyncManager; -import com.etesync.syncadapter.ui.Refreshable; - -import org.apache.commons.codec.Charsets; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; - -import at.bitfire.ical4android.CalendarStorageException; -import at.bitfire.ical4android.Event; -import at.bitfire.ical4android.InvalidCalendarException; -import at.bitfire.vcard4android.Contact; -import at.bitfire.vcard4android.ContactsStorageException; - -import static com.etesync.syncadapter.Constants.KEY_ACCOUNT; -import static com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO; -import static com.etesync.syncadapter.ui.importlocal.ResultFragment.ImportResult; - -public class ImportFragment extends DialogFragment { - private static final int REQUEST_CODE = 6384; // onActivityResult request - - private static final String TAG_PROGRESS_MAX = "progressMax"; - - private Account account; - private CollectionInfo info; - private File importFile; - - public static ImportFragment newInstance(Account account, CollectionInfo info) { - ImportFragment frag = new ImportFragment(); - Bundle args = new Bundle(1); - args.putParcelable(KEY_ACCOUNT, account); - args.putSerializable(KEY_COLLECTION_INFO, info); - frag.setArguments(args); - return frag; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setCancelable(false); - setRetainInstance(true); - - account = getArguments().getParcelable(KEY_ACCOUNT); - info = (CollectionInfo) getArguments().getSerializable(KEY_COLLECTION_INFO); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - chooseFile(); - } else { - ImportResult data = new ImportResult(); - data.e = new Exception(getString(R.string.import_permission_required)); - ((ResultFragment.OnImportCallback) getActivity()).onImportResult(data); - - dismissAllowingStateLoss(); - } - } - - @TargetApi(Build.VERSION_CODES.M) - private void requestPermissions() { - requestPermissions(new String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - }, 0); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreateDialog(savedInstanceState); - ProgressDialog progress = new ProgressDialog(getActivity()); - progress.setTitle(R.string.import_dialog_title); - progress.setMessage(getString(R.string.import_dialog_loading_file)); - progress.setCanceledOnTouchOutside(false); - progress.setIndeterminate(false); - progress.setIcon(R.drawable.ic_import_export_black); - progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - - if (savedInstanceState == null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(); - } else { - chooseFile(); - } - } else { - setDialogAddEntries(progress, savedInstanceState.getInt(TAG_PROGRESS_MAX)); - } - - return progress; - } - - private void setDialogAddEntries(ProgressDialog dialog, int length) { - dialog.setMax(length); - dialog.setMessage(getString(R.string.import_dialog_adding_entries)); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - final ProgressDialog dialog = (ProgressDialog) getDialog(); - - outState.putInt(TAG_PROGRESS_MAX, dialog.getMax()); - } - - @Override - public void onDestroyView() { - Dialog dialog = getDialog(); - // handles https://code.google.com/p/android/issues/detail?id=17423 - if (dialog != null && getRetainInstance()) { - dialog.setDismissMessage(null); - } - super.onDestroyView(); - } - - public void chooseFile() { - Intent intent = new Intent(); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setAction(Intent.ACTION_GET_CONTENT); - - if (info.type.equals(CollectionInfo.Type.CALENDAR)) { - intent.setType("text/calendar"); - } else if (info.type.equals(CollectionInfo.Type.ADDRESS_BOOK)) { - intent.setType("text/x-vcard"); - } - - Intent chooser = Intent.createChooser( - intent, getString(R.string.choose_file)); - try { - startActivityForResult(chooser, REQUEST_CODE); - } catch (ActivityNotFoundException e) { - ImportResult data = new ImportResult(); - data.e = new Exception("Failed to open file chooser.\nPlease install one."); - - ((ResultFragment.OnImportCallback) getActivity()).onImportResult(data); - - dismissAllowingStateLoss(); - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_CODE: - if (resultCode == Activity.RESULT_OK) { - if (data != null) { - // Get the URI of the selected file - final Uri uri = data.getData(); - App.log.info("Importing uri = " + uri.toString()); - try { - importFile = new File(com.etesync.syncadapter.utils.FileUtils.getPath(getContext(), uri)); - - new Thread(new ImportCalendarsLoader()).start(); - } catch (Exception e) { - App.log.severe("File select error: " + e.getLocalizedMessage()); - } - } - } else { - dismissAllowingStateLoss(); - } - break; - } - super.onActivityResult(requestCode, resultCode, data); - } - - public void loadFinished(ImportResult data) { - ((ResultFragment.OnImportCallback) getActivity()).onImportResult(data); - - dismissAllowingStateLoss(); - - if (getActivity() instanceof Refreshable) { - ((Refreshable) getActivity()).refresh(); - } - } - - private class ImportCalendarsLoader implements Runnable { - private void finishParsingFile(final int length) { - if (getActivity() == null) { - return; - } - - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - setDialogAddEntries((ProgressDialog) getDialog(), length); - } - }); - } - - private void entryProcessed() { - if (getActivity() == null) { - return; - } - - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - final ProgressDialog dialog = (ProgressDialog) getDialog(); - - dialog.incrementProgressBy(1); - } - }); - } - - @Override - public void run() { - final ImportResult result = loadInBackground(); - - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - loadFinished(result); - } - }); - } - - public ImportResult loadInBackground() { - ImportResult result = new ImportResult(); - - try { - FileInputStream importStream = new FileInputStream(importFile); - - if (info.type.equals(CollectionInfo.Type.CALENDAR)) { - final Event[] events = Event.fromStream(importStream, Charsets.UTF_8); - importStream.close(); - - if (events.length == 0) { - App.log.warning("Empty/invalid file."); - result.e = new Exception("Empty/invalid file."); - return result; - } - - result.total = events.length; - - finishParsingFile(events.length); - - ContentProviderClient provider = getContext().getContentResolver().acquireContentProviderClient(CalendarContract.CONTENT_URI); - LocalCalendar localCalendar; - try { - localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory.INSTANCE, info.uid); - if (localCalendar == null) { - throw new FileNotFoundException("Failed to load local resource."); - } - } catch (CalendarStorageException | FileNotFoundException e) { - App.log.info("Fail" + e.getLocalizedMessage()); - result.e = e; - return result; - } - - for (Event event : events) { - try { - LocalEvent localEvent = new LocalEvent(localCalendar, event, event.uid, null); - localEvent.addAsDirty(); - result.added++; - } catch (CalendarStorageException e) { - e.printStackTrace(); - } - - entryProcessed(); - } - } else if (info.type.equals(CollectionInfo.Type.ADDRESS_BOOK)) { - // FIXME: Handle groups and download icon? - Contact.Downloader downloader = new ContactsSyncManager.ResourceDownloader(getContext()); - final Contact[] contacts = Contact.fromStream(importStream, Charsets.UTF_8, downloader); - - if (contacts.length == 0) { - App.log.warning("Empty/invalid file."); - result.e = new Exception("Empty/invalid file."); - return result; - } - - result.total = contacts.length; - - finishParsingFile(contacts.length); - - ContentProviderClient provider = getContext().getContentResolver().acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI); - LocalAddressBook localAddressBook = LocalAddressBook.findByUid(getContext(), provider, account, info.uid); - - for (Contact contact : contacts) { - try { - LocalContact localContact = new LocalContact(localAddressBook, contact, null, null); - localContact.createAsDirty(); - result.added++; - } catch (ContactsStorageException e) { - e.printStackTrace(); - } - - entryProcessed(); - } - provider.release(); - } - - return result; - } catch (FileNotFoundException e) { - result.e = e; - return result; - } catch (InvalidCalendarException | IOException | ContactsStorageException e) { - result.e = e; - return result; - } - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt new file mode 100644 index 00000000..a953361d --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt @@ -0,0 +1,315 @@ +package com.etesync.syncadapter.ui.importlocal + +import android.Manifest +import android.accounts.Account +import android.annotation.TargetApi +import android.app.Activity +import android.app.Dialog +import android.app.ProgressDialog +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.provider.CalendarContract +import android.provider.ContactsContract +import android.support.v4.app.DialogFragment +import at.bitfire.ical4android.CalendarStorageException +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.InvalidCalendarException +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.ContactsStorageException +import com.etesync.syncadapter.App +import com.etesync.syncadapter.Constants.KEY_ACCOUNT +import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.resource.LocalAddressBook +import com.etesync.syncadapter.resource.LocalCalendar +import com.etesync.syncadapter.resource.LocalContact +import com.etesync.syncadapter.resource.LocalEvent +import com.etesync.syncadapter.syncadapter.ContactsSyncManager +import com.etesync.syncadapter.ui.Refreshable +import com.etesync.syncadapter.ui.importlocal.ResultFragment.ImportResult +import org.apache.commons.codec.Charsets +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.IOException + +class ImportFragment : DialogFragment() { + + private var account: Account? = null + private var info: CollectionInfo? = null + private var importFile: File? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isCancelable = false + retainInstance = true + + account = arguments!!.getParcelable(KEY_ACCOUNT) + info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + chooseFile() + } else { + val data = ImportResult() + data.e = Exception(getString(R.string.import_permission_required)) + (activity as ResultFragment.OnImportCallback).onImportResult(data) + + dismissAllowingStateLoss() + } + } + + @TargetApi(Build.VERSION_CODES.M) + private fun requestPermissions() { + requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 0) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + super.onCreateDialog(savedInstanceState) + val progress = ProgressDialog(activity) + progress.setTitle(R.string.import_dialog_title) + progress.setMessage(getString(R.string.import_dialog_loading_file)) + progress.setCanceledOnTouchOutside(false) + progress.isIndeterminate = false + progress.setIcon(R.drawable.ic_import_export_black) + progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL) + + if (savedInstanceState == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions() + } else { + chooseFile() + } + } else { + setDialogAddEntries(progress, savedInstanceState.getInt(TAG_PROGRESS_MAX)) + } + + return progress + } + + private fun setDialogAddEntries(dialog: ProgressDialog, length: Int) { + dialog.max = length + dialog.setMessage(getString(R.string.import_dialog_adding_entries)) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + val dialog = dialog as ProgressDialog + + outState.putInt(TAG_PROGRESS_MAX, dialog.max) + } + + override fun onDestroyView() { + val dialog = dialog + // handles https://code.google.com/p/android/issues/detail?id=17423 + if (dialog != null && retainInstance) { + dialog.setDismissMessage(null) + } + super.onDestroyView() + } + + fun chooseFile() { + val intent = Intent() + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.action = Intent.ACTION_GET_CONTENT + + if (info!!.type == CollectionInfo.Type.CALENDAR) { + intent.type = "text/calendar" + } else if (info!!.type == CollectionInfo.Type.ADDRESS_BOOK) { + intent.type = "text/x-vcard" + } + + val chooser = Intent.createChooser( + intent, getString(R.string.choose_file)) + try { + startActivityForResult(chooser, REQUEST_CODE) + } catch (e: ActivityNotFoundException) { + val data = ImportResult() + data.e = Exception("Failed to open file chooser.\nPlease install one.") + + (activity as ResultFragment.OnImportCallback).onImportResult(data) + + dismissAllowingStateLoss() + } + + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + REQUEST_CODE -> if (resultCode == Activity.RESULT_OK) { + if (data != null) { + // Get the URI of the selected file + val uri = data.data + App.log.info("Importing uri = " + uri!!.toString()) + try { + importFile = File(com.etesync.syncadapter.utils.FileUtils.getPath(context, uri)) + + Thread(ImportCalendarsLoader()).start() + } catch (e: Exception) { + App.log.severe("File select error: " + e.localizedMessage) + } + + } + } else { + dismissAllowingStateLoss() + } + } + super.onActivityResult(requestCode, resultCode, data) + } + + fun loadFinished(data: ImportResult) { + (activity as ResultFragment.OnImportCallback).onImportResult(data) + + dismissAllowingStateLoss() + + if (activity is Refreshable) { + (activity as Refreshable).refresh() + } + } + + private inner class ImportCalendarsLoader : Runnable { + private fun finishParsingFile(length: Int) { + if (activity == null) { + return + } + + activity!!.runOnUiThread { setDialogAddEntries(dialog as ProgressDialog, length) } + } + + private fun entryProcessed() { + if (activity == null) { + return + } + + activity!!.runOnUiThread { + val dialog = dialog as ProgressDialog + + dialog.incrementProgressBy(1) + } + } + + override fun run() { + val result = loadInBackground() + + activity!!.runOnUiThread { loadFinished(result) } + } + + fun loadInBackground(): ImportResult { + val result = ImportResult() + + try { + val importStream = FileInputStream(importFile!!) + + if (info!!.type == CollectionInfo.Type.CALENDAR) { + val events = Event.fromStream(importStream, Charsets.UTF_8) + importStream.close() + + if (events.size == 0) { + App.log.warning("Empty/invalid file.") + result.e = Exception("Empty/invalid file.") + return result + } + + result.total = events.size.toLong() + + finishParsingFile(events.size) + + val provider = context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI) + val localCalendar: LocalCalendar? + try { + localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory.INSTANCE, info!!.uid) + if (localCalendar == null) { + throw FileNotFoundException("Failed to load local resource.") + } + } catch (e: CalendarStorageException) { + App.log.info("Fail" + e.localizedMessage) + result.e = e + return result + } catch (e: FileNotFoundException) { + App.log.info("Fail" + e.localizedMessage) + result.e = e + return result + } + + for (event in events) { + try { + val localEvent = LocalEvent(localCalendar, event, event.uid, null) + localEvent.addAsDirty() + result.added++ + } catch (e: CalendarStorageException) { + e.printStackTrace() + } + + entryProcessed() + } + } else if (info!!.type == CollectionInfo.Type.ADDRESS_BOOK) { + // FIXME: Handle groups and download icon? + val downloader = ContactsSyncManager.ResourceDownloader(context!!) + val contacts = Contact.fromStream(importStream, Charsets.UTF_8, downloader) + + if (contacts.size == 0) { + App.log.warning("Empty/invalid file.") + result.e = Exception("Empty/invalid file.") + return result + } + + result.total = contacts.size.toLong() + + finishParsingFile(contacts.size) + + val provider = context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI) + val localAddressBook = LocalAddressBook.findByUid(context!!, provider!!, account, info!!.uid) + + for (contact in contacts) { + try { + val localContact = LocalContact(localAddressBook, contact, null, null) + localContact.createAsDirty() + result.added++ + } catch (e: ContactsStorageException) { + e.printStackTrace() + } + + entryProcessed() + } + provider.release() + } + + return result + } catch (e: FileNotFoundException) { + result.e = e + return result + } catch (e: InvalidCalendarException) { + result.e = e + return result + } catch (e: IOException) { + result.e = e + return result + } catch (e: ContactsStorageException) { + result.e = e + return result + } + + } + } + + companion object { + private val REQUEST_CODE = 6384 // onActivityResult request + + private val TAG_PROGRESS_MAX = "progressMax" + + fun newInstance(account: Account, info: CollectionInfo): ImportFragment { + val frag = ImportFragment() + val args = Bundle(1) + args.putParcelable(KEY_ACCOUNT, account) + args.putSerializable(KEY_COLLECTION_INFO, info) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.java deleted file mode 100644 index 873140c8..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.java +++ /dev/null @@ -1,267 +0,0 @@ -package com.etesync.syncadapter.ui.importlocal; - -import android.accounts.Account; -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; -import android.os.Bundle; -import android.provider.CalendarContract; -import android.support.v4.app.ListFragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseExpandableListAdapter; -import android.widget.ExpandableListView; -import android.widget.ImageView; -import android.widget.TextView; - -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.resource.LocalCalendar; -import com.etesync.syncadapter.resource.LocalEvent; - -import java.util.List; - -import at.bitfire.ical4android.CalendarStorageException; -import at.bitfire.ical4android.Event; - -import static com.etesync.syncadapter.Constants.KEY_ACCOUNT; -import static com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO; - -public class LocalCalendarImportFragment extends ListFragment { - - private Account account; - private CollectionInfo info; - - public static LocalCalendarImportFragment newInstance(Account account, CollectionInfo info) { - LocalCalendarImportFragment frag = new LocalCalendarImportFragment(); - Bundle args = new Bundle(1); - args.putParcelable(KEY_ACCOUNT, account); - args.putSerializable(KEY_COLLECTION_INFO, info); - frag.setArguments(args); - return frag; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - - account = getArguments().getParcelable(KEY_ACCOUNT); - info = (CollectionInfo) getArguments().getSerializable(KEY_COLLECTION_INFO); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_local_calendar_import, container, false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - importAccount(); - } - - protected void importAccount() { - final List calendarAccountList = CalendarAccount.loadAll(getContext().getContentResolver()); - - ExpandableListView listCalendar = (ExpandableListView) getListView(); - - final LocalCalendarImportFragment.ExpandableListAdapter adapter = - new LocalCalendarImportFragment.ExpandableListAdapter(getContext(), calendarAccountList); - listCalendar.setAdapter(adapter); - - listCalendar.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { - @Override - public boolean onChildClick(ExpandableListView aExpandableListView, View aView, int groupPosition, int childPosition, long aL) { - new ImportEvents().execute(calendarAccountList.get(groupPosition).getCalendars().get(childPosition)); - return false; - } - }); - } - - - private class ExpandableListAdapter extends BaseExpandableListAdapter { - - private Context context; - private List calendarAccounts; - private AccountResolver accountResolver; - - public ExpandableListAdapter(Context context, List calendarAccounts) { - this.context = context; - this.calendarAccounts = calendarAccounts; - this.accountResolver = new AccountResolver(context); - } - - private class ChildViewHolder { - TextView textView; - } - - private class GroupViewHolder { - TextView titleTextView; - TextView descriptionTextView; - ImageView iconImageView; - } - - @Override - public Object getChild(int groupPosition, int childPosititon) { - return calendarAccounts.get(groupPosition).getCalendars() - .get(childPosititon).getDisplayName(); - } - - @Override - public long getChildId(int groupPosition, int childPosition) { - return childPosition; - } - - @Override - public View getChildView(int groupPosition, final int childPosition, - boolean isLastChild, View convertView, ViewGroup parent) { - - final String childText = (String) getChild(groupPosition, childPosition); - ChildViewHolder viewHolder; - if (convertView == null) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.import_calendars_list_item, null); - } - - if (convertView.getTag() != null) { - viewHolder = (ChildViewHolder) convertView.getTag(); - } else { - viewHolder = new ChildViewHolder(); - viewHolder.textView = (TextView) convertView - .findViewById(R.id.listItemText); - convertView.setTag(viewHolder); - } - viewHolder.textView.setText(childText); - return convertView; - } - - @Override - public int getChildrenCount(int groupPosition) { - return calendarAccounts.get(groupPosition).getCalendars() - .size(); - } - - @Override - public Object getGroup(int groupPosition) { - return calendarAccounts.get(groupPosition); - } - - @Override - public int getGroupCount() { - return calendarAccounts.size(); - } - - @Override - public long getGroupId(int groupPosition) { - return groupPosition; - } - - @Override - public View getGroupView(int groupPosition, boolean isExpanded, - View convertView, ViewGroup parent) { - CalendarAccount calendarAccount = (CalendarAccount) getGroup(groupPosition); - GroupViewHolder viewHolder; - if (convertView == null) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.import_content_list_header, null); - } - if (convertView.getTag() != null) { - viewHolder = (GroupViewHolder) convertView.getTag(); - } else { - viewHolder = new GroupViewHolder(); - viewHolder.titleTextView = (TextView) convertView - .findViewById(R.id.title); - viewHolder.descriptionTextView = (TextView) convertView - .findViewById(R.id.description); - viewHolder.iconImageView = (ImageView) convertView.findViewById(R.id.icon); - convertView.setTag(viewHolder); - } - - viewHolder.titleTextView.setText(calendarAccount.getAccountName()); - AccountResolver.AccountInfo accountInfo = accountResolver.resolve(calendarAccount.getAccountType()); - viewHolder.descriptionTextView.setText(accountInfo.name); - viewHolder.iconImageView.setImageDrawable(accountInfo.icon); - - return convertView; - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public boolean isChildSelectable(int groupPosition, int childPosition) { - return true; - } - } - - protected class ImportEvents extends AsyncTask { - ProgressDialog progressDialog; - - @Override - protected void onPreExecute() { - progressDialog = new ProgressDialog(getActivity()); - progressDialog.setTitle(R.string.import_dialog_title); - progressDialog.setMessage(getString(R.string.import_dialog_adding_entries)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.setCancelable(false); - progressDialog.setIndeterminate(false); - progressDialog.setIcon(R.drawable.ic_import_export_black); - progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - progressDialog.show(); - } - - @Override - protected ResultFragment.ImportResult doInBackground(LocalCalendar... calendars) { - return importEvents(calendars[0]); - } - - @Override - protected void onProgressUpdate(Integer... progress) { - if (progressDialog != null) - progressDialog.setProgress(progress[0]); - } - - @Override - protected void onPostExecute(ResultFragment.ImportResult result) { - progressDialog.dismiss(); - ((ResultFragment.OnImportCallback) getActivity()).onImportResult(result); - } - - private ResultFragment.ImportResult importEvents(LocalCalendar fromCalendar) { - ResultFragment.ImportResult result = new ResultFragment.ImportResult(); - try { - LocalCalendar localCalendar = LocalCalendar.findByName(account, - getContext().getContentResolver().acquireContentProviderClient(CalendarContract.CONTENT_URI), - LocalCalendar.Factory.INSTANCE, info.uid); - LocalEvent[] localEvents = fromCalendar.getAll(); - int total = localEvents.length; - progressDialog.setMax(total); - result.total = total; - int progress = 0; - for (LocalEvent currentLocalEvent : localEvents) { - Event event = currentLocalEvent.getEvent(); - try { - LocalEvent localEvent = new LocalEvent(localCalendar, event, null, null); - localEvent.addAsDirty(); - result.added++; - } catch (CalendarStorageException e) { - e.printStackTrace(); - - } - publishProgress(++progress); - } - } catch (Exception e) { - e.printStackTrace(); - result.e = e; - } - return result; - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt new file mode 100644 index 00000000..9cbdc082 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt @@ -0,0 +1,241 @@ +package com.etesync.syncadapter.ui.importlocal + +import android.accounts.Account +import android.app.ProgressDialog +import android.content.Context +import android.os.AsyncTask +import android.os.Bundle +import android.provider.CalendarContract +import android.support.v4.app.ListFragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseExpandableListAdapter +import android.widget.ExpandableListView +import android.widget.ImageView +import android.widget.TextView +import at.bitfire.ical4android.CalendarStorageException +import com.etesync.syncadapter.Constants.KEY_ACCOUNT +import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.resource.LocalCalendar +import com.etesync.syncadapter.resource.LocalEvent + +class LocalCalendarImportFragment : ListFragment() { + + private var account: Account? = null + private var info: CollectionInfo? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = true + + account = arguments!!.getParcelable(KEY_ACCOUNT) + info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_local_calendar_import, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + importAccount() + } + + protected fun importAccount() { + val calendarAccountList = CalendarAccount.loadAll(context!!.contentResolver) + + val listCalendar = listView as ExpandableListView + + val adapter = ExpandableListAdapter(context!!, calendarAccountList) + listCalendar.setAdapter(adapter) + + listCalendar.setOnChildClickListener { aExpandableListView, aView, groupPosition, childPosition, aL -> + ImportEvents().execute(calendarAccountList[groupPosition].getCalendars()[childPosition]) + false + } + } + + + private inner class ExpandableListAdapter(private val context: Context, private val calendarAccounts: List) : BaseExpandableListAdapter() { + private val accountResolver: AccountResolver + + init { + this.accountResolver = AccountResolver(context) + } + + private inner class ChildViewHolder { + internal var textView: TextView? = null + } + + private inner class GroupViewHolder { + internal var titleTextView: TextView? = null + internal var descriptionTextView: TextView? = null + internal var iconImageView: ImageView? = null + } + + override fun getChild(groupPosition: Int, childPosititon: Int): Any { + return calendarAccounts[groupPosition].getCalendars()[childPosititon].displayName + } + + override fun getChildId(groupPosition: Int, childPosition: Int): Long { + return childPosition.toLong() + } + + override fun getChildView(groupPosition: Int, childPosition: Int, + isLastChild: Boolean, convertView: View?, parent: ViewGroup): View { + var convertView = convertView + + val childText = getChild(groupPosition, childPosition) as String + val viewHolder: ChildViewHolder + if (convertView == null) { + val inflater = context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + convertView = inflater.inflate(R.layout.import_calendars_list_item, null) + } + + if (convertView!!.tag != null) { + viewHolder = convertView.tag as ChildViewHolder + } else { + viewHolder = ChildViewHolder() + viewHolder.textView = convertView + .findViewById(R.id.listItemText) as TextView + convertView.tag = viewHolder + } + viewHolder.textView!!.text = childText + return convertView + } + + override fun getChildrenCount(groupPosition: Int): Int { + return calendarAccounts[groupPosition].getCalendars() + .size + } + + override fun getGroup(groupPosition: Int): Any { + return calendarAccounts[groupPosition] + } + + override fun getGroupCount(): Int { + return calendarAccounts.size + } + + override fun getGroupId(groupPosition: Int): Long { + return groupPosition.toLong() + } + + override fun getGroupView(groupPosition: Int, isExpanded: Boolean, + convertView: View?, parent: ViewGroup): View { + var convertView = convertView + val calendarAccount = getGroup(groupPosition) as CalendarAccount + val viewHolder: GroupViewHolder + if (convertView == null) { + val inflater = context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + convertView = inflater.inflate(R.layout.import_content_list_header, null) + } + if (convertView!!.tag != null) { + viewHolder = convertView.tag as GroupViewHolder + } else { + viewHolder = GroupViewHolder() + viewHolder.titleTextView = convertView + .findViewById(R.id.title) as TextView + viewHolder.descriptionTextView = convertView + .findViewById(R.id.description) as TextView + viewHolder.iconImageView = convertView.findViewById(R.id.icon) as ImageView + convertView.tag = viewHolder + } + + viewHolder.titleTextView!!.text = calendarAccount.accountName + val accountInfo = accountResolver.resolve(calendarAccount.accountType) + viewHolder.descriptionTextView!!.text = accountInfo.name + viewHolder.iconImageView!!.setImageDrawable(accountInfo.icon) + + return convertView + } + + override fun hasStableIds(): Boolean { + return false + } + + override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean { + return true + } + } + + protected inner class ImportEvents : AsyncTask() { + internal var progressDialog: ProgressDialog? = null + + override fun onPreExecute() { + progressDialog = ProgressDialog(activity) + progressDialog!!.setTitle(R.string.import_dialog_title) + progressDialog!!.setMessage(getString(R.string.import_dialog_adding_entries)) + progressDialog!!.setCanceledOnTouchOutside(false) + progressDialog!!.setCancelable(false) + progressDialog!!.isIndeterminate = false + progressDialog!!.setIcon(R.drawable.ic_import_export_black) + progressDialog!!.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL) + progressDialog!!.show() + } + + override fun doInBackground(vararg calendars: LocalCalendar): ResultFragment.ImportResult { + return importEvents(calendars[0]) + } + + override fun onProgressUpdate(vararg progress: Int?) { + if (progressDialog != null) + progressDialog!!.progress = progress[0]!! + } + + override fun onPostExecute(result: ResultFragment.ImportResult) { + progressDialog!!.dismiss() + (activity as ResultFragment.OnImportCallback).onImportResult(result) + } + + private fun importEvents(fromCalendar: LocalCalendar): ResultFragment.ImportResult { + val result = ResultFragment.ImportResult() + try { + val localCalendar = LocalCalendar.findByName(account, + context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI), + LocalCalendar.Factory.INSTANCE, info!!.uid) + val localEvents = fromCalendar.all + val total = localEvents.size + progressDialog!!.max = total + result.total = total.toLong() + var progress = 0 + for (currentLocalEvent in localEvents) { + val event = currentLocalEvent.event + try { + val localEvent = LocalEvent(localCalendar!!, event, null, null) + localEvent.addAsDirty() + result.added++ + } catch (e: CalendarStorageException) { + e.printStackTrace() + + } + + publishProgress(++progress) + } + } catch (e: Exception) { + e.printStackTrace() + result.e = e + } + + return result + } + } + + companion object { + + fun newInstance(account: Account, info: CollectionInfo): LocalCalendarImportFragment { + val frag = LocalCalendarImportFragment() + val args = Bundle(1) + args.putParcelable(KEY_ACCOUNT, account) + args.putSerializable(KEY_COLLECTION_INFO, info) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.java deleted file mode 100644 index ae15d399..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.java +++ /dev/null @@ -1,297 +0,0 @@ -package com.etesync.syncadapter.ui.importlocal; - -import android.accounts.Account; -import android.app.ProgressDialog; -import android.content.ContentProviderClient; -import android.content.Context; -import android.content.res.TypedArray; -import android.database.Cursor; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.os.Bundle; -import android.provider.ContactsContract; -import android.support.v4.app.Fragment; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.resource.LocalAddressBook; -import com.etesync.syncadapter.resource.LocalContact; - -import java.util.ArrayList; -import java.util.List; - -import at.bitfire.vcard4android.Contact; -import at.bitfire.vcard4android.ContactsStorageException; - -import static android.content.ContentValues.TAG; -import static com.etesync.syncadapter.Constants.KEY_ACCOUNT; -import static com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO; - -public class LocalContactImportFragment extends Fragment { - - private Account account; - private CollectionInfo info; - private RecyclerView recyclerView; - - public static LocalContactImportFragment newInstance(Account account, CollectionInfo info) { - LocalContactImportFragment frag = new LocalContactImportFragment(); - Bundle args = new Bundle(1); - args.putParcelable(KEY_ACCOUNT, account); - args.putSerializable(KEY_COLLECTION_INFO, info); - frag.setArguments(args); - - return frag; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - - account = getArguments().getParcelable(KEY_ACCOUNT); - info = (CollectionInfo) getArguments().getSerializable(KEY_COLLECTION_INFO); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_local_contact_import, container, false); - - recyclerView = (RecyclerView) view.findViewById(R.id.recyclerView); - - recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - recyclerView.addItemDecoration(new DividerItemDecoration(getActivity())); - return view; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - importAccount(); - } - - protected void importAccount() { - ContentProviderClient provider = getContext().getContentResolver().acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI); - Cursor cursor; - try { - cursor = provider.query(ContactsContract.RawContacts.CONTENT_URI, - new String[]{ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE} - , null, null, - ContactsContract.RawContacts.ACCOUNT_NAME + " ASC, " + ContactsContract.RawContacts.ACCOUNT_TYPE); - } catch (Exception except) { - Log.w(TAG, "Addressbook provider is missing columns, continuing anyway"); - - except.printStackTrace(); - return; - } - - final List localAddressBooks = new ArrayList<>(); - Account account = null; - int accountNameIndex = cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME); - int accountTypeIndex = cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_TYPE); - while (cursor.moveToNext()) { - String accountName = cursor.getString(accountNameIndex); - String accountType = cursor.getString(accountTypeIndex); - if (account == null || !(account.name.equals(accountName) && account.type.equals(accountType))) { - if ((accountName != null) && (accountType != null)) { - account = new Account(accountName, accountType); - localAddressBooks.add(new LocalAddressBook(getContext(), account, provider)); - } - } - } - - cursor.close(); - provider.release(); - - recyclerView.setAdapter(new ImportContactAdapter(getContext(), localAddressBooks, new OnAccountSelected() { - @Override - public void accountSelected(int index) { - new ImportContacts().execute(localAddressBooks.get(index)); - } - })); - } - - protected class ImportContacts extends AsyncTask { - ProgressDialog progressDialog; - - @Override - protected void onPreExecute() { - progressDialog = new ProgressDialog(getActivity()); - progressDialog.setTitle(R.string.import_dialog_title); - progressDialog.setMessage(getString(R.string.import_dialog_adding_entries)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.setCancelable(false); - progressDialog.setIndeterminate(false); - progressDialog.setIcon(R.drawable.ic_import_export_black); - progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - progressDialog.show(); - } - - @Override - protected ResultFragment.ImportResult doInBackground(LocalAddressBook... addressBooks) { - return importContacts(addressBooks[0]); - } - - @Override - protected void onProgressUpdate(Integer... progress) { - if (progressDialog != null) - progressDialog.setProgress(progress[0]); - } - - @Override - protected void onPostExecute(ResultFragment.ImportResult result) { - progressDialog.dismiss(); - ((ResultFragment.OnImportCallback) getActivity()).onImportResult(result); - } - - private ResultFragment.ImportResult importContacts(LocalAddressBook localAddressBook) { - ResultFragment.ImportResult result = new ResultFragment.ImportResult(); - try { - LocalAddressBook addressBook = LocalAddressBook.findByUid(getContext(), - getContext().getContentResolver().acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI), - account, info.uid); - LocalContact[] localContacts = localAddressBook.getAll(); - int total = localContacts.length; - progressDialog.setMax(total); - result.total = total; - int progress = 0; - for (LocalContact currentLocalContact : localContacts) { - Contact contact = currentLocalContact.getContact(); - - try { - LocalContact localContact = new LocalContact(addressBook, contact, null, null); - localContact.createAsDirty(); - result.added++; - } catch (ContactsStorageException e) { - e.printStackTrace(); - result.e = e; - } - publishProgress(++progress); - } - } catch (Exception e) { - result.e = e; - } - return result; - } - } - - public static class ImportContactAdapter extends RecyclerView.Adapter { - private static final String TAG = "ImportContactAdapter"; - - private List mAddressBooks; - private OnAccountSelected mOnAccountSelected; - private AccountResolver accountResolver; - - /** - * Provide a reference to the type of views that you are using (custom ViewHolder) - */ - public static class ViewHolder extends RecyclerView.ViewHolder { - private final TextView titleTextView; - private final TextView descTextView; - private final ImageView iconImageView; - - public ViewHolder(View v, final OnAccountSelected onAccountSelected) { - super(v); - // Define click listener for the ViewHolder's View. - v.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onAccountSelected.accountSelected(getAdapterPosition()); - } - }); - titleTextView = (TextView) v.findViewById(R.id.title); - descTextView = (TextView) v.findViewById(R.id.description); - iconImageView = (ImageView) v.findViewById(R.id.icon); - } - } - - /** - * Initialize the dataset of the Adapter. - * - * @param addressBooks containing the data to populate views to be used by RecyclerView. - */ - public ImportContactAdapter(Context context, List addressBooks, OnAccountSelected onAccountSelected) { - mAddressBooks = addressBooks; - mOnAccountSelected = onAccountSelected; - accountResolver = new AccountResolver(context); - } - - // Create new views (invoked by the layout manager) - @Override - public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { - // Create a new view. - View v = LayoutInflater.from(viewGroup.getContext()) - .inflate(R.layout.import_content_list_account, viewGroup, false); - - return new ViewHolder(v, mOnAccountSelected); - } - - @Override - public void onBindViewHolder(ViewHolder viewHolder, final int position) { - viewHolder.titleTextView.setText(mAddressBooks.get(position).account.name); - AccountResolver.AccountInfo accountInfo = accountResolver.resolve(mAddressBooks.get(position).account.type); - viewHolder.descTextView.setText(accountInfo.name); - viewHolder.iconImageView.setImageDrawable(accountInfo.icon); - } - - @Override - public int getItemCount() { - return mAddressBooks.size(); - } - } - - private interface OnAccountSelected { - void accountSelected(int index); - } - - public static class DividerItemDecoration extends RecyclerView.ItemDecoration { - - private static final int[] ATTRS = new int[]{ - android.R.attr.listDivider - }; - - private Drawable mDivider; - - public DividerItemDecoration(Context context) { - final TypedArray a = context.obtainStyledAttributes(ATTRS); - mDivider = a.getDrawable(0); - a.recycle(); - } - - @Override - public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { - drawVertical(c, parent); - } - - public void drawVertical(Canvas c, RecyclerView parent) { - final int left = parent.getPaddingLeft(); - final int right = parent.getWidth() - parent.getPaddingRight(); - - final int childCount = parent.getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = parent.getChildAt(i); - final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child - .getLayoutParams(); - final int top = child.getBottom() + params.bottomMargin; - final int bottom = top + mDivider.getIntrinsicHeight(); - mDivider.setBounds(left, top, right, bottom); - mDivider.draw(c); - } - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { - outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt new file mode 100644 index 00000000..20bcce36 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt @@ -0,0 +1,274 @@ +package com.etesync.syncadapter.ui.importlocal + +import android.accounts.Account +import android.app.ProgressDialog +import android.content.ContentValues.TAG +import android.content.Context +import android.database.Cursor +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.os.AsyncTask +import android.os.Bundle +import android.provider.ContactsContract +import android.support.v4.app.Fragment +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import at.bitfire.vcard4android.ContactsStorageException +import com.etesync.syncadapter.Constants.KEY_ACCOUNT +import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.resource.LocalAddressBook +import com.etesync.syncadapter.resource.LocalContact +import java.util.* + +class LocalContactImportFragment : Fragment() { + + private var account: Account? = null + private var info: CollectionInfo? = null + private var recyclerView: RecyclerView? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = true + + account = arguments!!.getParcelable(KEY_ACCOUNT) + info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.fragment_local_contact_import, container, false) + + recyclerView = view.findViewById(R.id.recyclerView) as RecyclerView + + recyclerView!!.layoutManager = LinearLayoutManager(activity) + recyclerView!!.addItemDecoration(DividerItemDecoration(activity!!)) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + importAccount() + } + + protected fun importAccount() { + val provider = context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI) + val cursor: Cursor? + try { + cursor = provider!!.query(ContactsContract.RawContacts.CONTENT_URI, + arrayOf(ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE), null, null, + ContactsContract.RawContacts.ACCOUNT_NAME + " ASC, " + ContactsContract.RawContacts.ACCOUNT_TYPE) + } catch (except: Exception) { + Log.w(TAG, "Addressbook provider is missing columns, continuing anyway") + + except.printStackTrace() + return + } + + val localAddressBooks = ArrayList() + var account: Account? = null + val accountNameIndex = cursor!!.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME) + val accountTypeIndex = cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_TYPE) + while (cursor.moveToNext()) { + val accountName = cursor.getString(accountNameIndex) + val accountType = cursor.getString(accountTypeIndex) + if (account == null || !(account.name == accountName && account.type == accountType)) { + if (accountName != null && accountType != null) { + account = Account(accountName, accountType) + localAddressBooks.add(LocalAddressBook(context, account, provider)) + } + } + } + + cursor.close() + provider.release() + + recyclerView!!.adapter = ImportContactAdapter(context!!, localAddressBooks, object : OnAccountSelected { + override fun accountSelected(index: Int) { + ImportContacts().execute(localAddressBooks[index]) + } + }) + } + + protected inner class ImportContacts : AsyncTask() { + internal var progressDialog: ProgressDialog? = null + + override fun onPreExecute() { + progressDialog = ProgressDialog(activity) + progressDialog!!.setTitle(R.string.import_dialog_title) + progressDialog!!.setMessage(getString(R.string.import_dialog_adding_entries)) + progressDialog!!.setCanceledOnTouchOutside(false) + progressDialog!!.setCancelable(false) + progressDialog!!.isIndeterminate = false + progressDialog!!.setIcon(R.drawable.ic_import_export_black) + progressDialog!!.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL) + progressDialog!!.show() + } + + override fun doInBackground(vararg addressBooks: LocalAddressBook): ResultFragment.ImportResult { + return importContacts(addressBooks[0]) + } + + override fun onProgressUpdate(vararg values: Int?) { + if (progressDialog != null) + progressDialog!!.progress = values[0]!! + } + + override fun onPostExecute(result: ResultFragment.ImportResult) { + progressDialog!!.dismiss() + (activity as ResultFragment.OnImportCallback).onImportResult(result) + } + + private fun importContacts(localAddressBook: LocalAddressBook): ResultFragment.ImportResult { + val result = ResultFragment.ImportResult() + try { + val addressBook = LocalAddressBook.findByUid(context!!, + context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!, + account, info!!.uid) + val localContacts = localAddressBook.all + val total = localContacts.size + progressDialog!!.max = total + result.total = total.toLong() + var progress = 0 + for (currentLocalContact in localContacts) { + val contact = currentLocalContact.contact + + try { + val localContact = LocalContact(addressBook, contact, null, null) + localContact.createAsDirty() + result.added++ + } catch (e: ContactsStorageException) { + e.printStackTrace() + result.e = e + } + + publishProgress(++progress) + } + } catch (e: Exception) { + result.e = e + } + + return result + } + } + + class ImportContactAdapter + /** + * Initialize the dataset of the Adapter. + * + * @param addressBooks containing the data to populate views to be used by RecyclerView. + */ + (context: Context, private val mAddressBooks: List, private val mOnAccountSelected: OnAccountSelected) : RecyclerView.Adapter() { + private val accountResolver: AccountResolver + + /** + * Provide a reference to the type of views that you are using (custom ViewHolder) + */ + class ViewHolder(v: View, onAccountSelected: OnAccountSelected) : RecyclerView.ViewHolder(v) { + internal val titleTextView: TextView + internal val descTextView: TextView + internal val iconImageView: ImageView + + init { + // Define click listener for the ViewHolder's View. + v.setOnClickListener { onAccountSelected.accountSelected(adapterPosition) } + titleTextView = v.findViewById(R.id.title) as TextView + descTextView = v.findViewById(R.id.description) as TextView + iconImageView = v.findViewById(R.id.icon) as ImageView + } + } + + init { + accountResolver = AccountResolver(context) + } + + // Create new views (invoked by the layout manager) + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { + // Create a new view. + val v = LayoutInflater.from(viewGroup.context) + .inflate(R.layout.import_content_list_account, viewGroup, false) + + return ViewHolder(v, mOnAccountSelected) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + viewHolder.titleTextView.text = mAddressBooks[position].account.name + val accountInfo = accountResolver.resolve(mAddressBooks[position].account.type) + viewHolder.descTextView.text = accountInfo.name + viewHolder.iconImageView.setImageDrawable(accountInfo.icon) + } + + override fun getItemCount(): Int { + return mAddressBooks.size + } + + companion object { + private val TAG = "ImportContactAdapter" + } + } + + interface OnAccountSelected { + fun accountSelected(index: Int) + } + + class DividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() { + + private val mDivider: Drawable? + + init { + val a = context.obtainStyledAttributes(ATTRS) + mDivider = a.getDrawable(0) + a.recycle() + } + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State?) { + drawVertical(c, parent) + } + + fun drawVertical(c: Canvas, parent: RecyclerView) { + val left = parent.paddingLeft + val right = parent.width - parent.paddingRight + + val childCount = parent.childCount + for (i in 0 until childCount) { + val child = parent.getChildAt(i) + val params = child + .layoutParams as RecyclerView.LayoutParams + val top = child.bottom + params.bottomMargin + val bottom = top + mDivider!!.intrinsicHeight + mDivider.setBounds(left, top, right, bottom) + mDivider.draw(c) + } + } + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State?) { + outRect.set(0, 0, 0, mDivider!!.intrinsicHeight) + } + + companion object { + + private val ATTRS = intArrayOf(android.R.attr.listDivider) + } + } + + companion object { + + fun newInstance(account: Account, info: CollectionInfo): LocalContactImportFragment { + val frag = LocalContactImportFragment() + val args = Bundle(1) + args.putParcelable(KEY_ACCOUNT, account) + args.putSerializable(KEY_COLLECTION_INFO, info) + frag.arguments = args + + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.java deleted file mode 100644 index b8fffb4c..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.etesync.syncadapter.ui.importlocal; - -import android.app.Activity; -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; -import android.support.v7.app.AlertDialog; - -import com.etesync.syncadapter.R; - -import java.io.Serializable; - -/** - * Created by tal on 30/03/17. - */ - -public class ResultFragment extends DialogFragment { - private static final String KEY_RESULT = "result"; - private ImportResult result; - - public static ResultFragment newInstance(ImportResult result) { - Bundle args = new Bundle(); - args.putSerializable(KEY_RESULT, result); - ResultFragment fragment = new ResultFragment(); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - result = (ImportResult) getArguments().getSerializable(KEY_RESULT); - } - - @Override - public void onDismiss(DialogInterface dialog) { - super.onDismiss(dialog); - Activity activity = getActivity(); - if (activity instanceof DialogInterface) { - ((DialogInterface)activity).dismiss(); - } - } - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - int icon; - int title; - String msg; - if (result.isFailed()) { - icon = R.drawable.ic_error_dark; - title = R.string.import_dialog_failed_title; - msg = result.e.getLocalizedMessage(); - } else { - icon = R.drawable.ic_import_export_black; - title = R.string.import_dialog_title; - msg = getString(R.string.import_dialog_success, result.total, result.added, result.updated, result.getSkipped()); - } - return new AlertDialog.Builder(getActivity()) - .setTitle(title) - .setIcon(icon) - .setMessage(msg) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // dismiss - } - }) - .create(); - } - - public static class ImportResult implements Serializable { - public long total; - public long added; - public long updated; - public Exception e; - - public boolean isFailed() { - return (e != null); - } - - public long getSkipped() { - return total - (added + updated); - } - - @java.lang.Override - @java.lang.SuppressWarnings("all") - public java.lang.String toString() { - return "ResultFragment.ImportResult(total=" + this.total + ", added=" + this.added + ", updated=" + this.updated + ", e=" + this.e + ")"; - } - } - - public interface OnImportCallback { - void onImportResult(ImportResult importResult); - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt new file mode 100644 index 00000000..50edeb70 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt @@ -0,0 +1,87 @@ +package com.etesync.syncadapter.ui.importlocal + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.support.v7.app.AlertDialog +import com.etesync.syncadapter.R +import java.io.Serializable + +/** + * Created by tal on 30/03/17. + */ + +class ResultFragment : DialogFragment() { + private var result: ImportResult? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + result = arguments!!.getSerializable(KEY_RESULT) as ImportResult + } + + override fun onDismiss(dialog: DialogInterface?) { + super.onDismiss(dialog) + val activity = activity + if (activity is DialogInterface) { + (activity as DialogInterface).dismiss() + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val icon: Int + val title: Int + val msg: String + if (result!!.isFailed) { + icon = R.drawable.ic_error_dark + title = R.string.import_dialog_failed_title + msg = result!!.e!!.localizedMessage + } else { + icon = R.drawable.ic_import_export_black + title = R.string.import_dialog_title + msg = getString(R.string.import_dialog_success, result!!.total, result!!.added, result!!.updated, result!!.skipped) + } + return AlertDialog.Builder(activity!!) + .setTitle(title) + .setIcon(icon) + .setMessage(msg) + .setPositiveButton(android.R.string.ok) { dialog, which -> + // dismiss + } + .create() + } + + class ImportResult : Serializable { + var total: Long = 0 + var added: Long = 0 + var updated: Long = 0 + var e: Exception? = null + + val isFailed: Boolean + get() = e != null + + val skipped: Long + get() = total - (added + updated) + + override fun toString(): String { + return "ResultFragment.ImportResult(total=" + this.total + ", added=" + this.added + ", updated=" + this.updated + ", e=" + this.e + ")" + } + } + + interface OnImportCallback { + fun onImportResult(importResult: ImportResult) + } + + companion object { + private val KEY_RESULT = "result" + + fun newInstance(result: ImportResult): ResultFragment { + val args = Bundle() + args.putSerializable(KEY_RESULT, result) + val fragment = ResultFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.java deleted file mode 100644 index 01322a56..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.etesync.syncadapter.ui.importlocal; - -/** - * Created by tal on 30/03/17. - */ - -public interface SelectImportMethod { - void importFile(); - - void importAccount(); -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.kt new file mode 100644 index 00000000..f2bf6aa0 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.kt @@ -0,0 +1,11 @@ +package com.etesync.syncadapter.ui.importlocal + +/** + * Created by tal on 30/03/17. + */ + +interface SelectImportMethod { + fun importFile() + + fun importAccount() +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.java deleted file mode 100644 index 40ce849b..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui.journalviewer; - -import android.content.Context; -import android.os.AsyncTask; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.ListFragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.EntryEntity; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.JournalModel; -import com.etesync.syncadapter.model.SyncEntry; -import com.etesync.syncadapter.ui.JournalItemActivity; - -import java.util.List; - -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; - -public class ListEntriesFragment extends ListFragment implements AdapterView.OnItemClickListener { - protected static final String EXTRA_COLLECTION_INFO = "collectionInfo"; - - private EntityDataStore data; - private CollectionInfo info; - private JournalEntity journalEntity; - private AsyncTask asyncTask; - - private TextView emptyTextView; - - public static ListEntriesFragment newInstance(CollectionInfo info) { - ListEntriesFragment frag = new ListEntriesFragment(); - Bundle args = new Bundle(1); - args.putSerializable(EXTRA_COLLECTION_INFO, info); - frag.setArguments(args); - return frag; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - data = ((App) getContext().getApplicationContext()).getData(); - info = (CollectionInfo) getArguments().getSerializable(EXTRA_COLLECTION_INFO); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - getActivity().setTitle(info.displayName); - View view = inflater.inflate(R.layout.journal_viewer_list, container, false); - - //This is instead of setEmptyText() function because of Google bug - //See: https://code.google.com/p/android/issues/detail?id=21742 - emptyTextView = (TextView) view.findViewById(android.R.id.empty); - return view; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - asyncTask = new JournalFetch().execute(); - - getListView().setOnItemClickListener(this); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - if (asyncTask != null) - asyncTask.cancel(true); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - EntryEntity entry = (EntryEntity) getListAdapter().getItem(position); - startActivity(JournalItemActivity.newIntent(getContext(), info, entry.getContent())); - } - - class EntriesListAdapter extends ArrayAdapter { - EntriesListAdapter(Context context) { - super(context, R.layout.journal_viewer_list_item); - } - - @Override - @NonNull - public View getView(int position, View v, @NonNull ViewGroup parent) { - if (v == null) - v = LayoutInflater.from(getContext()).inflate(R.layout.journal_viewer_list_item, parent, false); - - EntryEntity entryEntity = getItem(position); - - TextView tv = (TextView) v.findViewById(R.id.title); - - // FIXME: hacky way to make it show sensible info - CollectionInfo info = journalEntity.getInfo(); - setJournalEntryView(v, info, entryEntity.getContent()); - - return v; - } - } - - private static String getLine(String content, String prefix) { - if (content == null) { - return null; - } - - int start = content.indexOf(prefix); - if (start >= 0) { - int end = content.indexOf("\n", start); - content = content.substring(start + prefix.length(), end); - } else { - content = null; - } - return content; - } - - public static void setJournalEntryView(View v, CollectionInfo info, SyncEntry syncEntry) { - - TextView tv = (TextView) v.findViewById(R.id.title); - - // FIXME: hacky way to make it show sensible info - String fullContent = syncEntry.getContent(); - String prefix; - if (info.type == CollectionInfo.Type.CALENDAR) { - prefix = "SUMMARY:"; - } else { - prefix = "FN:"; - } - String content = getLine(fullContent, prefix); - content = (content != null) ? content : "Not found"; - tv.setText(content); - - tv = (TextView) v.findViewById(R.id.description); - content = getLine(fullContent, "UID:"); - content = "UID: " + ((content != null) ? content : "Not found"); - tv.setText(content); - - ImageView action = (ImageView) v.findViewById(R.id.action); - switch (syncEntry.getAction()) { - case ADD: - action.setImageResource(R.drawable.action_add); - break; - case CHANGE: - action.setImageResource(R.drawable.action_change); - break; - case DELETE: - action.setImageResource(R.drawable.action_delete); - break; - } - } - - private class JournalFetch extends AsyncTask> { - - @Override - protected List doInBackground(Void... voids) { - journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), info.uid); - return data.select(EntryEntity.class).where(EntryEntity.JOURNAL.eq(journalEntity)).orderBy(EntryEntity.ID.desc()).get().toList(); - } - - @Override - protected void onPostExecute(List result) { - EntriesListAdapter listAdapter = new EntriesListAdapter(getContext()); - setListAdapter(listAdapter); - - listAdapter.addAll(result); - - emptyTextView.setText(getString(R.string.journal_entries_list_empty)); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.kt new file mode 100644 index 00000000..d8c94c0e --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.kt @@ -0,0 +1,162 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.journalviewer + +import android.content.Context +import android.os.AsyncTask +import android.os.Bundle +import android.support.v4.app.ListFragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.TextView +import com.etesync.syncadapter.App +import com.etesync.syncadapter.R +import com.etesync.syncadapter.model.* +import com.etesync.syncadapter.ui.JournalItemActivity +import io.requery.Persistable +import io.requery.sql.EntityDataStore + +class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { + + private var data: EntityDataStore? = null + private lateinit var info: CollectionInfo + private var journalEntity: JournalEntity? = null + private var asyncTask: AsyncTask<*, *, *>? = null + + private var emptyTextView: TextView? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + data = (context!!.applicationContext as App).data + info = arguments!!.getSerializable(EXTRA_COLLECTION_INFO) as CollectionInfo + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity!!.title = info.displayName + val view = inflater.inflate(R.layout.journal_viewer_list, container, false) + + //This is instead of setEmptyText() function because of Google bug + //See: https://code.google.com/p/android/issues/detail?id=21742 + emptyTextView = view.findViewById(android.R.id.empty) as TextView + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + asyncTask = JournalFetch().execute() + + listView.onItemClickListener = this + } + + override fun onDestroyView() { + super.onDestroyView() + if (asyncTask != null) + asyncTask!!.cancel(true) + } + + override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val entry = listAdapter.getItem(position) as EntryEntity + startActivity(JournalItemActivity.newIntent(context!!, info, entry.content)) + } + + internal inner class EntriesListAdapter(context: Context) : ArrayAdapter(context, R.layout.journal_viewer_list_item) { + + override fun getView(position: Int, v: View?, parent: ViewGroup): View { + var v = v + if (v == null) + v = LayoutInflater.from(context).inflate(R.layout.journal_viewer_list_item, parent, false) + + val entryEntity = getItem(position) + + val tv = v!!.findViewById(R.id.title) as TextView + + // FIXME: hacky way to make it show sensible info + val info = journalEntity!!.info + setJournalEntryView(v, info, entryEntity!!.content) + + return v + } + } + + private inner class JournalFetch : AsyncTask>() { + + override fun doInBackground(vararg voids: Void): List { + journalEntity = JournalModel.Journal.fetch(data!!, info!!.getServiceEntity(data), info!!.uid) + return data!!.select(EntryEntity::class.java).where(EntryEntity.JOURNAL.eq(journalEntity)).orderBy(EntryEntity.ID.desc()).get().toList() + } + + override fun onPostExecute(result: List) { + val listAdapter = EntriesListAdapter(context!!) + setListAdapter(listAdapter) + + listAdapter.addAll(result) + + emptyTextView!!.text = getString(R.string.journal_entries_list_empty) + } + } + + companion object { + protected val EXTRA_COLLECTION_INFO = "collectionInfo" + + fun newInstance(info: CollectionInfo): ListEntriesFragment { + val frag = ListEntriesFragment() + val args = Bundle(1) + args.putSerializable(EXTRA_COLLECTION_INFO, info) + frag.arguments = args + return frag + } + + private fun getLine(content: String?, prefix: String): String? { + var content: String? = content ?: return null + + val start = content!!.indexOf(prefix) + if (start >= 0) { + val end = content.indexOf("\n", start) + content = content.substring(start + prefix.length, end) + } else { + content = null + } + return content + } + + fun setJournalEntryView(v: View, info: CollectionInfo, syncEntry: SyncEntry) { + + var tv = v.findViewById(R.id.title) as TextView + + // FIXME: hacky way to make it show sensible info + val fullContent = syncEntry.content + val prefix: String + if (info.type == CollectionInfo.Type.CALENDAR) { + prefix = "SUMMARY:" + } else { + prefix = "FN:" + } + var content = getLine(fullContent, prefix) + content = content ?: "Not found" + tv.text = content + + tv = v.findViewById(R.id.description) as TextView + content = getLine(fullContent, "UID:") + content = "UID: " + (content ?: "Not found") + tv.text = content + + val action = v.findViewById(R.id.action) as ImageView + when (syncEntry.action) { + SyncEntry.Actions.ADD -> action.setImageResource(R.drawable.action_add) + SyncEntry.Actions.CHANGE -> action.setImageResource(R.drawable.action_change) + SyncEntry.Actions.DELETE -> action.setImageResource(R.drawable.action_delete) + } + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.java deleted file mode 100644 index 9b285f39..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ -package com.etesync.syncadapter.ui.setup; - -import android.content.Context; -import android.support.annotation.NonNull; - -import com.etesync.syncadapter.HttpClient; -import com.etesync.syncadapter.journalmanager.Crypto; -import com.etesync.syncadapter.journalmanager.Exceptions; -import com.etesync.syncadapter.journalmanager.JournalAuthenticator; -import com.etesync.syncadapter.log.StringHandler; -import com.etesync.syncadapter.model.CollectionInfo; - -import java.io.IOException; -import java.io.Serializable; -import java.net.URI; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; - -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; - -public class BaseConfigurationFinder { - protected final Context context; - protected final LoginCredentials credentials; - - protected final Logger log; - protected final StringHandler logBuffer = new StringHandler(); - protected OkHttpClient httpClient; - - public BaseConfigurationFinder(@NonNull Context context, @NonNull LoginCredentials credentials) { - this.context = context; - this.credentials = credentials; - - log = Logger.getLogger("syncadapter.BaseConfigurationFinder"); - log.setLevel(Level.FINEST); - log.addHandler(logBuffer); - - httpClient = HttpClient.create(context, log); - } - - - public Configuration findInitialConfiguration() { - boolean failed = false; - Configuration.ServiceInfo - cardDavConfig = findInitialConfiguration(CollectionInfo.Type.ADDRESS_BOOK), - calDavConfig = findInitialConfiguration(CollectionInfo.Type.CALENDAR); - - JournalAuthenticator authenticator = new JournalAuthenticator(httpClient, HttpUrl.get(credentials.uri)); - - String authtoken = null; - try { - authtoken = authenticator.getAuthToken(credentials.userName, credentials.password); - } catch (Exceptions.HttpException|IOException e) { - log.warning(e.getMessage()); - - failed = true; - } - - return new Configuration( - credentials.uri, - credentials.userName, authtoken, - cardDavConfig, calDavConfig, - logBuffer.toString(), failed - ); - } - - protected Configuration.ServiceInfo findInitialConfiguration(@NonNull CollectionInfo.Type service) { - // put discovered information here - final Configuration.ServiceInfo config = new Configuration.ServiceInfo(); - log.info("Finding initial " + service.toString() + " service configuration"); - - return config; - } - - // data classes - - public static class Configuration implements Serializable { - // We have to use URI here because HttpUrl is not serializable! - - public Configuration(final URI url, final String userName, final String authtoken, final ServiceInfo cardDAV, final ServiceInfo calDAV, final String logs, final boolean failed) { - this.url = url; - this.userName = userName; - this.authtoken = authtoken; - this.cardDAV = cardDAV; - this.calDAV = calDAV; - this.logs = logs; - this.failed = failed; - } - - public static class ServiceInfo implements Serializable { - public final Map collections = new HashMap<>(); - - @java.lang.Override - @java.lang.SuppressWarnings("all") - public java.lang.String toString() { - return "BaseConfigurationFinder.Configuration.ServiceInfo(collections=" + this.collections + ")"; - } - } - - public final URI url; - - public final String userName, authtoken; - public String rawPassword; - public String password; - public Crypto.AsymmetricKeyPair keyPair; - - public final ServiceInfo cardDAV; - public final ServiceInfo calDAV; - - public final String logs; - - public Throwable error; - - private final boolean failed; - - public boolean isFailed() { - return failed; - } - - @java.lang.Override - @java.lang.SuppressWarnings("all") - public java.lang.String toString() { - return "BaseConfigurationFinder.Configuration(url=" + this.url + ", userName=" + this.userName + ", keyPair=" + this.keyPair + ", cardDAV=" + this.cardDAV + ", calDAV=" + this.calDAV + ", error=" + this.error + ", failed=" + this.isFailed() + ")"; - } - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt new file mode 100644 index 00000000..9797889e --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt @@ -0,0 +1,102 @@ +/* + * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ +package com.etesync.syncadapter.ui.setup + +import android.content.Context +import com.etesync.syncadapter.HttpClient +import com.etesync.syncadapter.journalmanager.Crypto +import com.etesync.syncadapter.journalmanager.Exceptions +import com.etesync.syncadapter.journalmanager.JournalAuthenticator +import com.etesync.syncadapter.log.StringHandler +import com.etesync.syncadapter.model.CollectionInfo +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import java.io.IOException +import java.io.Serializable +import java.net.URI +import java.util.* +import java.util.logging.Level +import java.util.logging.Logger + +class BaseConfigurationFinder(protected val context: Context, protected val credentials: LoginCredentials) { + + protected val log: Logger + protected val logBuffer = StringHandler() + protected var httpClient: OkHttpClient + + init { + + log = Logger.getLogger("syncadapter.BaseConfigurationFinder") + log.level = Level.FINEST + log.addHandler(logBuffer) + + httpClient = HttpClient.create(context, log) + } + + + fun findInitialConfiguration(): Configuration { + var failed = false + val cardDavConfig = findInitialConfiguration(CollectionInfo.Type.ADDRESS_BOOK) + val calDavConfig = findInitialConfiguration(CollectionInfo.Type.CALENDAR) + + val authenticator = JournalAuthenticator(httpClient, HttpUrl.get(credentials.uri!!)!!) + + var authtoken: String? = null + try { + authtoken = authenticator.getAuthToken(credentials.userName, credentials.password) + } catch (e: Exceptions.HttpException) { + log.warning(e.message) + + failed = true + } catch (e: IOException) { + log.warning(e.message) + failed = true + } + + return Configuration( + credentials.uri, + credentials.userName, authtoken, + cardDavConfig, calDavConfig, + logBuffer.toString(), failed + ) + } + + protected fun findInitialConfiguration(service: CollectionInfo.Type): Configuration.ServiceInfo { + // put discovered information here + val config = Configuration.ServiceInfo() + log.info("Finding initial " + service.toString() + " service configuration") + + return config + } + + // data classes + + class Configuration + // We have to use URI here because HttpUrl is not serializable! + + (val url: URI, val userName: String, val authtoken: String?, val cardDAV: ServiceInfo, val calDAV: ServiceInfo, val logs: String, val isFailed: Boolean) : Serializable { + var rawPassword: String? = null + var password: String? = null + var keyPair: Crypto.AsymmetricKeyPair? = null + + var error: Throwable? = null + + class ServiceInfo : Serializable { + val collections: Map = HashMap() + + override fun toString(): String { + return "BaseConfigurationFinder.Configuration.ServiceInfo(collections=" + this.collections + ")" + } + } + + override fun toString(): String { + return "BaseConfigurationFinder.Configuration(url=" + this.url + ", userName=" + this.userName + ", keyPair=" + this.keyPair + ", cardDAV=" + this.cardDAV + ", calDAV=" + this.calDAV + ", error=" + this.error + ", failed=" + this.isFailed + ")" + } + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.java deleted file mode 100644 index 3b69d691..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui.setup; - -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; -import android.support.v7.app.AlertDialog; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.ui.DebugInfoActivity; -import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration; - -public class DetectConfigurationFragment extends DialogFragment implements LoaderManager.LoaderCallbacks { - protected static final String ARG_LOGIN_CREDENTIALS = "credentials"; - - public static DetectConfigurationFragment newInstance(LoginCredentials credentials) { - DetectConfigurationFragment frag = new DetectConfigurationFragment(); - Bundle args = new Bundle(1); - args.putParcelable(ARG_LOGIN_CREDENTIALS, credentials); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - ProgressDialog progress = new ProgressDialog(getActivity()); - progress.setTitle(R.string.login_configuration_detection); - progress.setMessage(getString(R.string.login_querying_server)); - progress.setIndeterminate(true); - progress.setCanceledOnTouchOutside(false); - setCancelable(false); - return progress; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getLoaderManager().initLoader(0, getArguments(), this); - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new ServerConfigurationLoader(getContext(), (LoginCredentials)args.getParcelable(ARG_LOGIN_CREDENTIALS)); - } - - @Override - public void onLoadFinished(Loader loader, Configuration data) { - if (data != null) { - if (data.isFailed()) - // no service found: show error message - getFragmentManager().beginTransaction() - .add(NothingDetectedFragment.newInstance(data.logs), null) - .commitAllowingStateLoss(); - else - // service found: continue - getFragmentManager().beginTransaction() - .replace(android.R.id.content, EncryptionDetailsFragment.newInstance(data)) - .addToBackStack(null) - .commitAllowingStateLoss(); - } else - App.log.severe("Configuration detection failed"); - - dismissAllowingStateLoss(); - } - - @Override - public void onLoaderReset(Loader loader) { - } - - - public static class NothingDetectedFragment extends DialogFragment { - private static String KEY_LOGS = "logs"; - - public static NothingDetectedFragment newInstance(String logs) { - Bundle args = new Bundle(); - args.putString(KEY_LOGS, logs); - NothingDetectedFragment fragment = new NothingDetectedFragment(); - fragment.setArguments(args); - return fragment; - } - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - return new AlertDialog.Builder(getActivity()) - .setTitle(R.string.login_configuration_detection) - .setIcon(R.drawable.ic_error_dark) - .setMessage(R.string.login_wrong_username_or_password) - .setNeutralButton(R.string.login_view_logs, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(getActivity(), DebugInfoActivity.class); - intent.putExtra(DebugInfoActivity.KEY_LOGS, getArguments().getString(KEY_LOGS)); - startActivity(intent); - } - }) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // dismiss - } - }) - .create(); - } - } - - static class ServerConfigurationLoader extends AsyncTaskLoader { - final Context context; - final LoginCredentials credentials; - - public ServerConfigurationLoader(Context context, LoginCredentials credentials) { - super(context); - this.context = context; - this.credentials = credentials; - } - - @Override - protected void onStartLoading() { - forceLoad(); - } - - @Override - public Configuration loadInBackground() { - return new BaseConfigurationFinder(context, credentials).findInitialConfiguration(); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt new file mode 100644 index 00000000..dc399b31 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt @@ -0,0 +1,123 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.setup + +import android.app.Dialog +import android.app.ProgressDialog +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.support.v4.app.LoaderManager +import android.support.v4.content.AsyncTaskLoader +import android.support.v4.content.Loader +import android.support.v7.app.AlertDialog +import com.etesync.syncadapter.App +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.DebugInfoActivity +import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration + +class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallbacks { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val progress = ProgressDialog(activity) + progress.setTitle(R.string.login_configuration_detection) + progress.setMessage(getString(R.string.login_querying_server)) + progress.isIndeterminate = true + progress.setCanceledOnTouchOutside(false) + isCancelable = false + return progress + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + loaderManager.initLoader(0, arguments, this) + } + + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + return ServerConfigurationLoader(context!!, args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials) + } + + override fun onLoadFinished(loader: Loader, data: Configuration?) { + if (data != null) { + if (data.isFailed) + // no service found: show error message + fragmentManager!!.beginTransaction() + .add(NothingDetectedFragment.newInstance(data.logs), null) + .commitAllowingStateLoss() + else + // service found: continue + fragmentManager!!.beginTransaction() + .replace(android.R.id.content, EncryptionDetailsFragment.newInstance(data)) + .addToBackStack(null) + .commitAllowingStateLoss() + } else + App.log.severe("Configuration detection failed") + + dismissAllowingStateLoss() + } + + override fun onLoaderReset(loader: Loader) {} + + + class NothingDetectedFragment : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialog.Builder(activity!!) + .setTitle(R.string.login_configuration_detection) + .setIcon(R.drawable.ic_error_dark) + .setMessage(R.string.login_wrong_username_or_password) + .setNeutralButton(R.string.login_view_logs) { dialog, which -> + val intent = Intent(activity, DebugInfoActivity::class.java) + intent.putExtra(DebugInfoActivity.KEY_LOGS, arguments!!.getString(KEY_LOGS)) + startActivity(intent) + } + .setPositiveButton(android.R.string.ok) { dialog, which -> + // dismiss + } + .create() + } + + companion object { + private val KEY_LOGS = "logs" + + fun newInstance(logs: String): NothingDetectedFragment { + val args = Bundle() + args.putString(KEY_LOGS, logs) + val fragment = NothingDetectedFragment() + fragment.arguments = args + return fragment + } + } + } + + internal class ServerConfigurationLoader(context: Context, val credentials: LoginCredentials) : AsyncTaskLoader(context) { + + override fun onStartLoading() { + forceLoad() + } + + override fun loadInBackground(): Configuration? { + return BaseConfigurationFinder(context, credentials).findInitialConfiguration() + } + } + + companion object { + protected val ARG_LOGIN_CREDENTIALS = "credentials" + + fun newInstance(credentials: LoginCredentials): DetectConfigurationFragment { + val frag = DetectConfigurationFragment() + val args = Bundle(1) + args.putParcelable(ARG_LOGIN_CREDENTIALS, credentials) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/EncryptionDetailsFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/EncryptionDetailsFragment.java deleted file mode 100644 index e4cadee5..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/EncryptionDetailsFragment.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui.setup; - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.TextView; - -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.ui.widget.EditPassword; - -public class EncryptionDetailsFragment extends Fragment { - - private static final String KEY_CONFIG = "config"; - EditPassword editPassword = null; - - - public static EncryptionDetailsFragment newInstance(BaseConfigurationFinder.Configuration config) { - EncryptionDetailsFragment frag = new EncryptionDetailsFragment(); - Bundle args = new Bundle(1); - args.putSerializable(KEY_CONFIG, config); - frag.setArguments(args); - return frag; - } - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View v = inflater.inflate(R.layout.login_encryption_details, container, false); - - Button btnBack = (Button)v.findViewById(R.id.back); - btnBack.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - getFragmentManager().popBackStack(); - } - }); - - final BaseConfigurationFinder.Configuration config = (BaseConfigurationFinder.Configuration)getArguments().getSerializable(KEY_CONFIG); - - TextView accountName = (TextView)v.findViewById(R.id.account_name); - accountName.setText(getString(R.string.login_encryption_account_label) + " " + config.userName); - - editPassword = (EditPassword) v.findViewById(R.id.encryption_password); - - Button btnCreate = (Button)v.findViewById(R.id.create_account); - btnCreate.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (validateEncryptionData(config) == null) { - return; - } - - SetupEncryptionFragment.newInstance(config).show(getFragmentManager(), null); - } - }); - - final TextView extra_details = (TextView) v.findViewById(R.id.encryption_extra_info); - extra_details.setText(getString(R.string.login_encryption_extra_info, Constants.faqUri.buildUpon().appendEncodedPath("#securing-etesync").build().toString())); - - return v; - } - - private BaseConfigurationFinder.Configuration validateEncryptionData(BaseConfigurationFinder.Configuration config) { - boolean valid = true; - String password = editPassword.getText().toString(); - if (password.isEmpty()) { - editPassword.setError(getString(R.string.login_password_required)); - valid = false; - } - - config.rawPassword = password; - - return valid ? config : null; - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/EncryptionDetailsFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/EncryptionDetailsFragment.kt new file mode 100644 index 00000000..1863edfd --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/EncryptionDetailsFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.setup + +import android.os.Bundle +import android.support.v4.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView + +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.widget.EditPassword + +class EncryptionDetailsFragment : Fragment() { + internal var editPassword: EditPassword? = null + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val v = inflater.inflate(R.layout.login_encryption_details, container, false) + + val btnBack = v.findViewById(R.id.back) as Button + btnBack.setOnClickListener { fragmentManager!!.popBackStack() } + + val config = arguments!!.getSerializable(KEY_CONFIG) as BaseConfigurationFinder.Configuration + + val accountName = v.findViewById(R.id.account_name) as TextView + accountName.text = getString(R.string.login_encryption_account_label) + " " + config.userName + + editPassword = v.findViewById(R.id.encryption_password) as EditPassword + + val btnCreate = v.findViewById(R.id.create_account) as Button + btnCreate.setOnClickListener(View.OnClickListener { + if (validateEncryptionData(config) == null) { + return@OnClickListener + } + + SetupEncryptionFragment.newInstance(config).show(fragmentManager!!, null) + }) + + val extra_details = v.findViewById(R.id.encryption_extra_info) as TextView + extra_details.text = getString(R.string.login_encryption_extra_info, Constants.faqUri.buildUpon().appendEncodedPath("#securing-etesync").build().toString()) + + return v + } + + private fun validateEncryptionData(config: BaseConfigurationFinder.Configuration): BaseConfigurationFinder.Configuration? { + var valid = true + val password = editPassword!!.text.toString() + if (password.isEmpty()) { + editPassword!!.setError(getString(R.string.login_password_required)) + valid = false + } + + config.rawPassword = password + + return if (valid) config else null + } + + companion object { + + private val KEY_CONFIG = "config" + + + fun newInstance(config: BaseConfigurationFinder.Configuration): EncryptionDetailsFragment { + val frag = EncryptionDetailsFragment() + val args = Bundle(1) + args.putSerializable(KEY_CONFIG, config) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginActivity.java deleted file mode 100644 index 6e63d8d9..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginActivity.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui.setup; - -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; - -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.ui.BaseActivity; -import com.etesync.syncadapter.ui.WebViewActivity; - -/** - * Activity to initially connect to a server and create an account. - * Fields for server/user data can be pre-filled with extras in the Intent. - */ -public class LoginActivity extends BaseActivity { - /** - * When set, and {@link #EXTRA_PASSWORD} is set too, the user name field will be set to this value. - */ - public static final String EXTRA_USERNAME = "username"; - - /** - * When set, the password field will be set to this value. - */ - public static final String EXTRA_PASSWORD = "password"; - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState == null) - // first call, add fragment - getSupportFragmentManager().beginTransaction() - .replace(android.R.id.content, new LoginCredentialsFragment()) - .commit(); - - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.activity_login, menu); - return true; - } - - public void showHelp(MenuItem item) { - WebViewActivity.openUrl(this, Constants.helpUri); - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginActivity.kt new file mode 100644 index 00000000..47cf3388 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginActivity.kt @@ -0,0 +1,58 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.setup + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem + +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.BaseActivity +import com.etesync.syncadapter.ui.WebViewActivity + +/** + * Activity to initially connect to a server and create an account. + * Fields for server/user data can be pre-filled with extras in the Intent. + */ +class LoginActivity : BaseActivity() { + + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (savedInstanceState == null) + // first call, add fragment + supportFragmentManager.beginTransaction() + .replace(android.R.id.content, LoginCredentialsFragment()) + .commit() + + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.activity_login, menu) + return true + } + + fun showHelp(item: MenuItem) { + WebViewActivity.openUrl(this, Constants.helpUri) + } + + companion object { + /** + * When set, and [.EXTRA_PASSWORD] is set too, the user name field will be set to this value. + */ + val EXTRA_USERNAME = "username" + + /** + * When set, the password field will be set to this value. + */ + val EXTRA_PASSWORD = "password" + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.java deleted file mode 100644 index fd6eee51..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui.setup; - -import android.os.Parcel; -import android.os.Parcelable; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.Constants; - -import java.net.URI; -import java.net.URISyntaxException; - -public class LoginCredentials implements Parcelable { - public final URI uri; - public final String userName, password; - - public LoginCredentials(URI uri, String userName, String password) { - this.userName = userName; - this.password = password; - - if (uri == null) { - try { - uri = new URI(Constants.serviceUrl.toString()); - } catch (URISyntaxException e) { - App.log.severe("Should never happen, it's a constant"); - } - } - this.uri = uri; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeSerializable(uri); - dest.writeString(userName); - dest.writeString(password); - } - - public static final Creator CREATOR = new Creator() { - @Override - public LoginCredentials createFromParcel(Parcel source) { - return new LoginCredentials( - (URI)source.readSerializable(), - source.readString(), source.readString() - ); - } - - @Override - public LoginCredentials[] newArray(int size) { - return new LoginCredentials[size]; - } - }; -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.kt new file mode 100644 index 00000000..47d28552 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.kt @@ -0,0 +1,62 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.setup + +import android.os.Parcel +import android.os.Parcelable + +import com.etesync.syncadapter.App +import com.etesync.syncadapter.Constants + +import java.net.URI +import java.net.URISyntaxException + +class LoginCredentials(uri: URI?, val userName: String, val password: String) : Parcelable { + val uri: URI? + + init { + var uri = uri + + if (uri == null) { + try { + uri = URI(Constants.serviceUrl.toString()) + } catch (e: URISyntaxException) { + App.log.severe("Should never happen, it's a constant") + } + + } + this.uri = uri + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeSerializable(uri) + dest.writeString(userName) + dest.writeString(password) + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator<*> = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): LoginCredentials { + return LoginCredentials( + source.readSerializable() as URI, + source.readString(), source.readString() + ) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) as Array + } + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsChangeFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsChangeFragment.java deleted file mode 100644 index 0f5825d6..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsChangeFragment.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui.setup; - -import android.accounts.Account; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; -import android.support.v7.app.AlertDialog; - -import java.util.logging.Level; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.InvalidAccountException; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.ui.DebugInfoActivity; -import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration; - -public class LoginCredentialsChangeFragment extends DialogFragment implements LoaderManager.LoaderCallbacks { - protected static final String ARG_LOGIN_CREDENTIALS = "credentials", - ARG_ACCOUNT = "account"; - private Account account; - - public static LoginCredentialsChangeFragment newInstance(Account account, LoginCredentials credentials) { - LoginCredentialsChangeFragment frag = new LoginCredentialsChangeFragment(); - Bundle args = new Bundle(1); - args.putParcelable(ARG_ACCOUNT, account); - args.putParcelable(ARG_LOGIN_CREDENTIALS, credentials); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - ProgressDialog progress = new ProgressDialog(getActivity()); - progress.setTitle(R.string.login_configuration_detection); - progress.setMessage(getString(R.string.login_querying_server)); - progress.setIndeterminate(true); - progress.setCanceledOnTouchOutside(false); - setCancelable(false); - return progress; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getLoaderManager().initLoader(0, getArguments(), this); - account = getArguments().getParcelable(ARG_ACCOUNT); - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new ServerConfigurationLoader(getContext(), (LoginCredentials) args.getParcelable(ARG_LOGIN_CREDENTIALS)); - } - - @Override - public void onLoadFinished(Loader loader, Configuration data) { - if (data != null) { - if (data.isFailed()) - // no service found: show error message - getFragmentManager().beginTransaction() - .add(NothingDetectedFragment.newInstance(data.logs), null) - .commitAllowingStateLoss(); - else { - final AccountSettings settings; - - try { - settings = new AccountSettings(getActivity(), account); - } catch (InvalidAccountException e) { - App.log.log(Level.INFO, "Account is invalid or doesn't exist (anymore)", e); - getActivity().finish(); - return; - } - settings.setAuthToken(data.authtoken); - } - } else - App.log.severe("Configuration detection failed"); - - dismissAllowingStateLoss(); - } - - @Override - public void onLoaderReset(Loader loader) { - } - - - public static class NothingDetectedFragment extends DialogFragment { - private static String KEY_LOGS = "logs"; - - public static NothingDetectedFragment newInstance(String logs) { - Bundle args = new Bundle(); - args.putString(KEY_LOGS, logs); - NothingDetectedFragment fragment = new NothingDetectedFragment(); - fragment.setArguments(args); - return fragment; - } - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - return new AlertDialog.Builder(getActivity()) - .setTitle(R.string.login_configuration_detection) - .setIcon(R.drawable.ic_error_dark) - .setMessage(R.string.login_wrong_username_or_password) - .setNeutralButton(R.string.login_view_logs, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(getActivity(), DebugInfoActivity.class); - intent.putExtra(DebugInfoActivity.KEY_LOGS, getArguments().getString(KEY_LOGS)); - startActivity(intent); - } - }) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // dismiss - } - }) - .create(); - } - } - - static class ServerConfigurationLoader extends AsyncTaskLoader { - final Context context; - final LoginCredentials credentials; - - public ServerConfigurationLoader(Context context, LoginCredentials credentials) { - super(context); - this.context = context; - this.credentials = credentials; - } - - @Override - protected void onStartLoading() { - forceLoad(); - } - - @Override - public Configuration loadInBackground() { - return new BaseConfigurationFinder(context, credentials).findInitialConfiguration(); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsChangeFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsChangeFragment.kt new file mode 100644 index 00000000..0f792f84 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsChangeFragment.kt @@ -0,0 +1,138 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.setup + +import android.accounts.Account +import android.app.Dialog +import android.app.ProgressDialog +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.support.v4.app.LoaderManager +import android.support.v4.content.AsyncTaskLoader +import android.support.v4.content.Loader +import android.support.v7.app.AlertDialog +import com.etesync.syncadapter.AccountSettings +import com.etesync.syncadapter.App +import com.etesync.syncadapter.InvalidAccountException +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.DebugInfoActivity +import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration +import java.util.logging.Level + +class LoginCredentialsChangeFragment : DialogFragment(), LoaderManager.LoaderCallbacks { + private var account: Account? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val progress = ProgressDialog(activity) + progress.setTitle(R.string.login_configuration_detection) + progress.setMessage(getString(R.string.login_querying_server)) + progress.isIndeterminate = true + progress.setCanceledOnTouchOutside(false) + isCancelable = false + return progress + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + loaderManager.initLoader(0, arguments, this) + account = arguments!!.getParcelable(ARG_ACCOUNT) + } + + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + return ServerConfigurationLoader(context!!, args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials) + } + + override fun onLoadFinished(loader: Loader, data: Configuration?) { + if (data != null) { + if (data.isFailed) + // no service found: show error message + fragmentManager!!.beginTransaction() + .add(NothingDetectedFragment.newInstance(data.logs), null) + .commitAllowingStateLoss() + else { + val settings: AccountSettings + + try { + settings = AccountSettings(activity!!, account!!) + } catch (e: InvalidAccountException) { + App.log.log(Level.INFO, "Account is invalid or doesn't exist (anymore)", e) + activity!!.finish() + return + } + + settings.setAuthToken(data.authtoken!!) + } + } else + App.log.severe("Configuration detection failed") + + dismissAllowingStateLoss() + } + + override fun onLoaderReset(loader: Loader) {} + + + class NothingDetectedFragment : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialog.Builder(activity!!) + .setTitle(R.string.login_configuration_detection) + .setIcon(R.drawable.ic_error_dark) + .setMessage(R.string.login_wrong_username_or_password) + .setNeutralButton(R.string.login_view_logs) { dialog, which -> + val intent = Intent(activity, DebugInfoActivity::class.java) + intent.putExtra(DebugInfoActivity.KEY_LOGS, arguments!!.getString(KEY_LOGS)) + startActivity(intent) + } + .setPositiveButton(android.R.string.ok) { dialog, which -> + // dismiss + } + .create() + } + + companion object { + private val KEY_LOGS = "logs" + + fun newInstance(logs: String): NothingDetectedFragment { + val args = Bundle() + args.putString(KEY_LOGS, logs) + val fragment = NothingDetectedFragment() + fragment.arguments = args + return fragment + } + } + } + + internal class ServerConfigurationLoader(context: Context, val credentials: LoginCredentials) : AsyncTaskLoader(context) { + + override fun onStartLoading() { + forceLoad() + } + + override fun loadInBackground(): Configuration? { + return BaseConfigurationFinder(context, credentials).findInitialConfiguration() + } + } + + companion object { + protected val ARG_LOGIN_CREDENTIALS = "credentials" + protected val ARG_ACCOUNT = "account" + + fun newInstance(account: Account, credentials: LoginCredentials): LoginCredentialsChangeFragment { + val frag = LoginCredentialsChangeFragment() + val args = Bundle(1) + args.putParcelable(ARG_ACCOUNT, account) + args.putParcelable(ARG_LOGIN_CREDENTIALS, credentials) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.java deleted file mode 100644 index ccc8c039..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui.setup; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.CheckedTextView; -import android.widget.EditText; -import android.widget.TextView; - -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.ui.WebViewActivity; -import com.etesync.syncadapter.ui.widget.EditPassword; - -import net.cachapa.expandablelayout.ExpandableLayout; - -import java.net.URI; -import java.net.URISyntaxException; - -import okhttp3.HttpUrl; - -public class LoginCredentialsFragment extends Fragment { - EditText editUserName; - EditPassword editUrlPassword; - - CheckedTextView showAdvanced; - EditText customServer; - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.login_credentials_fragment, container, false); - - editUserName = (EditText) v.findViewById(R.id.user_name); - editUrlPassword = (EditPassword) v.findViewById(R.id.url_password); - showAdvanced = (CheckedTextView) v.findViewById(R.id.show_advanced); - customServer = (EditText) v.findViewById(R.id.custom_server); - - if (savedInstanceState == null) { - Activity activity = getActivity(); - Intent intent = (activity != null) ? activity.getIntent() : null; - if (intent != null) { - // we've got initial login data - String username = intent.getStringExtra(LoginActivity.EXTRA_USERNAME), - password = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD); - - editUserName.setText(username); - editUrlPassword.setText(password); - } - } - - final Button createAccount = (Button) v.findViewById(R.id.create_account); - createAccount.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Uri createUri = Constants.registrationUrl.buildUpon().appendQueryParameter("email", editUserName.getText().toString()).build(); - WebViewActivity.openUrl(getContext(), createUri); - } - }); - - final Button login = (Button) v.findViewById(R.id.login); - login.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LoginCredentials credentials = validateLoginData(); - if (credentials != null) - DetectConfigurationFragment.newInstance(credentials).show(getFragmentManager(), null); - } - }); - - final TextView forgotPassword = (TextView) v.findViewById(R.id.forgot_password); - forgotPassword.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - WebViewActivity.openUrl(getContext(), Constants.forgotPassword); - } - }); - - final ExpandableLayout advancedLayout = (ExpandableLayout) v.findViewById(R.id.advanced_layout); - - showAdvanced.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - - if (showAdvanced.isChecked()) { - showAdvanced.setChecked(false); - advancedLayout.collapse(); - } else { - showAdvanced.setChecked(true); - advancedLayout.expand(); - } - } - }); - - return v; - } - - protected LoginCredentials validateLoginData() { - boolean valid = true; - - String userName = editUserName.getText().toString(); - if (userName.isEmpty()) { - editUserName.setError(getString(R.string.login_email_address_error)); - valid = false; - } - - String password = editUrlPassword.getText().toString(); - if (password.isEmpty()) { - editUrlPassword.setError(getString(R.string.login_password_required)); - valid = false; - } - - URI uri = null; - if (showAdvanced.isChecked()) { - String server = customServer.getText().toString(); - // If this field is null, just use the default - if (!server.isEmpty()) { - HttpUrl url = HttpUrl.parse(server); - if (url != null) { - uri = url.uri(); - } else { - customServer.setError(getString(R.string.login_custom_server_error)); - valid = false; - } - } - } - - return valid ? new LoginCredentials(uri, userName, password) : null; - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt new file mode 100644 index 00000000..5f74c9ff --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt @@ -0,0 +1,120 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.setup + +import android.os.Bundle +import android.support.v4.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.CheckedTextView +import android.widget.EditText +import android.widget.TextView +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.WebViewActivity +import com.etesync.syncadapter.ui.widget.EditPassword +import net.cachapa.expandablelayout.ExpandableLayout +import okhttp3.HttpUrl +import java.net.URI + +class LoginCredentialsFragment : Fragment() { + internal lateinit var editUserName: EditText + internal lateinit var editUrlPassword: EditPassword + + internal lateinit var showAdvanced: CheckedTextView + internal lateinit var customServer: EditText + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val v = inflater.inflate(R.layout.login_credentials_fragment, container, false) + + editUserName = v.findViewById(R.id.user_name) as EditText + editUrlPassword = v.findViewById(R.id.url_password) as EditPassword + showAdvanced = v.findViewById(R.id.show_advanced) as CheckedTextView + customServer = v.findViewById(R.id.custom_server) as EditText + + if (savedInstanceState == null) { + val activity = activity + val intent = activity?.intent + if (intent != null) { + // we've got initial login data + val username = intent.getStringExtra(LoginActivity.EXTRA_USERNAME) + val password = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD) + + editUserName.setText(username) + editUrlPassword.setText(password) + } + } + + val createAccount = v.findViewById(R.id.create_account) as Button + createAccount.setOnClickListener { + val createUri = Constants.registrationUrl.buildUpon().appendQueryParameter("email", editUserName.text.toString()).build() + WebViewActivity.openUrl(context!!, createUri) + } + + val login = v.findViewById(R.id.login) as Button + login.setOnClickListener { + val credentials = validateLoginData() + if (credentials != null) + DetectConfigurationFragment.newInstance(credentials).show(fragmentManager!!, null) + } + + val forgotPassword = v.findViewById(R.id.forgot_password) as TextView + forgotPassword.setOnClickListener { WebViewActivity.openUrl(context!!, Constants.forgotPassword) } + + val advancedLayout = v.findViewById(R.id.advanced_layout) as ExpandableLayout + + showAdvanced.setOnClickListener { + if (showAdvanced.isChecked) { + showAdvanced.isChecked = false + advancedLayout.collapse() + } else { + showAdvanced.isChecked = true + advancedLayout.expand() + } + } + + return v + } + + protected fun validateLoginData(): LoginCredentials? { + var valid = true + + val userName = editUserName.text.toString() + if (userName.isEmpty()) { + editUserName.error = getString(R.string.login_email_address_error) + valid = false + } + + val password = editUrlPassword.text.toString() + if (password.isEmpty()) { + editUrlPassword.setError(getString(R.string.login_password_required)) + valid = false + } + + var uri: URI? = null + if (showAdvanced.isChecked) { + val server = customServer.text.toString() + // If this field is null, just use the default + if (!server.isEmpty()) { + val url = HttpUrl.parse(server) + if (url != null) { + uri = url.uri() + } else { + customServer.error = getString(R.string.login_custom_server_error) + valid = false + } + } + } + + return if (valid) LoginCredentials(uri, userName, password) else null + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java deleted file mode 100644 index 19aa9616..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui.setup; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.app.Activity; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.ContentResolver; -import android.content.Context; -import android.content.DialogInterface; -import android.os.AsyncTask; -import android.os.Bundle; -import android.provider.CalendarContract; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v7.app.AlertDialog; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.Constants; -import com.etesync.syncadapter.HttpClient; -import com.etesync.syncadapter.InvalidAccountException; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.journalmanager.Crypto; -import com.etesync.syncadapter.journalmanager.Exceptions; -import com.etesync.syncadapter.journalmanager.UserInfoManager; -import com.etesync.syncadapter.model.CollectionInfo; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.ServiceEntity; -import com.etesync.syncadapter.resource.LocalTaskList; -import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration; -import com.etesync.syncadapter.utils.AndroidCompat; - -import java.util.logging.Level; - -import at.bitfire.ical4android.TaskProvider; -import io.requery.Persistable; -import io.requery.sql.EntityDataStore; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; - -public class SetupEncryptionFragment extends DialogFragment { - private static final String KEY_CONFIG = "config"; - - public static SetupEncryptionFragment newInstance(BaseConfigurationFinder.Configuration config) { - SetupEncryptionFragment frag = new SetupEncryptionFragment(); - Bundle args = new Bundle(1); - args.putSerializable(KEY_CONFIG, config); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - ProgressDialog progress = new ProgressDialog(getActivity()); - progress.setTitle(R.string.login_encryption_setup_title); - progress.setMessage(getString(R.string.login_encryption_setup)); - progress.setIndeterminate(true); - progress.setCanceledOnTouchOutside(false); - setCancelable(false); - return progress; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - new SetupEncryptionLoader(getContext(), (Configuration) getArguments().getSerializable(KEY_CONFIG)).execute(); - } - - private class SetupEncryptionLoader extends AsyncTask { - final Context context; - final Configuration config; - - public SetupEncryptionLoader(Context context, Configuration config) { - super(); - this.context = context; - this.config = config; - } - - @Override - protected void onPostExecute(Configuration result) { - if ((config.error != null) && (config.error instanceof Exceptions.IntegrityException)) { - App.log.severe("Wrong encryption password."); - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.wrong_encryption_password) - .setIcon(R.drawable.ic_error_dark) - .setMessage(getString(R.string.wrong_encryption_password_content, config.error.getLocalizedMessage())) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // dismiss - } - }).show(); - } else { - try { - if (createAccount(config.userName, config)) { - getActivity().setResult(Activity.RESULT_OK); - getActivity().finish(); - } - } catch (InvalidAccountException e) { - App.log.severe("Account creation failed!"); - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.account_creation_failed) - .setIcon(R.drawable.ic_error_dark) - .setMessage(e.getLocalizedMessage()) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // dismiss - } - }).show(); - } - } - - dismissAllowingStateLoss(); - } - - @Override - protected Configuration doInBackground(Void... aVoids) { - App.log.info("Started deriving key"); - config.password = Crypto.deriveKey(config.userName, config.rawPassword); - App.log.info("Finished deriving key"); - config.error = null; - - try { - Crypto.CryptoManager cryptoManager; - OkHttpClient httpClient = HttpClient.create(getContext(), config.url, config.authtoken); - - UserInfoManager userInfoManager = new UserInfoManager(httpClient, HttpUrl.get(config.url)); - UserInfoManager.UserInfo userInfo = userInfoManager.get(config.userName); - if (userInfo != null) { - App.log.info("Fetched userInfo for " + config.userName); - cryptoManager = new Crypto.CryptoManager(userInfo.getVersion(), config.password, "userInfo"); - userInfo.verify(cryptoManager); - config.keyPair = new Crypto.AsymmetricKeyPair(userInfo.getContent(cryptoManager), userInfo.getPubkey()); - } - } catch (Exception e) { - e.printStackTrace(); - config.error = e; - } - - return config; - } - } - - - protected boolean createAccount(String accountName, BaseConfigurationFinder.Configuration config) throws InvalidAccountException { - Account account = new Account(accountName, App.getAccountType()); - - // create Android account - App.log.log(Level.INFO, "Creating Android account with initial config", new Object[] { account, config.userName, config.url }); - - AccountManager accountManager = AccountManager.get(getContext()); - if (!accountManager.addAccountExplicitly(account, config.password, null)) - return false; - - AccountSettings.setUserData(accountManager, account, config.url, config.userName); - - // add entries for account to service DB - App.log.log(Level.INFO, "Writing account configuration to database", config); - try { - AccountSettings settings = new AccountSettings(getContext(), account); - - settings.setAuthToken(config.authtoken); - if (config.keyPair != null) { - settings.setKeyPair(config.keyPair); - } - - if (config.cardDAV != null) { - // insert CardDAV service - insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV); - - // contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml - settings.setSyncInterval(App.getAddressBooksAuthority(), Constants.DEFAULT_SYNC_INTERVAL); - } else { - ContentResolver.setIsSyncable(account, App.getAddressBooksAuthority(), 0); - } - - if (config.calDAV != null) { - // insert CalDAV service - insertService(accountName, CollectionInfo.Type.CALENDAR, config.calDAV); - - // calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml - settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL); - - // enable task sync if OpenTasks is installed - // further changes will be handled by PackageChangedReceiver - if (LocalTaskList.tasksProviderAvailable(getContext())) { - ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1); - settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, Constants.DEFAULT_SYNC_INTERVAL); - } - } else { - ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0); - } - - } catch(InvalidAccountException e) { - App.log.log(Level.SEVERE, "Couldn't access account settings", e); - AndroidCompat.removeAccount(accountManager, account); - throw e; - } - - return true; - } - - protected void insertService(String accountName, CollectionInfo.Type serviceType, BaseConfigurationFinder.Configuration.ServiceInfo info) { - EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); - - // insert service - ServiceEntity serviceEntity = new ServiceEntity(); - serviceEntity.setAccount(accountName); - serviceEntity.setType(serviceType); - data.upsert(serviceEntity); - - // insert collections - for (CollectionInfo collection : info.collections.values()) { - collection.serviceID = serviceEntity.getId(); - JournalEntity journalEntity = new JournalEntity(data, collection); - data.insert(journalEntity); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.kt new file mode 100644 index 00000000..2c23b788 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.kt @@ -0,0 +1,204 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.setup + +import android.accounts.Account +import android.accounts.AccountManager +import android.app.Activity +import android.app.Dialog +import android.app.ProgressDialog +import android.content.ContentResolver +import android.content.Context +import android.os.AsyncTask +import android.os.Bundle +import android.provider.CalendarContract +import android.support.v4.app.DialogFragment +import android.support.v7.app.AlertDialog +import at.bitfire.ical4android.TaskProvider +import com.etesync.syncadapter.* +import com.etesync.syncadapter.journalmanager.Crypto +import com.etesync.syncadapter.journalmanager.Exceptions +import com.etesync.syncadapter.journalmanager.UserInfoManager +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.model.ServiceEntity +import com.etesync.syncadapter.resource.LocalTaskList +import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration +import com.etesync.syncadapter.utils.AndroidCompat +import okhttp3.HttpUrl +import java.util.logging.Level + +class SetupEncryptionFragment : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val progress = ProgressDialog(activity) + progress.setTitle(R.string.login_encryption_setup_title) + progress.setMessage(getString(R.string.login_encryption_setup)) + progress.isIndeterminate = true + progress.setCanceledOnTouchOutside(false) + isCancelable = false + return progress + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + SetupEncryptionLoader(context!!, arguments!!.getSerializable(KEY_CONFIG) as Configuration).execute() + } + + private inner class SetupEncryptionLoader(internal val context: Context, internal val config: Configuration) : AsyncTask() { + + override fun onPostExecute(result: Configuration) { + if (config.error != null && config.error is Exceptions.IntegrityException) { + App.log.severe("Wrong encryption password.") + AlertDialog.Builder(activity!!) + .setTitle(R.string.wrong_encryption_password) + .setIcon(R.drawable.ic_error_dark) + .setMessage(getString(R.string.wrong_encryption_password_content, config.error!!.localizedMessage)) + .setPositiveButton(android.R.string.ok) { dialog, which -> + // dismiss + }.show() + } else { + try { + if (createAccount(config.userName, config)) { + activity!!.setResult(Activity.RESULT_OK) + activity!!.finish() + } + } catch (e: InvalidAccountException) { + App.log.severe("Account creation failed!") + AlertDialog.Builder(activity!!) + .setTitle(R.string.account_creation_failed) + .setIcon(R.drawable.ic_error_dark) + .setMessage(e.localizedMessage) + .setPositiveButton(android.R.string.ok) { dialog, which -> + // dismiss + }.show() + } + + } + + dismissAllowingStateLoss() + } + + override fun doInBackground(vararg aVoids: Void): Configuration { + App.log.info("Started deriving key") + config.password = Crypto.deriveKey(config.userName, config.rawPassword!!) + App.log.info("Finished deriving key") + config.error = null + + try { + val cryptoManager: Crypto.CryptoManager + val httpClient = HttpClient.create(getContext(), config.url, config.authtoken) + + val userInfoManager = UserInfoManager(httpClient, HttpUrl.get(config.url)!!) + val userInfo = userInfoManager[config.userName] + if (userInfo != null) { + App.log.info("Fetched userInfo for " + config.userName) + cryptoManager = Crypto.CryptoManager(userInfo.version!!.toInt(), config.password!!, "userInfo") + userInfo.verify(cryptoManager) + config.keyPair = Crypto.AsymmetricKeyPair(userInfo.getContent(cryptoManager)!!, userInfo.pubkey!!) + } + } catch (e: Exception) { + e.printStackTrace() + config.error = e + } + + return config + } + } + + + @Throws(InvalidAccountException::class) + protected fun createAccount(accountName: String, config: BaseConfigurationFinder.Configuration): Boolean { + val account = Account(accountName, App.getAccountType()) + + // create Android account + App.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, config.userName, config.url)) + + val accountManager = AccountManager.get(context) + if (!accountManager.addAccountExplicitly(account, config.password, null)) + return false + + AccountSettings.setUserData(accountManager, account, config.url, config.userName) + + // add entries for account to service DB + App.log.log(Level.INFO, "Writing account configuration to database", config) + try { + val settings = AccountSettings(context!!, account) + + settings.authToken = config.authtoken + if (config.keyPair != null) { + settings.keyPair = config.keyPair!! + } + + if (config.cardDAV != null) { + // insert CardDAV service + insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV) + + // contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml + settings.setSyncInterval(App.getAddressBooksAuthority(), Constants.DEFAULT_SYNC_INTERVAL.toLong()) + } else { + ContentResolver.setIsSyncable(account, App.getAddressBooksAuthority(), 0) + } + + if (config.calDAV != null) { + // insert CalDAV service + insertService(accountName, CollectionInfo.Type.CALENDAR, config.calDAV) + + // calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml + settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL.toLong()) + + // enable task sync if OpenTasks is installed + // further changes will be handled by PackageChangedReceiver + if (LocalTaskList.tasksProviderAvailable(context!!)) { + ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1) + settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, Constants.DEFAULT_SYNC_INTERVAL.toLong()) + } + } else { + ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0) + } + + } catch (e: InvalidAccountException) { + App.log.log(Level.SEVERE, "Couldn't access account settings", e) + AndroidCompat.removeAccount(accountManager, account) + throw e + } + + return true + } + + protected fun insertService(accountName: String, serviceType: CollectionInfo.Type, info: BaseConfigurationFinder.Configuration.ServiceInfo) { + val data = (context!!.applicationContext as App).data + + // insert service + val serviceEntity = ServiceEntity() + serviceEntity.account = accountName + serviceEntity.type = serviceType + data.upsert(serviceEntity) + + // insert collections + for (collection in info.collections.values) { + collection.serviceID = serviceEntity.id + val journalEntity = JournalEntity(data, collection) + data.insert(journalEntity) + } + } + + companion object { + private val KEY_CONFIG = "config" + + fun newInstance(config: BaseConfigurationFinder.Configuration): SetupEncryptionFragment { + val frag = SetupEncryptionFragment() + val args = Bundle(1) + args.putSerializable(KEY_CONFIG, config) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java deleted file mode 100644 index 62a668b2..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.etesync.syncadapter.ui.setup; - -import android.accounts.Account; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.AsyncTask; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v7.app.AlertDialog; - -import com.etesync.syncadapter.AccountSettings; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.HttpClient; -import com.etesync.syncadapter.InvalidAccountException; -import com.etesync.syncadapter.R; -import com.etesync.syncadapter.journalmanager.Constants; -import com.etesync.syncadapter.journalmanager.Crypto; -import com.etesync.syncadapter.journalmanager.UserInfoManager; - -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; - -import static com.etesync.syncadapter.Constants.KEY_ACCOUNT; - -public class SetupUserInfoFragment extends DialogFragment { - private Account account; - private AccountSettings settings; - - public static SetupUserInfoFragment newInstance(Account account) { - SetupUserInfoFragment frag = new SetupUserInfoFragment(); - Bundle args = new Bundle(1); - args.putParcelable(KEY_ACCOUNT, account); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - ProgressDialog progress = new ProgressDialog(getActivity()); - progress.setTitle(R.string.login_encryption_setup_title); - progress.setMessage(getString(R.string.login_encryption_setup)); - progress.setIndeterminate(true); - progress.setCanceledOnTouchOutside(false); - setCancelable(false); - return progress; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - account = getArguments().getParcelable(KEY_ACCOUNT); - - try { - settings = new AccountSettings(getContext(), account); - } catch (Exception e) { - e.printStackTrace(); - } - - new SetupUserInfo().execute(account); - } - - public static boolean hasUserInfo(Context context, Account account) { - AccountSettings settings; - try { - settings = new AccountSettings(context, account); - } catch (InvalidAccountException e) { - e.printStackTrace(); - return false; - } - return settings.getKeyPair() != null; - } - - protected class SetupUserInfo extends AsyncTask { - ProgressDialog progressDialog; - - @Override - protected void onPreExecute() { - progressDialog = (ProgressDialog) getDialog(); - } - - @Override - protected SetupUserInfo.SetupUserInfoResult doInBackground(Account... accounts) { - try { - Crypto.CryptoManager cryptoManager; - OkHttpClient httpClient = HttpClient.create(getContext(), settings); - - UserInfoManager userInfoManager = new UserInfoManager(httpClient, HttpUrl.get(settings.getUri())); - UserInfoManager.UserInfo userInfo = userInfoManager.get(account.name); - - if (userInfo == null) { - App.log.info("Creating userInfo for " + account.name); - cryptoManager = new Crypto.CryptoManager(Constants.CURRENT_VERSION, settings.password(), "userInfo"); - userInfo = UserInfoManager.UserInfo.generate(cryptoManager, account.name); - userInfoManager.create(userInfo); - } else { - App.log.info("Fetched userInfo for " + account.name); - cryptoManager = new Crypto.CryptoManager(userInfo.getVersion(), settings.password(), "userInfo"); - userInfo.verify(cryptoManager); - } - - Crypto.AsymmetricKeyPair keyPair = new Crypto.AsymmetricKeyPair(userInfo.getContent(cryptoManager), userInfo.getPubkey()); - - return new SetupUserInfoResult(keyPair, null); - } catch (Exception e) { - e.printStackTrace(); - return new SetupUserInfoResult(null, e); - } - } - - @Override - protected void onPostExecute(SetupUserInfoResult result) { - if (result.exception == null) { - settings.setKeyPair(result.keyPair); - } else { - Dialog dialog = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.login_user_info_error_title) - .setIcon(R.drawable.ic_error_dark) - .setMessage(result.exception.getLocalizedMessage()) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // dismiss - } - }) - .create(); - dialog.show(); - } - - dismissAllowingStateLoss(); - } - - class SetupUserInfoResult { - final Crypto.AsymmetricKeyPair keyPair; - final Exception exception; - - SetupUserInfoResult(final Crypto.AsymmetricKeyPair keyPair, final Exception exception) { - this.keyPair = keyPair; - this.exception = exception; - } - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.kt new file mode 100644 index 00000000..2a72e49a --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.kt @@ -0,0 +1,125 @@ +package com.etesync.syncadapter.ui.setup + +import android.accounts.Account +import android.app.Dialog +import android.app.ProgressDialog +import android.content.Context +import android.os.AsyncTask +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.support.v7.app.AlertDialog +import com.etesync.syncadapter.* +import com.etesync.syncadapter.Constants.KEY_ACCOUNT +import com.etesync.syncadapter.journalmanager.Constants +import com.etesync.syncadapter.journalmanager.Crypto +import com.etesync.syncadapter.journalmanager.UserInfoManager +import okhttp3.HttpUrl + +class SetupUserInfoFragment : DialogFragment() { + private var account: Account? = null + private var settings: AccountSettings? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val progress = ProgressDialog(activity) + progress.setTitle(R.string.login_encryption_setup_title) + progress.setMessage(getString(R.string.login_encryption_setup)) + progress.isIndeterminate = true + progress.setCanceledOnTouchOutside(false) + isCancelable = false + return progress + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + account = arguments!!.getParcelable(KEY_ACCOUNT) + + try { + settings = AccountSettings(context!!, account!!) + } catch (e: Exception) { + e.printStackTrace() + } + + SetupUserInfo().execute(account) + } + + protected inner class SetupUserInfo : AsyncTask() { + internal var progressDialog = dialog as ProgressDialog + + override fun onPreExecute() { + progressDialog = dialog as ProgressDialog + } + + override fun doInBackground(vararg accounts: Account): SetupUserInfo.SetupUserInfoResult { + try { + val cryptoManager: Crypto.CryptoManager + val httpClient = HttpClient.create(context!!, settings!!) + + val userInfoManager = UserInfoManager(httpClient, HttpUrl.get(settings!!.uri!!)!!) + var userInfo: UserInfoManager.UserInfo? = userInfoManager[account!!.name] + + if (userInfo == null) { + App.log.info("Creating userInfo for " + account!!.name) + cryptoManager = Crypto.CryptoManager(Constants.CURRENT_VERSION, settings!!.password(), "userInfo") + userInfo = UserInfoManager.UserInfo.generate(cryptoManager, account!!.name) + userInfoManager.create(userInfo) + } else { + App.log.info("Fetched userInfo for " + account!!.name) + cryptoManager = Crypto.CryptoManager(userInfo.version!!.toInt(), settings!!.password(), "userInfo") + userInfo.verify(cryptoManager) + } + + val keyPair = Crypto.AsymmetricKeyPair(userInfo.getContent(cryptoManager)!!, userInfo.pubkey!!) + + return SetupUserInfoResult(keyPair, null) + } catch (e: Exception) { + e.printStackTrace() + return SetupUserInfoResult(null, e) + } + + } + + override fun onPostExecute(result: SetupUserInfoResult) { + if (result.exception == null) { + settings!!.keyPair = result.keyPair + } else { + val dialog = AlertDialog.Builder(activity!!) + .setTitle(R.string.login_user_info_error_title) + .setIcon(R.drawable.ic_error_dark) + .setMessage(result.exception.localizedMessage) + .setPositiveButton(android.R.string.ok) { dialog, which -> + // dismiss + } + .create() + dialog.show() + } + + dismissAllowingStateLoss() + } + + inner class SetupUserInfoResult(val keyPair: Crypto.AsymmetricKeyPair?, val exception: Exception?) + } + + companion object { + + fun newInstance(account: Account): SetupUserInfoFragment { + val frag = SetupUserInfoFragment() + val args = Bundle(1) + args.putParcelable(KEY_ACCOUNT, account) + frag.arguments = args + return frag + } + + fun hasUserInfo(context: Context, account: Account): Boolean { + val settings: AccountSettings + try { + settings = AccountSettings(context, account) + } catch (e: InvalidAccountException) { + e.printStackTrace() + return false + } + + return settings.keyPair != null + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/widget/EditPassword.java b/app/src/main/java/com/etesync/syncadapter/ui/widget/EditPassword.java deleted file mode 100644 index 44c32bf8..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/widget/EditPassword.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui.widget; - -import android.content.Context; -import android.text.Editable; -import android.util.AttributeSet; -import android.view.inputmethod.EditorInfo; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.EditText; -import android.widget.LinearLayout; - -import com.etesync.syncadapter.R; - -public class EditPassword extends LinearLayout { - private static final String NS_ANDROID = "http://schemas.android.com/apk/res/android"; - - EditText editPassword; - - public EditPassword(Context context) { - super(context, null); - } - - public EditPassword(Context context, AttributeSet attrs) { - super(context, attrs); - - inflate(context, R.layout.edit_password, this); - - editPassword = (EditText)findViewById(R.id.password); - editPassword.setHint(attrs.getAttributeResourceValue(NS_ANDROID, "hint", 0)); - editPassword.setText(attrs.getAttributeValue(NS_ANDROID, "text")); - - CheckBox checkShowPassword = (CheckBox)findViewById(R.id.show_password); - checkShowPassword.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - int inputType = editPassword.getInputType() & ~EditorInfo.TYPE_MASK_VARIATION; - inputType |= isChecked ? EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : EditorInfo.TYPE_TEXT_VARIATION_PASSWORD; - editPassword.setInputType(inputType); - } - }); - } - - public Editable getText() { - return editPassword.getText(); - } - - public void setError(CharSequence error) { - editPassword.setError(error); - } - - public void setText(CharSequence text) { - editPassword.setText(text); - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/widget/EditPassword.kt b/app/src/main/java/com/etesync/syncadapter/ui/widget/EditPassword.kt new file mode 100644 index 00000000..d3a79289 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/widget/EditPassword.kt @@ -0,0 +1,60 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.widget + +import android.content.Context +import android.text.Editable +import android.util.AttributeSet +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.CheckBox +import android.widget.EditText +import android.widget.LinearLayout +import com.etesync.syncadapter.R + +class EditPassword : LinearLayout { + + internal var editPassword: EditText + + val text: Editable + get() = editPassword.text + + init { + View.inflate(context, R.layout.edit_password, this) + + editPassword = findViewById(R.id.password) as EditText + } + + constructor(context: Context) : super(context) {} + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + editPassword.setHint(attrs.getAttributeResourceValue(NS_ANDROID, "hint", 0)) + editPassword.setText(attrs.getAttributeValue(NS_ANDROID, "text")) + + val checkShowPassword = findViewById(R.id.show_password) as CheckBox + checkShowPassword.setOnCheckedChangeListener { buttonView, isChecked -> + var inputType = editPassword.inputType and EditorInfo.TYPE_MASK_VARIATION.inv() + inputType = inputType or if (isChecked) EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD else EditorInfo.TYPE_TEXT_VARIATION_PASSWORD + editPassword.inputType = inputType + } + } + + fun setError(error: CharSequence) { + editPassword.error = error + } + + fun setText(text: CharSequence) { + editPassword.setText(text) + } + + companion object { + private val NS_ANDROID = "http://schemas.android.com/apk/res/android" + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/widget/MaximizedListView.java b/app/src/main/java/com/etesync/syncadapter/ui/widget/MaximizedListView.java deleted file mode 100644 index f73f07ff..00000000 --- a/app/src/main/java/com/etesync/syncadapter/ui/widget/MaximizedListView.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package com.etesync.syncadapter.ui.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ListAdapter; -import android.widget.ListView; - -public class MaximizedListView extends ListView { - - public MaximizedListView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - int widthMode = MeasureSpec.getMode(widthMeasureSpec), - widthSize = MeasureSpec.getSize(widthMeasureSpec), - heightMode = MeasureSpec.getMode(heightMeasureSpec), - heightSize = MeasureSpec.getSize(heightMeasureSpec); - - int width = 0, height = 0; - - if (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST) - width = widthSize; - - if (heightMode == MeasureSpec.EXACTLY) - height = heightSize; - else { - ListAdapter listAdapter = getAdapter(); - if (listAdapter != null) { - int widthSpec = View.MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); - for (int i = 0; i < listAdapter.getCount(); i++) { - View listItem = listAdapter.getView(i, null, this); - listItem.measure(widthSpec, View.MeasureSpec.UNSPECIFIED); - height += listItem.getMeasuredHeight(); - } - height += getDividerHeight() * (listAdapter.getCount() - 1); - } - } - - setMeasuredDimension(width, height); - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/widget/MaximizedListView.kt b/app/src/main/java/com/etesync/syncadapter/ui/widget/MaximizedListView.kt new file mode 100644 index 00000000..aba91dfe --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/widget/MaximizedListView.kt @@ -0,0 +1,50 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.ListView + +class MaximizedListView(context: Context, attrs: AttributeSet) : ListView(context, attrs) { + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val widthMode = View.MeasureSpec.getMode(widthMeasureSpec) + val widthSize = View.MeasureSpec.getSize(widthMeasureSpec) + val heightMode = View.MeasureSpec.getMode(heightMeasureSpec) + val heightSize = View.MeasureSpec.getSize(heightMeasureSpec) + + var width = 0 + var height = 0 + + if (widthMode == View.MeasureSpec.EXACTLY || widthMode == View.MeasureSpec.AT_MOST) + width = widthSize + + if (heightMode == View.MeasureSpec.EXACTLY) + height = heightSize + else { + val listAdapter = adapter + if (listAdapter != null) { + val widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY) + for (i in 0 until listAdapter.count) { + val listItem = listAdapter.getView(i, null, this) + listItem.measure(widthSpec, View.MeasureSpec.UNSPECIFIED) + height += listItem.measuredHeight + } + height += dividerHeight * (listAdapter.count - 1) + } + } + + setMeasuredDimension(width, height) + } + +}