1
0
mirror of https://github.com/etesync/android synced 2024-11-22 16:08:13 +00:00

Version 0.4.4

* allow to enter account name at the end of account setup (fixes #13)
* advise user to enter email address as account name (closes #24)
* support for iCal TRANSP (availability free/busy field)
* create better UIDs using iCal4j UidGenerator and Settings.Secure.ANDROID_ID
* better ORGANIZER/ATTENDEE support, but there are big Android issues yet
* various improvements and bug fixes
This commit is contained in:
rfc2822 2013-12-01 16:07:27 +01:00
parent 44cf628dd6
commit 1986837be8
21 changed files with 352 additions and 126 deletions

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="at.bitfire.davdroid"
android:versionCode="16"
android:versionName="0.4.3-alpha" >
android:versionCode="17"
android:versionName="0.4.4-alpha" >
<uses-sdk
android:minSdkVersion="14"

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scrollbars="vertical" >
<GridLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:columnCount="2"
android:padding="10dp"
android:useDefaultMargins="true" >
<TextView
android:layout_columnSpan="2"
android:layout_gravity="left"
android:layout_marginBottom="20dp"
android:text="@string/account_details"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:layout_gravity="left"
android:text="@string/account_name"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/account_name"
android:layout_gravity="fill_horizontal"
android:hint="@string/account_name_hint"
android:inputType="textEmailAddress" />
<TextView
android:id="@+id/account_name_info"
android:layout_width="fill_parent"
android:layout_columnSpan="2"
android:layout_gravity="left"
android:drawableLeft="@drawable/extra_actions_about"
android:drawablePadding="10dp"
android:padding="10dp"
android:text="@string/account_name_info"
android:textAppearance="?android:attr/textAppearanceMedium" />
<Space
android:layout_gravity="left|top"
android:layout_row="3" />
</GridLayout>
</ScrollView>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/add_account"
android:icon="@drawable/navigation_accept"
android:showAsAction="always|withText"
android:title="@string/add_account">
</item>
</menu>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" ><item
android:id="@+id/add_account"
android:icon="@drawable/navigation_accept"
android:showAsAction="always|withText"
android:title="@string/add_account" android:orderInCategory="2">
</item>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
</menu>
<item
android:id="@+id/next"
android:icon="@drawable/navigation_forward"
android:showAsAction="always|withText"
android:title="@string/next">
</item>
</menu>

View File

@ -75,5 +75,11 @@
* <a href="http://simple.sourceforge.net/">Simple XML Serialization</a> (<a href="http://www.apache.org/licenses/LICENSE-2.0">Apache License, Version 2.0</a>)<br/>
* <a href="http://projectlombok.org/">Project Lombok</a> (<a href="http://opensource.org/licenses/mit-license.php">MIT License</a>)</p>
]]></string>
<string name="account_details">Konto-Details</string>
<string name="account_name">Kontoname:</string>
<string name="account_name_hint">Mein CalDAV/CardDAV-Konto</string>
<string name="email_address">Email-Adresse:</string>
<string name="organizer_hint">"ORGANIZER der von Ihnen angelegten Termine; notwendig für Teilnehmer-Info"</string>
<string name="account_name_info">"Verwenden Sie Ihre Email-Addresse als Kontoname, da Android den Kontonamen als ORGANIZER-Feld in Terminen benutzt. Sie können keine zwei Konten mit dem gleichen Namen anlegen.</string>
</resources>

View File

@ -83,5 +83,11 @@
* <a href="http://simple.sourceforge.net/">Simple XML Serialization</a> (<a href="http://www.apache.org/licenses/LICENSE-2.0">Apache License, Version 2.0</a>)<br/>
* <a href="http://projectlombok.org/">Project Lombok</a> (<a href="http://opensource.org/licenses/mit-license.php">MIT License</a>)</p>
]]></string>
<string name="account_details">Account details</string>
<string name="account_name">Account name:</string>
<string name="account_name_hint">My CalDAV/CardDAV Account</string>
<string name="email_address">Email address:</string>
<string name="organizer_hint">"ORGANIZER of your events; required if you use attendee info"</string>
<string name="account_name_info">"Use your email address as account name because Android will use the account name as ORGANIZER field for events you create. You can't have two accounts with the same name.</string>
</resources>

View File

@ -9,7 +9,7 @@ package at.bitfire.davdroid;
public class Constants {
public static final String
APP_VERSION = "0.4.3-alpha",
APP_VERSION = "0.4.4-alpha",
ACCOUNT_TYPE = "bitfire.at.davdroid",

View File

@ -50,12 +50,13 @@ import net.fortuna.ical4j.model.property.RDate;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.model.property.Status;
import net.fortuna.ical4j.model.property.Summary;
import net.fortuna.ical4j.model.property.Transp;
import net.fortuna.ical4j.model.property.Uid;
import net.fortuna.ical4j.model.property.Version;
import net.fortuna.ical4j.util.UidGenerator;
import android.text.format.Time;
import android.util.Log;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.syncadapter.DavSyncAdapter;
public class Event extends Resource {
@ -76,6 +77,7 @@ public class Event extends Resource {
@Getter @Setter private Boolean forPublic;
@Getter @Setter private Status status;
@Getter @Setter private boolean opaque;
@Getter @Setter private Organizer organizer;
@Getter private List<Attendee> attendees = new LinkedList<Attendee>();
@ -122,8 +124,7 @@ public class Event extends Resource {
uid = event.getUid().getValue();
else {
Log.w(TAG, "Received VEVENT without UID, generating new one");
UidGenerator uidGenerator = new UidGenerator(Integer.toString(android.os.Process.myPid()));
uid = uidGenerator.generateUid().getValue();
uid = DavSyncAdapter.generateUID();
}
dtStart = event.getStartDate(); validateTimeZone(dtStart);
@ -144,6 +145,10 @@ public class Event extends Resource {
status = event.getStatus();
opaque = true;
if (event.getTransparency() == Transp.TRANSPARENT)
opaque = false;
organizer = event.getOrganizer();
for (Object o : event.getProperties(Property.ATTENDEE))
attendees.add((Attendee)o);
@ -196,6 +201,8 @@ public class Event extends Resource {
if (status != null)
props.add(status);
if (!opaque)
props.add(Transp.TRANSPARENT);
if (organizer != null)
props.add(organizer);

View File

@ -7,6 +7,7 @@
******************************************************************************/
package at.bitfire.davdroid.resource;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.Date;
@ -120,7 +121,10 @@ public class LocalCalendar extends LocalCollection<Event> {
values.put(Calendars.CALENDAR_COLOR, color);
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE);
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_RESOURCE);
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE);
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1);
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1);
values.put(Calendars.OWNER_ACCOUNT, account.name);
values.put(Calendars.SYNC_EVENTS, 1);
values.put(Calendars.VISIBLE, 1);
@ -196,7 +200,7 @@ public class LocalCalendar extends LocalCollection<Event> {
/* 8 */ Events.STATUS, Events.ACCESS_LEVEL,
/* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE,
/* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS,
/* 17 */ entryColumnUID(), Events.DURATION
/* 17 */ entryColumnUID(), Events.DURATION, Events.AVAILABILITY
}, null, null, null);
if (cursor != null && cursor.moveToNext()) {
e.setUid(cursor.getString(17));
@ -270,12 +274,15 @@ public class LocalCalendar extends LocalCollection<Event> {
e.setStatus(Status.VEVENT_CANCELLED);
}
// availability
e.setOpaque(cursor.getInt(19) != Events.AVAILABILITY_FREE);
// attendees
if (cursor.getInt(14) != 0) { // has attendees
try {
e.setOrganizer(new Organizer("mailto:" + cursor.getString(15)));
e.setOrganizer(new Organizer(new URI("mailto", cursor.getString(15), null)));
} catch (URISyntaxException ex) {
Log.e(TAG, "Error parsing organizer URI, ignoring");
Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex);
}
Uri attendeesUri = Attendees.CONTENT_URI.buildUpon()
@ -287,7 +294,7 @@ public class LocalCalendar extends LocalCollection<Event> {
}, Attendees.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
while (c != null && c.moveToNext()) {
try {
Attendee attendee = new Attendee("mailto:" + c.getString(0));
Attendee attendee = new Attendee(new URI("mailto", c.getString(0), null));
ParameterList params = attendee.getParameters();
String cn = c.getString(1);
@ -314,13 +321,7 @@ public class LocalCalendar extends LocalCollection<Event> {
}
// status
int status = Attendees.ATTENDEE_STATUS_NONE;
if (relationship == Attendees.RELATIONSHIP_ORGANIZER) // we are organizer
status = cursor.getInt(16);
else
status = c.getInt(4);
switch (status) {
switch (c.getInt(4)) {
case Attendees.ATTENDEE_STATUS_INVITED:
params.add(PartStat.NEEDS_ACTION);
break;
@ -337,7 +338,7 @@ public class LocalCalendar extends LocalCollection<Event> {
e.addAttendee(attendee);
} catch (URISyntaxException ex) {
Log.e(TAG, "Couldn't parse attendee member URI, ignoring member");
Log.e(TAG, "Couldn't parse attendee information, ignoring", ex);
}
}
}
@ -429,7 +430,10 @@ public class LocalCalendar extends LocalCollection<Event> {
.withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
.withValue(Events.DTSTART, event.getDtStartInMillis())
.withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID())
.withValue(Events.HAS_ATTENDEE_DATA, event.getAttendees().isEmpty() ? 0 : 1);
.withValue(Events.HAS_ATTENDEE_DATA, event.getAttendees().isEmpty() ? 0 : 1)
.withValue(Events.GUESTS_CAN_INVITE_OTHERS, 1)
.withValue(Events.GUESTS_CAN_MODIFY, 1)
.withValue(Events.GUESTS_CAN_SEE_GUESTS, 1);
boolean recurring = false;
if (event.getRrule() != null) {
@ -478,6 +482,12 @@ public class LocalCalendar extends LocalCollection<Event> {
if (event.getDescription() != null)
builder = builder.withValue(Events.DESCRIPTION, event.getDescription());
if (event.getOrganizer() != null && event.getOrganizer().getCalAddress() != null) {
URI organizer = event.getOrganizer().getCalAddress();
if (organizer.getScheme().equalsIgnoreCase("mailto"))
builder = builder.withValue(Events.ORGANIZER, organizer.getSchemeSpecificPart());
}
Status status = event.getStatus();
if (status != null) {
int statusCode = Events.STATUS_TENTATIVE;
@ -488,6 +498,8 @@ public class LocalCalendar extends LocalCollection<Event> {
builder = builder.withValue(Events.STATUS, statusCode);
}
builder = builder.withValue(Events.AVAILABILITY, event.isOpaque() ? Events.AVAILABILITY_BUSY : Events.AVAILABILITY_FREE);
if (event.getForPublic() != null)
builder = builder.withValue(Events.ACCESS_LEVEL, event.getForPublic() ? Events.ACCESS_PUBLIC : Events.ACCESS_PRIVATE);
@ -547,7 +559,7 @@ public class LocalCalendar extends LocalCollection<Event> {
int status = Attendees.ATTENDEE_STATUS_NONE;
PartStat partStat = (PartStat)attendee.getParameter(Parameter.PARTSTAT);
if (partStat == PartStat.NEEDS_ACTION)
if (partStat == null || partStat == PartStat.NEEDS_ACTION)
status = Attendees.ATTENDEE_STATUS_INVITED;
else if (partStat == PartStat.ACCEPTED)
status = Attendees.ATTENDEE_STATUS_ACCEPTED;

View File

@ -9,7 +9,6 @@ package at.bitfire.davdroid.resource;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.UUID;
import net.fortuna.ical4j.model.ValidationException;
import android.accounts.Account;
@ -23,6 +22,7 @@ import android.net.Uri;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.util.Log;
import at.bitfire.davdroid.syncadapter.DavSyncAdapter;
public abstract class LocalCollection<T extends Resource> {
private static final String TAG = "davdroid.LocalCollection";
@ -100,9 +100,9 @@ public abstract class LocalCollection<T extends Resource> {
where, null, null);
LinkedList<T> fresh = new LinkedList<T>();
while (cursor != null && cursor.moveToNext()) {
String uid = UUID.randomUUID().toString(),
resourceName = uid + fileExtension();
T resource = findById(cursor.getLong(0), resourceName, null, true); //new Event(cursor.getLong(0), resourceName, null);
String uid = DavSyncAdapter.generateUID(),
resourceName = uid.replace("@", "_") + fileExtension();
T resource = findById(cursor.getLong(0), resourceName, null, true);
resource.setUid(uid);
// new record: set generated resource name in database

View File

@ -10,11 +10,11 @@ package at.bitfire.davdroid.resource;
import java.io.IOException;
import java.io.InputStream;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.ValidationException;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.ValidationException;
@ToString
public abstract class Resource {

View File

@ -0,0 +1,143 @@
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Fragment;
import android.content.ContentResolver;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalCalendar;
public class AccountDetailsFragment extends Fragment implements TextWatcher {
public static final String KEY_SERVER_INFO = "server_info";
ServerInfo serverInfo;
EditText editAccountName;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.account_details, container, false);
serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO);
editAccountName = (EditText)v.findViewById(R.id.account_name);
editAccountName.addTextChangedListener(this);
TextView textAccountNameInfo = (TextView)v.findViewById(R.id.account_name_info);
if (!serverInfo.hasEnabledCalendars())
textAccountNameInfo.setVisibility(View.GONE);
setHasOptionsMenu(true);
return v;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.account_details, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.add_account:
addAccount();
break;
default:
return false;
}
return true;
}
// actions
void addAccount() {
ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO);
try {
String accountName = editAccountName.getText().toString();
AccountManager accountManager = AccountManager.get(getActivity());
Account account = new Account(accountName, Constants.ACCOUNT_TYPE);
Bundle userData = new Bundle();
userData.putString(Constants.ACCOUNT_KEY_BASE_URL, serverInfo.getBaseURL());
userData.putString(Constants.ACCOUNT_KEY_USERNAME, serverInfo.getUserName());
userData.putString(Constants.ACCOUNT_KEY_AUTH_PREEMPTIVE, Boolean.toString(serverInfo.isAuthPreemptive()));
boolean syncContacts = false;
for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks())
if (addressBook.isEnabled()) {
userData.putString(Constants.ACCOUNT_KEY_ADDRESSBOOK_PATH, addressBook.getPath());
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
syncContacts = true;
continue;
}
if (syncContacts) {
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
} else
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0);
if (accountManager.addAccountExplicitly(account, serverInfo.getPassword(), userData)) {
// account created, now create calendars
boolean syncCalendars = false;
for (ServerInfo.ResourceInfo calendar : serverInfo.getCalendars())
if (calendar.isEnabled()) {
LocalCalendar.create(account, getActivity().getContentResolver(), calendar);
syncCalendars = true;
}
if (syncCalendars) {
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, true);
} else
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0);
getActivity().finish();
} else
Toast.makeText(getActivity(), "Couldn't create account (account with this name already existing?)", Toast.LENGTH_LONG).show();
} catch (RemoteException e) {
}
}
// input validation
@Override
public void onPrepareOptionsMenu(Menu menu) {
boolean ok = false;
ok = editAccountName.getText().length() > 0;
MenuItem item = menu.findItem(R.id.add_account);
item.setEnabled(ok);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
getActivity().invalidateOptionsMenu();
}
@Override
public void afterTextChanged(Editable s) {
}
}

View File

@ -3,6 +3,9 @@ package at.bitfire.davdroid.syncadapter;
import java.io.IOException;
import java.util.Map;
import net.fortuna.ical4j.util.SimpleHostInfo;
import net.fortuna.ical4j.util.UidGenerator;
import org.apache.http.HttpException;
import org.apache.http.auth.AuthenticationException;
@ -16,6 +19,7 @@ import android.content.OperationApplicationException;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.Log;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.RemoteCollection;
@ -26,12 +30,24 @@ public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter {
protected AccountManager accountManager;
private static String androidID;
public DavSyncAdapter(Context context) {
super(context, true);
androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
accountManager = AccountManager.get(context);
}
public static String generateUID() {
UidGenerator generator = new UidGenerator(new SimpleHostInfo(androidID), String.valueOf(android.os.Process.myPid()));
return generator.generateUid().getValue();
}
protected abstract Map<LocalCollection<?>, RemoteCollection<?>> getSyncPairs(Account account, ContentProviderClient provider);

View File

@ -7,6 +7,9 @@
******************************************************************************/
package at.bitfire.davdroid.syncadapter;
import java.net.MalformedURLException;
import java.net.URL;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentTransaction;
@ -38,11 +41,6 @@ public class EnterCredentialsFragment extends Fragment implements TextWatcher {
Button btnNext;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.enter_credentials, container, false);
@ -103,10 +101,9 @@ public class EnterCredentialsFragment extends Fragment implements TextWatcher {
void queryServer() {
FragmentTransaction ft = getFragmentManager().beginTransaction();
String host_path = editBaseURL.getText().toString();
Bundle args = new Bundle();
String host_path = editBaseURL.getText().toString();
args.putString(QueryServerDialogFragment.EXTRA_BASE_URL, URIUtils.sanitize(protocol + host_path));
args.putString(QueryServerDialogFragment.EXTRA_USER_NAME, editUserName.getText().toString());
args.putString(QueryServerDialogFragment.EXTRA_PASSWORD, editPassword.getText().toString());
@ -118,15 +115,22 @@ public class EnterCredentialsFragment extends Fragment implements TextWatcher {
}
// form validation
// input validation
@Override
public void onPrepareOptionsMenu(Menu menu) {
boolean ok =
editBaseURL.getText().length() > 0 &&
!editBaseURL.getText().toString().startsWith("/") && // host name required
editUserName.getText().length() > 0 &&
editPassword.getText().length() > 0;
// check host name
try {
URL url = new URL(protocol + editBaseURL.getText().toString());
if (url.getHost() == null || url.getHost().isEmpty())
ok = false;
} catch (MalformedURLException e) {
ok = false;
}
MenuItem item = menu.findItem(R.id.next);
item.setEnabled(ok);

View File

@ -7,19 +7,8 @@
******************************************************************************/
package at.bitfire.davdroid.syncadapter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedList;
import java.util.List;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ListFragment;
import android.content.ContentResolver;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -30,10 +19,7 @@ import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.Toast;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalCalendar;
public class SelectCollectionsFragment extends ListFragment {
public static final String KEY_SERVER_INFO = "server_info";
@ -45,6 +31,12 @@ public class SelectCollectionsFragment extends ListFragment {
return v;
}
@Override
public void onDestroyView() {
super.onDestroyView();
setListAdapter(null);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
@ -84,8 +76,32 @@ public class SelectCollectionsFragment extends ListFragment {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.add_account:
addAccount();
case R.id.next:
ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO);
// synchronize only selected collections
for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks())
addressBook.setEnabled(false);
for (ServerInfo.ResourceInfo calendar : serverInfo.getCalendars())
calendar.setEnabled(false);
ListAdapter adapter = getListView().getAdapter();
for (long id : getListView().getCheckedItemIds()) {
int position = (int)id + 1; // +1 because header view is inserted at pos. 0
ServerInfo.ResourceInfo info = (ServerInfo.ResourceInfo)adapter.getItem(position);
info.setEnabled(true);
}
// pass to "account details" fragment
AccountDetailsFragment accountDetails = new AccountDetailsFragment();
Bundle arguments = new Bundle();
arguments.putSerializable(SelectCollectionsFragment.KEY_SERVER_INFO, serverInfo);
accountDetails.setArguments(arguments);
getFragmentManager().beginTransaction()
.replace(R.id.fragment_container, accountDetails)
.addToBackStack(null)
.commitAllowingStateLoss();
break;
default:
return false;
@ -94,7 +110,7 @@ public class SelectCollectionsFragment extends ListFragment {
}
// form validation
// input validation
@Override
public void onPrepareOptionsMenu(Menu menu) {
@ -103,65 +119,7 @@ public class SelectCollectionsFragment extends ListFragment {
ok = getListView().getCheckedItemCount() > 0;
} catch(IllegalStateException e) {
}
MenuItem item = menu.findItem(R.id.add_account);
MenuItem item = menu.findItem(R.id.next);
item.setEnabled(ok);
}
// actions
void addAccount() {
List<ServerInfo.ResourceInfo> addressBooks = new LinkedList<ServerInfo.ResourceInfo>(),
calendars = new LinkedList<ServerInfo.ResourceInfo>();
ListAdapter adapter = getListView().getAdapter();
for (long id : getListView().getCheckedItemIds()) {
int position = (int)id + 1; // +1 because header view is inserted at pos. 0
ServerInfo.ResourceInfo info = (ServerInfo.ResourceInfo)adapter.getItem(position);
switch (info.getType()) {
case ADDRESS_BOOK:
addressBooks.add(info);
break;
case CALENDAR:
calendars.add(info);
}
}
ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO);
try {
URI baseURI = new URI(serverInfo.getBaseURL());
String accountName = serverInfo.getUserName() + "@" + baseURI.getHost() + baseURI.getPath();
AccountManager accountManager = AccountManager.get(getActivity());
Account account = new Account(accountName, Constants.ACCOUNT_TYPE);
Bundle userData = new Bundle();
userData.putString(Constants.ACCOUNT_KEY_BASE_URL, serverInfo.getBaseURL());
userData.putString(Constants.ACCOUNT_KEY_USERNAME, serverInfo.getUserName());
userData.putString(Constants.ACCOUNT_KEY_AUTH_PREEMPTIVE, Boolean.toString(serverInfo.isAuthPreemptive()));
if (!addressBooks.isEmpty()) {
userData.putString(Constants.ACCOUNT_KEY_ADDRESSBOOK_PATH, addressBooks.get(0).getPath());
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
} else
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0);
if (accountManager.addAccountExplicitly(account, serverInfo.getPassword(), userData)) {
// account created, now create calendars
if (!calendars.isEmpty()) {
for (ServerInfo.ResourceInfo calendar : calendars)
LocalCalendar.create(account, getActivity().getContentResolver(), calendar);
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, true);
} else
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0);
getActivity().finish();
} else
Toast.makeText(getActivity(), "Couldn't create account (already existing?)", Toast.LENGTH_LONG).show();
} catch (URISyntaxException e) {
} catch (RemoteException e) {
}
}
}

View File

@ -8,6 +8,7 @@
package at.bitfire.davdroid.syncadapter;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import lombok.Data;
@ -25,7 +26,17 @@ public class ServerInfo implements Serializable {
private String errorMessage;
private boolean calDAV, cardDAV;
private List<ResourceInfo> addressBooks, calendars;
private List<ResourceInfo>
addressBooks = new LinkedList<ResourceInfo>(),
calendars = new LinkedList<ResourceInfo>();
public boolean hasEnabledCalendars() {
for (ResourceInfo calendar : calendars)
if (calendar.enabled)
return true;
return false;
}
@RequiredArgsConstructor(suppressConstructorProperties=true)
@Data
@ -37,6 +48,8 @@ public class ServerInfo implements Serializable {
CALENDAR
}
boolean enabled = false;
final Type type;
final String path, title, description, color;

View File

@ -10,7 +10,6 @@ package at.bitfire.davdroid.test;
import java.io.IOException;
import java.io.InputStream;
import junit.framework.Assert;
import net.fortuna.ical4j.data.ParserException;
import android.content.res.AssetManager;
import android.test.InstrumentationTestCase;
@ -26,10 +25,11 @@ public class CalendarTest extends InstrumentationTestCase {
public void testTimeZonesByEvolution() throws IOException, ParserException {
Event e = parseCalendar("vienna-evolution.ics");
Assert.assertEquals("Test-Ereignis im schönen Wien", e.getSummary());
assertEquals("Test-Ereignis im schönen Wien", e.getSummary());
//DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T170000
Assert.assertEquals(1381327200, e.getDtStartInMillis());
/*assertEquals(1381330800000L, e.getDtStartInMillis());
assertEquals(1381334400000L, (long)e.getDtEndInMillis());*/
}