Create/delete calendars

pull/2/head
Ricki Hirner 8 years ago
parent 212cd8ddb0
commit 19bfe5c5f2

@ -68,6 +68,7 @@ dependencies {
compile 'com.android.support:design:23.+'
compile 'com.android.support:preference-v7:23.+'
compile 'com.github.yukuku:ambilwarna:2.0.1'
compile project(':MemorizingTrustManager')
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.1.2'

@ -131,6 +131,8 @@
<activity android:name=".ui.AccountSettingsActivity"/>
<activity android:name=".ui.CreateAddressBookActivity"
android:label="@string/create_address_book_title"/>
<activity android:name=".ui.CreateCalendarActivity"
android:label="@string/create_calendar_title"/>
<activity
android:name=".ui.DebugInfoActivity"
android:exported="true"

@ -28,4 +28,11 @@ public class DavUtils {
}
return color;
}
public static String ARGBtoCalDAVColor(int colorWithAlpha) {
byte alpha = (byte)(colorWithAlpha >> 24);
int color = colorWithAlpha & 0xFFFFFF;
return String.format("#%06X%02X", color, alpha);
}
}

@ -23,7 +23,7 @@ public class DavService {
service.accountName = values.getAsString(ServiceDB.Services.ACCOUNT_NAME);
service.service = values.getAsString(ServiceDB.Services.SERVICE);
service.principal = values.getAsString(ServiceDB.Services.PRINCIPAL);
//service.lastRefresh = values.getAsLong(ServiceDB.Services.LAST_REFRESH);
//FIXME service.lastRefresh = values.getAsLong(ServiceDB.Services.LAST_REFRESH);
return service;
}

@ -66,6 +66,9 @@ public class CalendarsSyncAdapterService extends Service {
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Constants.log.info("Starting calendar sync (" + authority + ")");
// required for ical4j and dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
try {
updateLocalCalendars(provider, account);

@ -62,6 +62,9 @@ public class ContactsSyncAdapterService extends Service {
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Constants.log.info("Starting address book sync (" + authority + ")");
// required for dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
long service = getService(account);
CollectionInfo remote = remoteAddressBook(service);

@ -107,9 +107,6 @@ abstract public class SyncManager {
this.authority = authority;
this.syncResult = syncResult;
// required for ical4j and dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(context.getClassLoader());
// get account settings and log to file (if requested)
settings = new AccountSettings(context, account);
try {
@ -423,7 +420,7 @@ abstract public class SyncManager {
syncResult.stats.numDeletes++;
} else {
// contact is still on server, check whether it has been updated remotely
GetETag getETag = (GetETag) remote.properties.get(GetETag.NAME);
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
if (getETag == null || getETag.eTag == null)
throw new DavException("Server didn't provide ETag");
String localETag = localResources.get(localName).getETag(),

@ -65,6 +65,9 @@ public class TasksSyncAdapterService extends Service {
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient providerClient, SyncResult syncResult) {
Constants.log.info("Starting task sync (" + authority + ")");
// required for ical4j and dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
try {
@Cleanup TaskProvider provider = TaskProvider.acquire(getContext().getContentResolver(), TaskProvider.ProviderName.OpenTasks);
if (provider == null)

@ -28,6 +28,7 @@ import android.content.ServiceConnection;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
@ -56,6 +57,8 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.apache.commons.lang3.BooleanUtils;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
@ -93,13 +96,18 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
setContentView(R.layout.activity_account);
Drawable icMenu = Build.VERSION.SDK_INT >= 21 ? getDrawable(R.drawable.ic_menu_light) :
getResources().getDrawable(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);
// CalDAV toolbar
tbCalDAV = (Toolbar)findViewById(R.id.caldav_menu);
tbCalDAV.setOverflowIcon(icMenu);
tbCalDAV.inflateMenu(R.menu.caldav_actions);
tbCalDAV.setOnMenuItemClickListener(this);
@ -165,6 +173,11 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
intent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, accountInfo.caldav.id);
startService(intent);
break;
case R.id.create_calendar:
intent = new Intent(this, CreateCalendarActivity.class);
intent.putExtra(CreateCalendarActivity.EXTRA_ACCOUNT, account);
startActivity(intent);
break;
}
return false;
}
@ -231,14 +244,14 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
final ArrayAdapter<CollectionInfo> adapter = (ArrayAdapter)list.getAdapter();
final CollectionInfo info = adapter.getItem(position);
PopupMenu popup = new PopupMenu(AccountActivity.this, view, Gravity.CENTER);
PopupMenu popup = new PopupMenu(AccountActivity.this, view);
popup.inflate(R.menu.account_collection_operations);
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.delete_collection:
DeleteCollectionFragment.newInstance(account, info).show(getSupportFragmentManager(), null);
DeleteCollectionFragment.ConfirmDeleteCollectionFragment.newInstance(account, info).show(getSupportFragmentManager(), null);
break;
}
return true;
@ -331,7 +344,6 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
davService = (DavService.InfoBinder)service;
davService.addRefreshingStatusListener(this, false);
SQLiteDatabase db;
forceLoad();
}
@ -442,10 +454,12 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
CheckBox checked = (CheckBox)v.findViewById(R.id.checked);
checked.setChecked(info.selected);
View vColor = v.findViewById(R.id.color);
if (info.color != null) {
View vColor = v.findViewById(R.id.color);
vColor.setVisibility(View.VISIBLE);
vColor.setBackgroundColor(info.color);
}
} else
vColor.setVisibility(View.GONE);
TextView tv = (TextView)v.findViewById(R.id.title);
tv.setText(TextUtils.isEmpty(info.displayName) ? info.url : info.displayName);
@ -462,10 +476,10 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
tv.setVisibility(info.readOnly ? View.VISIBLE : View.GONE);
tv = (TextView)v.findViewById(R.id.events);
tv.setVisibility(info.supportsVEVENT ? View.VISIBLE : View.GONE);
tv.setVisibility(BooleanUtils.isTrue(info.supportsVEVENT) ? View.VISIBLE : View.GONE);
tv = (TextView)v.findViewById(R.id.tasks);
tv.setVisibility(info.supportsVTODO ? View.VISIBLE : View.GONE);
tv.setVisibility(BooleanUtils.isTrue(info.supportsVTODO) ? View.VISIBLE : View.GONE);
return v;
}

@ -9,20 +9,25 @@
package at.bitfire.davdroid.ui;
import android.accounts.Account;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
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.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.SimpleAdapter;
import android.widget.Spinner;
import android.widget.Toast;
@ -78,7 +83,7 @@ public class CreateAddressBookActivity extends AppCompatActivity implements Load
case android.R.id.home:
Intent intent = new Intent(this, AccountActivity.class);
intent.putExtra(AccountActivity.EXTRA_ACCOUNT_NAME, account.name);
startActivity(intent);
NavUtils.navigateUpTo(this, intent);
break;
case R.id.create_address_book:
createAddressBook();
@ -92,71 +97,158 @@ public class CreateAddressBookActivity extends AppCompatActivity implements Load
protected void createAddressBook() {
Spinner spnrHomeSets = (Spinner)findViewById(R.id.homeset);
HashMap<String, String> homeSet = (HashMap<String, String>)spnrHomeSets.getSelectedItem();
HttpUrl urlHomeSet = HttpUrl.parse(homeSet.get(ServiceDB.HomeSets.URL));
CollectionInfo info = new CollectionInfo();
info.url = urlHomeSet.resolve("myAddrBook.vcf").toString();
info.displayName = "myAddrBook";
new AddressBookCreator().execute( info);
boolean ok = true;
String displayName = ((EditText)findViewById(R.id.title)).getText().toString();
if (!TextUtils.isEmpty(displayName))
info.displayName = displayName;
EditText editPathSegment = (EditText)findViewById(R.id.path_segment);
String pathSegment = editPathSegment.getText().toString();
if (TextUtils.isEmpty(pathSegment)) { // TODO further validations
editPathSegment.setError("MUST NOT BE EMPTY");
ok = false;
} else
info.url = urlHomeSet.resolve(pathSegment).toString();
String description = ((EditText)findViewById(R.id.description)).getText().toString();
if (!TextUtils.isEmpty(description))
info.description = description;
if (ok)
CreatingAddressBookFragment.newInstance(account, info).show(getSupportFragmentManager(), null);
}
// AsyncTask for creating the address book
public static class CreatingAddressBookFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<Exception> {
protected static final String
ARGS_ACCOUNT = "account",
ARGS_COLLECTION_INFO = "collectionInfo";
public static CreatingAddressBookFragment newInstance(Account account, CollectionInfo info) {
CreatingAddressBookFragment frag = new CreatingAddressBookFragment();
Bundle args = new Bundle(2);
args.putParcelable(ARGS_ACCOUNT, account);
args.putSerializable(ARGS_COLLECTION_INFO, info);
frag.setArguments(args);
return frag;
}
class AddressBookCreator extends AsyncTask<CollectionInfo, Void, Exception> {
@Override
protected void onPostExecute(Exception e) {
String msg = (e == null) ? "Created!" : e.getLocalizedMessage();
Toast.makeText(CreateAddressBookActivity.this, msg, Toast.LENGTH_LONG).show();
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().initLoader(0, getArguments(), this);
}
@Override
protected Exception doInBackground(CollectionInfo[] infoArray) {
AccountSettings accountSettings = new AccountSettings(CreateAddressBookActivity.this, account);
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = new ProgressDialog.Builder(getActivity())
.setTitle(R.string.create_address_book_creating)
.setMessage(R.string.please_wait)
.setCancelable(false)
.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
CollectionInfo info = infoArray[0];
OkHttpClient client = HttpClient.create(CreateAddressBookActivity.this);
client = HttpClient.addAuthentication(client, accountSettings.username(), accountSettings.password(), accountSettings.preemptiveAuth());
@Override
public Loader<Exception> onCreateLoader(int id, Bundle args) {
Account account = (Account)args.getParcelable(ARGS_ACCOUNT);
CollectionInfo info = (CollectionInfo)args.getSerializable(ARGS_COLLECTION_INFO);
return new AddressBookCreator(getActivity(), account, info);
}
DavResource addressBook = new DavResource(null, client, HttpUrl.parse(info.url));
@Override
public void onLoadFinished(Loader<Exception> loader, Exception exception) {
dismissAllowingStateLoss();
StringWriter writer = new StringWriter();
try {
XmlSerializer serializer = XmlUtils.newSerializer();
serializer.setOutput(writer);
serializer.startDocument("UTF-8", null);
serializer.setPrefix("", XmlUtils.NS_WEBDAV);
serializer.setPrefix("CARD", XmlUtils.NS_CARDDAV);
serializer.startTag(XmlUtils.NS_WEBDAV, "mkcol");
serializer.startTag(XmlUtils.NS_WEBDAV, "set");
serializer.startTag(XmlUtils.NS_WEBDAV, "prop");
serializer.startTag(XmlUtils.NS_WEBDAV, "resourcetype");
serializer.startTag(XmlUtils.NS_WEBDAV, "collection");
serializer.endTag(XmlUtils.NS_WEBDAV, "collection");
serializer.startTag(XmlUtils.NS_CARDDAV, "addressbook");
serializer.endTag(XmlUtils.NS_CARDDAV, "addressbook");
serializer.endTag(XmlUtils.NS_WEBDAV, "resourcetype");
serializer.startTag(XmlUtils.NS_WEBDAV, "displayname");
serializer.text(info.displayName);
serializer.endTag(XmlUtils.NS_WEBDAV, "displayname");
serializer.endTag(XmlUtils.NS_WEBDAV, "prop");
serializer.endTag(XmlUtils.NS_WEBDAV, "set");
serializer.endTag(XmlUtils.NS_WEBDAV, "mkcol");
serializer.endDocument();
} catch (IOException e) {
Constants.log.error("Couldn't assemble MKCOL request", e);
if (exception == null)
getActivity().finish();
else
Toast.makeText(getActivity(), exception.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
@Override
public void onLoaderReset(Loader<Exception> loader) {
}
protected static class AddressBookCreator extends AsyncTaskLoader<Exception> {
final Account account;
final CollectionInfo info;
final ServiceDB.OpenHelper dbHelper;
public AddressBookCreator(Context context, Account account, CollectionInfo collectionInfo) {
super(context);
this.account = account;
info = collectionInfo;
dbHelper = new ServiceDB.OpenHelper(context);
}
String error = null;
try {
addressBook.mkCol(writer.toString());
} catch (IOException|HttpException e) {
return e;
@Override
protected void onStartLoading() {
forceLoad();
}
@Override
public Exception loadInBackground() {
OkHttpClient client = HttpClient.create(getContext());
client = HttpClient.addAuthentication(client, new AccountSettings(getContext(), account));
StringWriter writer = new StringWriter();
try {
XmlSerializer serializer = XmlUtils.newSerializer();
serializer.setOutput(writer);
serializer.startDocument("UTF-8", null);
serializer.setPrefix("", XmlUtils.NS_WEBDAV);
serializer.setPrefix("CARD", XmlUtils.NS_CARDDAV);
serializer.startTag(XmlUtils.NS_WEBDAV, "mkcol");
serializer.startTag(XmlUtils.NS_WEBDAV, "set");
serializer.startTag(XmlUtils.NS_WEBDAV, "prop");
serializer.startTag(XmlUtils.NS_WEBDAV, "resourcetype");
serializer.startTag(XmlUtils.NS_WEBDAV, "collection");
serializer.endTag(XmlUtils.NS_WEBDAV, "collection");
serializer.startTag(XmlUtils.NS_CARDDAV, "addressbook");
serializer.endTag(XmlUtils.NS_CARDDAV, "addressbook");
serializer.endTag(XmlUtils.NS_WEBDAV, "resourcetype");
if (info.displayName != null) {
serializer.startTag(XmlUtils.NS_WEBDAV, "displayname");
serializer.text(info.displayName);
serializer.endTag(XmlUtils.NS_WEBDAV, "displayname");
}
if (info.description != null) {
serializer.startTag(XmlUtils.NS_CARDDAV, "addressbook-description");
serializer.text(info.description);
serializer.endTag(XmlUtils.NS_CARDDAV, "addressbook-description");
}
serializer.endTag(XmlUtils.NS_WEBDAV, "prop");
serializer.endTag(XmlUtils.NS_WEBDAV, "set");
serializer.endTag(XmlUtils.NS_WEBDAV, "mkcol");
serializer.endDocument();
} catch (IOException e) {
Constants.log.error("Couldn't assemble MKCOL request", e);
}
DavResource addressBook = new DavResource(null, client, HttpUrl.parse(info.url));
try {
addressBook.mkCol(writer.toString());
// TODO
/*SQLiteDatabase db = dbHelper.getWritableDatabase();
db.insert(ServiceDB.Collections._TABLE, null, info.toDB());*/
// TODO add to database
} catch (IOException|HttpException e) {
return e;
} finally {
dbHelper.close();
}
return null;
}
return null;
}
}
@ -194,12 +286,11 @@ public class CreateAddressBookActivity extends AppCompatActivity implements Load
private static class AccountLoader extends AsyncTaskLoader<AccountInfo> {
private final Account account;
ServiceDB.OpenHelper dbHelper;
private final ServiceDB.OpenHelper dbHelper;
public AccountLoader(Context context, Account account) {
super(context);
this.account = account;
dbHelper = new ServiceDB.OpenHelper(context);
}

@ -0,0 +1,221 @@
/*
* 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 at.bitfire.davdroid.ui;
import android.accounts.Account;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.AppCompatCheckBox;
import android.support.v7.widget.AppCompatRadioButton;
import android.support.v7.widget.AppCompatSpinner;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.URLUtil;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Toast;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.TimeZoneRegistry;
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
import net.fortuna.ical4j.util.TimeZones;
import org.apache.commons.lang3.StringUtils;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;
import java.util.UUID;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.ical4android.DateUtils;
import lombok.Cleanup;
import okhttp3.HttpUrl;
import yuku.ambilwarna.AmbilWarnaDialog;
import yuku.ambilwarna.AmbilWarnaSquare;
public class CreateCalendarActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<CreateCalendarActivity.AccountInfo> {
public static final String EXTRA_ACCOUNT = "account";
protected Account account;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
account = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.activity_create_calendar);
final View colorSquare = findViewById(R.id.color);
colorSquare.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AmbilWarnaDialog(CreateCalendarActivity.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();
}
});
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_create_calendar, menu);
return true;
}
public void onCreateCalendar(MenuItem item) {
boolean ok = true;
CollectionInfo info = new CollectionInfo();
AppCompatSpinner spinner = (AppCompatSpinner)findViewById(R.id.home_set);
String homeSet = (String)spinner.getSelectedItem();
EditText edit = (EditText)findViewById(R.id.display_name);
info.displayName = edit.getText().toString();
if (TextUtils.isEmpty(info.displayName)) {
edit.setError("Enter a calendar title.");
ok = false;
}
edit = (EditText)findViewById(R.id.description);
info.description = StringUtils.trimToNull(edit.getText().toString());
View view = findViewById(R.id.color);
info.color = ((ColorDrawable)view.getBackground()).getColor();
spinner = (AppCompatSpinner)findViewById(R.id.time_zone);
net.fortuna.ical4j.model.TimeZone tz = DateUtils.tzRegistry.getTimeZone((String)spinner.getSelectedItem());
if (tz != null) {
Calendar cal = new Calendar();
cal.getComponents().add(tz.getVTimeZone());
info.timeZone = cal.toString();
}
RadioGroup typeGroup = (RadioGroup)findViewById(R.id.type);
switch (typeGroup.getCheckedRadioButtonId()) {
case R.id.type_events:
info.supportsVEVENT = true;
break;
case R.id.type_tasks:
info.supportsVTODO = true;
break;
case R.id.type_events_and_tasks:
info.supportsVEVENT = true;
info.supportsVTODO = true;
break;
}
if (ok) {
info.type = CollectionInfo.Type.CALENDAR;
info.url = HttpUrl.parse(homeSet).resolve(UUID.randomUUID().toString() + "/").toString();
CreateCollectionFragment.newInstance(account, info).show(getSupportFragmentManager(), null);
}
}
@Override
public Loader<AccountInfo> onCreateLoader(int id, Bundle args) {
return new AccountInfoLoader(this, account);
}
@Override
public void onLoadFinished(Loader<AccountInfo> loader, AccountInfo info) {
AppCompatSpinner spinner = (AppCompatSpinner)findViewById(R.id.time_zone);
String[] timeZones = TimeZone.getAvailableIDs();
spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, timeZones));
// select system time zone
String defaultTimeZone = TimeZone.getDefault().getID();
for (int i = 0; i < timeZones.length; i++)
if (timeZones[i].equals(defaultTimeZone)) {
spinner.setSelection(i);
break;
}
if (info != null) {
spinner = (AppCompatSpinner)findViewById(R.id.home_set);
spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, info.homeSets));
}
}
@Override
public void onLoaderReset(Loader<AccountInfo> loader) {
}
protected static class AccountInfo {
List<String> homeSets = new LinkedList<>();
}
protected static class AccountInfoLoader extends AsyncTaskLoader<AccountInfo> {
private final Account account;
private final ServiceDB.OpenHelper dbHelper;
public AccountInfoLoader(Context context, Account account) {
super(context);
this.account = account;
dbHelper = new ServiceDB.OpenHelper(context);
}
@Override
protected void onStartLoading() {
forceLoad();
}
@Override
public AccountInfo loadInBackground() {
final AccountInfo info = new AccountInfo();
// find DAV service and home sets
SQLiteDatabase db = dbHelper.getReadableDatabase();
try {
@Cleanup Cursor cursorService = db.query(ServiceDB.Services._TABLE, new String[] { ServiceDB.Services.ID },
ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?",
new String[] { account.name, ServiceDB.Services.SERVICE_CALDAV }, null, null, null);
if (!cursorService.moveToNext())
return null;
String strServiceID = cursorService.getString(0);
@Cleanup Cursor cursorHomeSets = db.query(ServiceDB.HomeSets._TABLE, new String[] { ServiceDB.HomeSets.URL },
ServiceDB.HomeSets.SERVICE_ID + "=?", new String[] { strServiceID }, null, null, null);
while (cursorHomeSets.moveToNext())
info.homeSets.add(cursorHomeSets.getString(0));
} finally {
dbHelper.close();
}
return info;
}
}
}

@ -0,0 +1,222 @@
/*
* 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 at.bitfire.davdroid.ui;
import android.accounts.Account;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.app.DialogFragment;
import android.widget.Toast;
import org.apache.commons.lang3.BooleanUtils;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.StringWriter;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.XmlUtils;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.DavUtils;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.syncadapter.AccountSettings;
import at.bitfire.ical4android.DateUtils;
import lombok.Cleanup;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
public class CreateCollectionFragment extends DialogFragment {
private static final String
ARG_ACCOUNT = "account",
ARG_COLLECTION_INFO = "collectionInfo";
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);
setRetainInstance(true);
new CreateCollectionTask().execute(getArguments());
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = new ProgressDialog.Builder(getContext())
.setTitle(R.string.create_collection_creating)
.setMessage(R.string.please_wait)
.create();
setCancelable(false);
return dialog;
}
protected class CreateCollectionTask extends AsyncTask<Bundle, Void, Exception> {
@Override
protected Exception doInBackground(Bundle... params) {
Bundle args = params[0];
Account account = args.getParcelable(ARG_ACCOUNT);
CollectionInfo info = (CollectionInfo)args.getSerializable(ARG_COLLECTION_INFO);
OkHttpClient client = HttpClient.create(getContext());
client = HttpClient.addAuthentication(client, new AccountSettings(getContext(), account));
StringWriter writer = new StringWriter();
try {
XmlSerializer serializer = XmlUtils.newSerializer();
serializer.setOutput(writer);
serializer.startDocument("UTF-8", null);
serializer.setPrefix("", XmlUtils.NS_WEBDAV);
serializer.setPrefix("CAL", XmlUtils.NS_CALDAV);
serializer.setPrefix("CARD", XmlUtils.NS_CARDDAV);
serializer.startTag(XmlUtils.NS_WEBDAV, "mkcol");
serializer.startTag(XmlUtils.NS_WEBDAV, "set");
serializer.startTag(XmlUtils.NS_WEBDAV, "prop");
serializer.startTag(XmlUtils.NS_WEBDAV, "resourcetype");
serializer.startTag(XmlUtils.NS_WEBDAV, "collection");
serializer.endTag(XmlUtils.NS_WEBDAV, "collection");
if (info.type == CollectionInfo.Type.ADDRESS_BOOK) {
serializer.startTag(XmlUtils.NS_CARDDAV, "addressbook");
serializer.endTag(XmlUtils.NS_CARDDAV, "addressbook");
} else if (info.type == CollectionInfo.Type.CALENDAR) {
serializer.startTag(XmlUtils.NS_CALDAV, "calendar");
serializer.endTag(XmlUtils.NS_CALDAV, "calendar");
}
serializer.endTag(XmlUtils.NS_WEBDAV, "resourcetype");
if (info.displayName != null) {
serializer.startTag(XmlUtils.NS_WEBDAV, "displayname");
serializer.text(info.displayName);
serializer.endTag(XmlUtils.NS_WEBDAV, "displayname");
}
// addressbook-specific properties
if (info.type == CollectionInfo.Type.ADDRESS_BOOK) {
if (info.description != null) {
serializer.startTag(XmlUtils.NS_CARDDAV, "addressbook-description");
serializer.text(info.description);
serializer.endTag(XmlUtils.NS_CARDDAV, "addressbook-description");
}
}
// calendar-specific properties
if (info.type == CollectionInfo.Type.CALENDAR) {
if (info.description != null) {
serializer.startTag(XmlUtils.NS_CALDAV, "calendar-description");
serializer.text(info.description);
serializer.endTag(XmlUtils.NS_CALDAV, "calendar-description");
}
if (info.color != null) {
serializer.startTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
serializer.text(DavUtils.ARGBtoCalDAVColor(info.color));
serializer.endTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
}
if (info.timeZone != null) {
serializer.startTag(XmlUtils.NS_CALDAV, "calendar-timezone");
serializer.cdsect(info.timeZone);
serializer.endTag(XmlUtils.NS_CALDAV, "calendar-timezone");
}
serializer.startTag(XmlUtils.NS_CALDAV, "supported-calendar-component-set");
if (BooleanUtils.isTrue(info.supportsVEVENT)) {
serializer.startTag(XmlUtils.NS_CALDAV, "comp");
serializer.attribute(null, "name", "VEVENT");
serializer.endTag(XmlUtils.NS_CALDAV, "comp");
}
if (BooleanUtils.isTrue(info.supportsVTODO)) {
serializer.startTag(XmlUtils.NS_CALDAV, "comp");
serializer.attribute(null, "name", "VTODO");
serializer.endTag(XmlUtils.NS_CALDAV, "comp");
}
serializer.endTag(XmlUtils.NS_CALDAV, "supported-calendar-component-set");
}
serializer.endTag(XmlUtils.NS_WEBDAV, "prop");
serializer.endTag(XmlUtils.NS_WEBDAV, "set");
serializer.endTag(XmlUtils.NS_WEBDAV, "mkcol");
serializer.endDocument();
} catch (IOException e) {
Constants.log.error("Couldn't assemble Extended MKCOL request", e);
}
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
DavResource collection = new DavResource(null, client, HttpUrl.parse(info.url));
try {
// create collection on remote server
collection.mkCol(writer.toString());
// now insert collection into database:
SQLiteDatabase db = dbHelper.getWritableDatabase();
// 1. find service ID
String serviceType = null;
if (info.type == CollectionInfo.Type.ADDRESS_BOOK)
serviceType = ServiceDB.Services.SERVICE_CARDDAV;
else if (info.type == CollectionInfo.Type.CALENDAR)
serviceType = ServiceDB.Services.SERVICE_CALDAV;
else
throw new IllegalArgumentException("Collection must be an address book or calendar");
@Cleanup Cursor c = db.query(ServiceDB.Services._TABLE, new String[] { ServiceDB.Services.ID },
ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?",
new String[] { account.name, serviceType }, null, null, null
);
if (!c.moveToNext())
throw new IllegalStateException();
long serviceID = c.getLong(0);
// 2. add collection to service
ContentValues values = info.toDB();
values.put(ServiceDB.Collections.SERVICE_ID, serviceID);
db.insert(ServiceDB.Collections._TABLE, null, values);
} catch(IOException|HttpException|IllegalStateException e) {
return e;
} finally {
dbHelper.close();
}
return null;
}
@Override
protected void onPostExecute(Exception e) {
dismissAllowingStateLoss();
Activity parent = getActivity();
if (parent != null) {
if (e != null)
Toast.makeText(parent, e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
else
parent.finish();
}
}
}
}

@ -12,6 +12,7 @@ import android.accounts.Account;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.support.annotation.NonNull;
@ -19,14 +20,16 @@ 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 android.widget.Toast;
import java.io.IOException;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.syncadapter.AccountSettings;
@ -38,15 +41,6 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa
ARG_ACCOUNT = "account",
ARG_COLLECTION_INFO = "collectionInfo";
public static DeleteCollectionFragment newInstance(Account account, CollectionInfo collectionInfo) {
DeleteCollectionFragment frag = new DeleteCollectionFragment();
Bundle args = new Bundle(2);
args.putParcelable(ARG_ACCOUNT, account);
args.putSerializable(ARG_COLLECTION_INFO, collectionInfo);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -57,11 +51,10 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = new ProgressDialog.Builder(getContext())
.setTitle("Deleting collection")
.setMessage("Deleting collection from server, please wait.")
.setCancelable(false)
.setTitle(R.string.delete_collection_deleting_collection)
.setMessage(R.string.please_wait)
.create();
dialog.setCanceledOnTouchOutside(false);
setCancelable(false);
return dialog;
}
@ -79,8 +72,8 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa
@Override
public void onLoadFinished(Loader<Exception> loader, Exception exception) {
String msg = (exception == null) ? "Collection deleted" : exception.getLocalizedMessage();
Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show();
if (exception != null)
Toast.makeText(getContext(), exception.getLocalizedMessage(), Toast.LENGTH_LONG).show();
dismissAllowingStateLoss();
AccountActivity activity = (AccountActivity)getActivity();
@ -113,23 +106,64 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa
@Override
public Exception loadInBackground() {
SQLiteDatabase db = dbHelper.getReadableDatabase();
OkHttpClient httpClient = HttpClient.create(getContext());
httpClient = HttpClient.addAuthentication(httpClient, new AccountSettings(getContext(), account));
DavResource collection = new DavResource(null, httpClient, url);
try {
// delete collection from server
collection.delete(null);
// delete collection locally
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete(ServiceDB.Collections._TABLE, ServiceDB.Collections.ID + "=?", new String[] { String.valueOf(collectionId) });
return null;
} catch (IOException|HttpException e) {
return e;
} finally {
db.close();
dbHelper.close();
}
}
}
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.url : 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,17 @@
<!--
~ 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
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,18h18v-2H3v2zm0,-5h18v-2H3v2zm0,-7v2h18V6H3z"/>
</vector>

@ -22,4 +22,22 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Title"/>
<EditText
android:id="@+id/path_segment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Path segment (collection name)"/>
<EditText
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Description"/>
</LinearLayout>

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent">
<LinearLayout android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Create CalDAV collection"
android:textAppearance="@style/TextView.Heading"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Home set:"/>
<android.support.v7.widget.AppCompatSpinner
android:id="@+id/home_set"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Display name (title) of this collection:"/>
<EditText
android:id="@+id/display_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="My Calendar"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Description (optional):"/>
<EditText
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="Calendar description"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<View
android:id="@+id/color"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="8dp"
android:background="@color/orangeA700"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Set a collection color"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Time zone:"/>
<android.support.v7.widget.AppCompatSpinner
android:id="@+id/time_zone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Collection type"
android:textAppearance="@style/TextView.Heading"/>
<RadioGroup
android:id="@+id/type"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.AppCompatRadioButton
android:id="@+id/type_events"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="Calendar (only events)"/>
<android.support.v7.widget.AppCompatRadioButton
android:id="@+id/type_tasks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Task list (only tasks)"/>
<android.support.v7.widget.AppCompatRadioButton
android:id="@+id/type_events_and_tasks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Combined (events and tasks)"/>
</RadioGroup>
</LinearLayout>
</ScrollView>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="Create calendar"
android:onClick="onCreateCalendar"
app:showAsAction="always"/>
</menu>

@ -13,7 +13,7 @@
<string name="app_name">DAVdroid</string>
<string name="help">Help</string>
<string name="manage_accounts">Manage accounts</string>
<string name="please_wait">Please wait</string>
<string name="please_wait">Please wait</string>
<string name="send">Send</string>
<string name="skip">Skip</string>
@ -170,8 +170,14 @@
<item>Once a day</item>
</string-array>
<!-- CreateAddressBookActivity -->
<!-- collection management -->
<string name="create_address_book_title">Create address book</string>
<string name="create_address_book_creating">Creating address book</string>
<string name="create_calendar_title">Create calendar</string>
<string name="create_collection_creating">Creating collection</string>
<string name="delete_collection_confirm_title">Are you sure?</string>
<string name="delete_collection_confirm_warning">This collection (%s) and all its entries from the server will be removed from the server.</string>
<string name="delete_collection_deleting_collection">Deleting collection</string>
<string name="settings_android_update_title">Android version update</string>
<string name="settings_android_update_description">Android version updates may have an impact on how DAVdroid works. If there are problems, please delete your DAVdroid accounts and add them again.</string>

@ -92,10 +92,10 @@
<!-- text content -->
<!-- <style name="TextView.Heading" parent="AppTheme">
<item name="android:padding">5dp</item>
<item name="android:background">#7ca42b</item>
<item name="android:textColor">@color/white</item>
</style> -->
<style name="TextView.Heading" parent="AppTheme">
<item name="android:layout_marginBottom">8dp</item>
<item name="android:textSize">22sp</item>
<item name="android:textColor">@color/orangeA700</item>
</style>
</resources>

@ -1 +1 @@
Subproject commit 0e9522bb3a2d4e33bac214883f8316b91be0960e
Subproject commit fd19c6531ad5c1cbe210a7e70f5781cbfd5744a7
Loading…
Cancel
Save