Kotlin: more kotlin migration.

tmp
Tom Hacohen 5 years ago
parent c26ae4fba6
commit f77063ff1a

@ -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) {

@ -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);

@ -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<Spanned> {
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<Spanned> onCreateLoader(int id, Bundle args) {
return new LicenseLoader(getContext(), args.getString(KEY_FILE_NAME));
}
@Override
public void onLoadFinished(Loader<Spanned> 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<Spanned> loader) {
}
}
private static class LicenseLoader extends AsyncTaskLoader<Spanned> {
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;
}
}
}
}

@ -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<View>(R.id.toolbar) as Toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
val viewPager = findViewById<View>(R.id.viewpager) as ViewPager
viewPager.adapter = TabsAdapter(supportFragmentManager)
val tabLayout = findViewById<View>(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<Spanned> {
@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<View>(R.id.title) as TextView
tv.text = info.title + if (info.version != null) " " + info.version else ""
tv = v.findViewById<View>(R.id.website) as TextView
tv.autoLinkMask = Linkify.WEB_URLS
tv.text = info.website
tv = v.findViewById<View>(R.id.copyright) as TextView
tv.text = "© " + info.copyright
tv = v.findViewById<View>(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<Spanned> {
return LicenseLoader(context!!, args!!.getString(KEY_FILE_NAME))
}
override fun onLoadFinished(loader: Loader<Spanned>, license: Spanned) {
if (view != null) {
val tv = view!!.findViewById<View>(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<Spanned>) {}
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<Spanned>(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"
))
}
}

@ -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<AccountActivity.AccountInfo>, 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<JournalEntity> 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<JournalEntity> journals;
}
}
@Override
public Loader<AccountInfo> 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<AccountInfo> 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<AccountInfo> loader) {
if (listCardDAV != null)
listCardDAV.setAdapter(null);
if (listCalDAV != null)
listCalDAV.setAdapter(null);
}
private static class AccountLoader extends AsyncTaskLoader<AccountInfo> 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<Persistable> 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<JournalEntity> {
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<Bundle>() {
@Override
public void run(AccountManagerFuture<Bundle> 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<Boolean>() {
@Override
public void run(AccountManagerFuture<Boolean> 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();
}
}

@ -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<AccountActivity.AccountInfo>, 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<View>(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<View>(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<View>(R.id.body).visibility = View.GONE
(view.findViewById<View>(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<JournalEntity>? = null
}
}
override fun onCreateLoader(id: Int, args: Bundle): Loader<AccountInfo> {
return AccountLoader(this, account!!)
}
override fun refresh() {
loaderManager.restartLoader(0, intent.extras, this)
}
override fun onLoadFinished(loader: Loader<AccountInfo>, info: AccountInfo) {
accountInfo = info
var card = findViewById<View>(R.id.carddav) as CardView
if (info.carddav != null) {
val progress = findViewById<View>(R.id.carddav_refreshing) as ProgressBar
progress.visibility = if (info.carddav!!.refreshing) View.VISIBLE else View.GONE
listCardDAV = findViewById<View>(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<View>(R.id.caldav) as CardView
if (info.caldav != null) {
val progress = findViewById<View>(R.id.caldav_refreshing) as ProgressBar
progress.visibility = if (info.caldav!!.refreshing) View.VISIBLE else View.GONE
listCalDAV = findViewById<View>(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<AccountInfo>) {
if (listCardDAV != null)
listCardDAV!!.adapter = null
if (listCalDAV != null)
listCalDAV!!.adapter = null
}
private class AccountLoader(context: Context, private val account: Account) : AsyncTaskLoader<AccountInfo>(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<JournalEntity>(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<View>(R.id.title) as TextView
tv.text = if (TextUtils.isEmpty(info.displayName)) info.uid else info.displayName
tv = v.findViewById<View>(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<View>(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<View>(R.id.read_only)
readOnly.visibility = if (journalEntity.isReadOnly) View.VISIBLE else View.GONE
val shared = v.findViewById<View>(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)
}
}
}
}

@ -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<Account[]>, 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<Account[]> onCreateLoader(int id, Bundle args) {
return new AccountLoader(getContext());
}
@Override
public void onLoadFinished(Loader<Account[]> loader, Account[] accounts) {
AccountListAdapter adapter = (AccountListAdapter)getListAdapter();
adapter.clear();
adapter.addAll(accounts);
}
@Override
public void onLoaderReset(Loader<Account[]> loader) {
((AccountListAdapter)getListAdapter()).clear();
}
private static class AccountLoader extends AsyncTaskLoader<Account[]> 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<Account> {
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;
}
}
}

@ -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<Array<Account>>, 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<Array<Account>> {
return AccountLoader(context!!)
}
override fun onLoadFinished(loader: Loader<Array<Account>>, accounts: Array<Account>) {
val adapter = listAdapter as AccountListAdapter
adapter.clear()
adapter.addAll(*accounts)
}
override fun onLoaderReset(loader: Loader<Array<Account>>) {
(listAdapter as AccountListAdapter).clear()
}
private class AccountLoader(context: Context) : AsyncTaskLoader<Array<Account>>(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<Account>) {
forceLoad()
}
@SuppressLint("MissingPermission")
override fun loadInBackground(): Array<Account>? {
return accountManager.getAccountsByType(App.getAccountType())
}
}
// list adapter
internal class AccountListAdapter(context: Context) : ArrayAdapter<Account>(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<View>(R.id.account_name) as TextView
tv.text = account!!.name
return v
}
}
}

@ -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<AccountSettings> {
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<AccountSettings> onCreateLoader(int id, Bundle args) {
return new AccountSettingsLoader(getContext(), (Account)args.getParcelable(KEY_ACCOUNT));
}
@Override
public void onLoadFinished(Loader<AccountSettings> 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<AccountSettings> loader) {
}
}
private static class AccountSettingsLoader extends AsyncTaskLoader<AccountSettings> 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();
}
}
}

@ -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<AccountSettings> {
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<AccountSettings> {
return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account)
}
override fun onLoadFinished(loader: Loader<AccountSettings>, 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<AccountSettings>) {}
}
private class AccountSettingsLoader(context: Context, internal val account: Account) : AsyncTaskLoader<AccountSettings>(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()
}
}
}

@ -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;
}
}

@ -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<View>(R.id.toolbar) as Toolbar
setSupportActionBar(toolbar)
val fab = findViewById<View>(R.id.fab) as FloatingActionButton
fab.setOnClickListener { startActivity(Intent(this@AccountsActivity, LoginActivity::class.java)) }
val drawer = findViewById<View>(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<View>(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<View>(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<View>(R.id.drawer_layout) as DrawerLayout
drawer.closeDrawer(GravityCompat.START)
return true
}
companion object {
val HINT_ACCOUNT_ADD = "AddAccount"
}
}

@ -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<Void, Void, MemberAdd.AddResult> {
@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<Void, Void, MemberAddSecond.AddResultSecond> {
@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;
}
}
}
}

@ -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<Void, Void, MemberAdd.AddResult>() {
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<View>(R.id.body) as TextView).text = getString(R.string.trust_fingerprint_body, memberEmail)
(view.findViewById<View>(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<Void, Void, MemberAddSecond.AddResultSecond>() {
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
}
}
}

@ -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<Void, Void, LanguageUtils.LocaleList> {
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;
}
});
}
}
}
}

@ -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<Void, Void, LanguageUtils.LocaleList>() {
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
}
}
}
}
}

@ -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;
}
}

@ -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
}
}

@ -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<Persistable> 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();
}
}

@ -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<View>(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<View>(R.id.progressBar).visibility = View.GONE
val title = findViewById<View>(R.id.display_name) as TextView
title.text = info.displayName
val desc = findViewById<View>(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
}
}
}

@ -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<Persistable> 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<JournalManager.Member> {
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<Void, Void, JournalMembersFetch.MembersResult> {
@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<JournalManager.Member> members;
final Throwable throwable;
MembersResult(final List<JournalManager.Member> members, final Throwable throwable) {
this.members = members;
this.throwable = throwable;
}
}
}
}

@ -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<Persistable>
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<View>(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<JournalManager.Member>(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<View>(R.id.title) as TextView
tv.text = member!!.user
return v
}
}
private inner class JournalMembersFetch : AsyncTask<Void, Void, JournalMembersFetch.MembersResult>() {
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<JournalManager.Member>?, 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
}
}
}

@ -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);
}
}
}

@ -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<View>(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<View>(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<View>(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<View>(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<View>(R.id.description) as EditText
info!!.description = StringUtils.trimToNull(edit.text.toString())
if (ok) {
if (info!!.type == CollectionInfo.Type.CALENDAR) {
val view = findViewById<View>(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
}
}
}

@ -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<Exception> {
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<Exception> onCreateLoader(int id, Bundle args) {
return new CreateCollectionLoader(getContext(), account, info);
}
@Override
public void onLoadFinished(Loader<Exception> 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<Exception> loader) {
}
protected static class CreateCollectionLoader extends AsyncTaskLoader<Exception> {
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<Persistable> 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);
}
}
}

@ -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<Exception> {
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<Exception> {
return CreateCollectionLoader(context!!, account, info)
}
override fun onLoadFinished(loader: Loader<Exception>, 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<Exception>) {}
protected class CreateCollectionLoader(context: Context, internal val account: Account, internal val info: CollectionInfo) : AsyncTaskLoader<Exception>(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
}
}
}

@ -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<String> {
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<String> onCreateLoader(int id, Bundle args) {
return new ReportLoader(this, args);
}
@Override
public void onLoadFinished(Loader<String> loader, String data) {
if (data != null)
tvReport.setText(report = data);
}
@Override
public void onLoaderReset(Loader<String> loader) {
}
static class ReportLoader extends AsyncTaskLoader<String> {
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<Persistable> 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<JournalEntity> 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") :
"—";
}
}
}

@ -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<String> {
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<String> {
return ReportLoader(this, args)
}
override fun onLoadFinished(loader: Loader<String>, data: String?) {
if (data != null) {
report = data
tvReport.setText(report)
}
}
override fun onLoaderReset(loader: Loader<String>) {}
internal class ReportLoader(context: Context, val extras: Bundle?) : AsyncTaskLoader<String>(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"
}
}

@ -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<Exception> {
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<Exception> 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<Exception> {
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<Persistable> 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();
}
}
}

@ -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<Exception> {
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<Exception> {
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: 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<Exception>) {}
protected class DeleteCollectionLoader(context: Context, internal val account: Account, internal val collectionInfo: CollectionInfo) : AsyncTaskLoader<Exception>(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"
}
}

@ -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<Persistable> 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);
}
}
}

@ -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<View>(R.id.color)
if (info!!.color != null) {
colorSquare.setBackgroundColor(info!!.color)
} else {
colorSquare.setBackgroundColor(LocalCalendar.defaultColor)
}
}
val edit = findViewById<View>(R.id.display_name) as EditText
edit.setText(info!!.displayName)
val desc = findViewById<View>(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
}
}
}

@ -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;
}
}

@ -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<Account>(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
}
}
}

@ -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<Persistable> 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<Void, Void, Event> {
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<Void, Void, Contact> {
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<Telephone> labeledPhone : contact.phoneNumbers) {
List<TelephoneType> 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<Email> labeledEmail : contact.emails) {
List<EmailType> 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<Impp> 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<Address> labeledAddress : contact.addresses) {
List<AddressType> 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<Url> 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<RelatedType> 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();
}
}

@ -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<View>(R.id.viewpager) as ViewPager
viewPager.adapter = TabsAdapter(supportFragmentManager, this, info, syncEntry)
val tabLayout = findViewById<View>(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<View>(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<Void, Void, Event>() {
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<View>(R.id.event_info_loading_msg)
loader.visibility = View.GONE
val contentContainer = view.findViewById<View>(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<View>(R.id.organizer) as TextView
tv.text = event.organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
} else {
val organizer = view.findViewById<View>(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<Void, Void, Contact>() {
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<View>(R.id.loading_msg)
loader.visibility = View.GONE
val contentContainer = view.findViewById<View>(R.id.content_container)
contentContainer.visibility = View.VISIBLE
val tv = view.findViewById<View>(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<View>(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<View>(R.id.main_card) as ViewGroup
val aboutCard = view.findViewById<View>(R.id.about_card) as ViewGroup
aboutCard.findViewById<View>(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<View>(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<View>(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
}
}
}

@ -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);
}
}

@ -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<View>(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<View>(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<View>(R.id.opentasks_permissions).visibility = if (noTaskPermissions) View.VISIBLE else View.GONE
} else {
findViewById<View>(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<String>, 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)
}
}
}

@ -1,5 +0,0 @@
package com.etesync.syncadapter.ui;
public interface Refreshable {
public void refresh();
}

@ -0,0 +1,5 @@
package com.etesync.syncadapter.ui
interface Refreshable {
fun refresh()
}

@ -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<Void, Void, MemberRemove.RemoveResult> {
@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;
}
}
}
}

@ -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<Account>(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<Void, Void, MemberRemove.RemoveResult>() {
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
}
}
}

@ -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<StartupDialogFragment> 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;
}
}
}

@ -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<StartupDialogFragment> {
val dialogs = LinkedList<StartupDialogFragment>()
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
}
}
}
}

@ -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<Persistable> 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<Void, Void, Long> {
private int entryCount;
@Override
protected Long doInBackground(Void... aVoids) {
EntityDataStore<Persistable> 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));
}
}
}
}
}

@ -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<View>(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<View>(R.id.display_name) as TextView
title.text = info.displayName
val desc = findViewById<View>(R.id.description) as TextView
desc.text = info.description
val owner = findViewById<View>(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<View>(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<Void, Void, Long>() {
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<View>(R.id.stats) as TextView
findViewById<View>(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
}
}
}

@ -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 = "<html><title>" +
getString(R.string.loading_error_title) +
"</title>" +
"<style>" +
".btn {" +
" display: inline-block;" +
" padding: 6px 12px;" +
" font-size: 20px;" +
" font-weight: 400;" +
" line-height: 1.42857143;" +
" text-align: center;" +
" white-space: nowrap;" +
" vertical-align: middle;" +
" touch-action: manipulation;" +
" cursor: pointer;" +
" user-select: none;" +
" border: 1px solid #ccc;" +
" border-radius: 4px;" +
" color: #333;" +
" text-decoration: none;" +
" margin-top: 50px;" +
"}" +
"</style>" +
"<body>" +
"<div align=\"center\">" +
"<a class=\"btn\" href=\"" + failingUrl + "\">" + getString(R.string.loading_error_content) +
"</a>" +
"</form></body></html>";
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);
}
}

@ -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<Uri>(KEY_URL)
uri = addQueryParams(uri)
mWebView = findViewById<View>(R.id.webView) as WebView
mProgressBar = findViewById<View>(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 = "<html><title>" +
getString(R.string.loading_error_title) +
"</title>" +
"<style>" +
".btn {" +
" display: inline-block;" +
" padding: 6px 12px;" +
" font-size: 20px;" +
" font-weight: 400;" +
" line-height: 1.42857143;" +
" text-align: center;" +
" white-space: nowrap;" +
" vertical-align: middle;" +
" touch-action: manipulation;" +
" cursor: pointer;" +
" user-select: none;" +
" border: 1px solid #ccc;" +
" border-radius: 4px;" +
" color: #333;" +
" text-decoration: none;" +
" margin-top: 50px;" +
"}" +
"</style>" +
"<body>" +
"<div align=\"center\">" +
"<a class=\"btn\" href=\"" + failingUrl + "\">" + getString(R.string.loading_error_content) +
"</a>" +
"</form></body></html>"
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<Uri>, 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!!)
}
}
}

@ -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<String, AccountInfo> 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;
}
}
}

@ -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<String, AccountInfo>
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)
}

@ -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<LocalCalendar> 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<CalendarAccount> 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<CalendarAccount> 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<LocalCalendar> getCalendars() {
return calendars;
}
public Account getAccount() {
return account;
}
@Override
public String toString() {
return account.toString();
}
}

@ -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<LocalCalendar>()
val accountName: String
get() = account.name
val accountType: String
get() = account.type
fun getCalendars(): List<LocalCalendar> {
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<CalendarAccount> {
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<CalendarAccount>(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
}
}
}

@ -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;
}
}
}

@ -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<View>(R.id.import_file)
var img = card.findViewById<View>(R.id.action_icon) as ImageView
var text = card.findViewById<View>(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<View>(R.id.action_icon) as ImageView
text = card.findViewById<View>(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
}
}
}

@ -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;
}
}
}
}

@ -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<String>, 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
}
}
}

@ -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<CalendarAccount> 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<CalendarAccount> calendarAccounts;
private AccountResolver accountResolver;
public ExpandableListAdapter(Context context, List<CalendarAccount> 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<LocalCalendar, Integer, ResultFragment.ImportResult> {
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;
}
}
}

@ -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<CalendarAccount>) : 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<View>(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<View>(R.id.title) as TextView
viewHolder.descriptionTextView = convertView
.findViewById<View>(R.id.description) as TextView
viewHolder.iconImageView = convertView.findViewById<View>(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<LocalCalendar, Int, ResultFragment.ImportResult>() {
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
}
}
}

@ -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<LocalAddressBook> 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<LocalAddressBook, Integer, ResultFragment.ImportResult> {
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<ImportContactAdapter.ViewHolder> {
private static final String TAG = "ImportContactAdapter";
private List<LocalAddressBook> 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<LocalAddressBook> 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());
}
}
}

@ -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<View>(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<LocalAddressBook>()
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<LocalAddressBook, Int, ResultFragment.ImportResult>() {
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<LocalAddressBook>, private val mOnAccountSelected: OnAccountSelected) : RecyclerView.Adapter<ImportContactAdapter.ViewHolder>() {
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<View>(R.id.title) as TextView
descTextView = v.findViewById<View>(R.id.description) as TextView
iconImageView = v.findViewById<View>(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
}
}
}

@ -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);
}
}

@ -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
}
}
}

@ -1,11 +0,0 @@
package com.etesync.syncadapter.ui.importlocal;
/**
* Created by tal on 30/03/17.
*/
public interface SelectImportMethod {
void importFile();
void importAccount();
}

@ -0,0 +1,11 @@
package com.etesync.syncadapter.ui.importlocal
/**
* Created by tal on 30/03/17.
*/
interface SelectImportMethod {
fun importFile()
fun importAccount()
}

@ -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<Persistable> 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<EntryEntity> {
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<Void, Void, List<EntryEntity>> {
@Override
protected List<EntryEntity> 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<EntryEntity> result) {
EntriesListAdapter listAdapter = new EntriesListAdapter(getContext());
setListAdapter(listAdapter);
listAdapter.addAll(result);
emptyTextView.setText(getString(R.string.journal_entries_list_empty));
}
}
}

@ -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<Persistable>? = 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<View>(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<EntryEntity>(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<View>(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<Void, Void, List<EntryEntity>>() {
override fun doInBackground(vararg voids: Void): List<EntryEntity> {
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<EntryEntity>) {
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<View>(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<View>(R.id.description) as TextView
content = getLine(fullContent, "UID:")
content = "UID: " + (content ?: "Not found")
tv.text = content
val action = v.findViewById<View>(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)
}
}
}
}

@ -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<String, CollectionInfo> 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() + ")";
}
}
}

@ -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<String, CollectionInfo> = 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 + ")"
}
}
}

@ -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<Configuration> {
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<Configuration> onCreateLoader(int id, Bundle args) {
return new ServerConfigurationLoader(getContext(), (LoginCredentials)args.getParcelable(ARG_LOGIN_CREDENTIALS));
}
@Override
public void onLoadFinished(Loader<Configuration> 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<Configuration> 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<Configuration> {
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();
}
}
}

@ -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<Configuration> {
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<Configuration> {
return ServerConfigurationLoader(context!!, args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials)
}
override fun onLoadFinished(loader: Loader<Configuration>, 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<Configuration>) {}
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<Configuration>(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
}
}
}

@ -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;
}
}

@ -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<View>(R.id.back) as Button
btnBack.setOnClickListener { fragmentManager!!.popBackStack() }
val config = arguments!!.getSerializable(KEY_CONFIG) as BaseConfigurationFinder.Configuration
val accountName = v.findViewById<View>(R.id.account_name) as TextView
accountName.text = getString(R.string.login_encryption_account_label) + " " + config.userName
editPassword = v.findViewById<View>(R.id.encryption_password) as EditPassword
val btnCreate = v.findViewById<View>(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<View>(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
}
}
}

@ -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);
}
}

@ -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"
}
}

@ -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<LoginCredentials>() {
@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];
}
};
}

@ -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<LoginCredentials> {
override fun createFromParcel(source: Parcel): LoginCredentials {
return LoginCredentials(
source.readSerializable() as URI,
source.readString(), source.readString()
)
}
override fun newArray(size: Int): Array<LoginCredentials> {
return arrayOfNulls<LoginCredentials>(size) as Array<LoginCredentials>
}
}
}
}

@ -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<Configuration> {
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<Configuration> onCreateLoader(int id, Bundle args) {
return new ServerConfigurationLoader(getContext(), (LoginCredentials) args.getParcelable(ARG_LOGIN_CREDENTIALS));
}
@Override
public void onLoadFinished(Loader<Configuration> 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<Configuration> 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<Configuration> {
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();
}
}
}

@ -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<Configuration> {
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<Configuration> {
return ServerConfigurationLoader(context!!, args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials)
}
override fun onLoadFinished(loader: Loader<Configuration>, 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<Configuration>) {}
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<Configuration>(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
}
}
}

@ -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;
}
}

@ -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<View>(R.id.user_name) as EditText
editUrlPassword = v.findViewById<View>(R.id.url_password) as EditPassword
showAdvanced = v.findViewById<View>(R.id.show_advanced) as CheckedTextView
customServer = v.findViewById<View>(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<View>(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<View>(R.id.login) as Button
login.setOnClickListener {
val credentials = validateLoginData()
if (credentials != null)
DetectConfigurationFragment.newInstance(credentials).show(fragmentManager!!, null)
}
val forgotPassword = v.findViewById<View>(R.id.forgot_password) as TextView
forgotPassword.setOnClickListener { WebViewActivity.openUrl(context!!, Constants.forgotPassword) }
val advancedLayout = v.findViewById<View>(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
}
}

@ -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<Void, Void, Configuration> {
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<Persistable> 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);
}
}
}

@ -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<Void, Void, Configuration>() {
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
}
}
}

@ -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<Account, Integer, SetupUserInfo.SetupUserInfoResult> {
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;
}
}
}
}

@ -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<Account, Int, SetupUserInfo.SetupUserInfoResult>() {
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
}
}
}

@ -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);
}
}

@ -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<View>(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<View>(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"
}
}

@ -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);
}
}

@ -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)
}
}
Loading…
Cancel
Save