diff --git a/app/build.gradle b/app/build.gradle index 0cc1ae6b..42adb17a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0203ef80..aed7d5b3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -131,6 +131,8 @@ + > 24); + int color = colorWithAlpha & 0xFFFFFF; + return String.format("#%06X%02X", color, alpha); + } + } diff --git a/app/src/main/java/at/bitfire/davdroid/model/DavService.java b/app/src/main/java/at/bitfire/davdroid/model/DavService.java index 26c94462..1fcf7c40 100644 --- a/app/src/main/java/at/bitfire/davdroid/model/DavService.java +++ b/app/src/main/java/at/bitfire/davdroid/model/DavService.java @@ -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; } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java index 1c7cd06f..839037a6 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java @@ -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); diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java index 2d2c9505..982465e3 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java @@ -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); diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java index 6c44bfee..1e3106e7 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java @@ -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(), diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java index 2bef01f4..395fbabb 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java @@ -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) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/AccountActivity.java b/app/src/main/java/at/bitfire/davdroid/ui/AccountActivity.java index c576cb31..a700e565 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/AccountActivity.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/AccountActivity.java @@ -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 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; } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/CreateAddressBookActivity.java b/app/src/main/java/at/bitfire/davdroid/ui/CreateAddressBookActivity.java index 9aa4d285..888bc7f7 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/CreateAddressBookActivity.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/CreateAddressBookActivity.java @@ -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 homeSet = (HashMap)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 { + 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 { @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 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 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 loader) { + } + + protected static class AddressBookCreator extends AsyncTaskLoader { + 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 { 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); } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/CreateCalendarActivity.java b/app/src/main/java/at/bitfire/davdroid/ui/CreateCalendarActivity.java new file mode 100644 index 00000000..f2983b2c --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/CreateCalendarActivity.java @@ -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 { + 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 onCreateLoader(int id, Bundle args) { + return new AccountInfoLoader(this, account); + } + + @Override + public void onLoadFinished(Loader 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 loader) { + } + + protected static class AccountInfo { + List homeSets = new LinkedList<>(); + } + + protected static class AccountInfoLoader extends AsyncTaskLoader { + 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; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/ui/CreateCollectionFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/CreateCollectionFragment.java new file mode 100644 index 00000000..58e50e62 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/CreateCollectionFragment.java @@ -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 { + @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(); + } + } + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/ui/DeleteCollectionFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/DeleteCollectionFragment.java index b9d152f9..82ab838c 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/DeleteCollectionFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/DeleteCollectionFragment.java @@ -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 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(); + } + } + } diff --git a/app/src/main/res/drawable/ic_menu_light.xml b/app/src/main/res/drawable/ic_menu_light.xml new file mode 100644 index 00000000..811eef7c --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_light.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/layout/activity_create_address_book.xml b/app/src/main/res/layout/activity_create_address_book.xml index b5c774f3..bb95d179 100644 --- a/app/src/main/res/layout/activity_create_address_book.xml +++ b/app/src/main/res/layout/activity_create_address_book.xml @@ -22,4 +22,22 @@ android:layout_width="match_parent" android:layout_height="wrap_content"/> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_create_calendar.xml b/app/src/main/res/layout/activity_create_calendar.xml new file mode 100644 index 00000000..a695bdec --- /dev/null +++ b/app/src/main/res/layout/activity_create_calendar.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_create_calendar.xml b/app/src/main/res/menu/activity_create_calendar.xml new file mode 100644 index 00000000..4de60523 --- /dev/null +++ b/app/src/main/res/menu/activity_create_calendar.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5daee6b1..19ed8a6e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ DAVdroid Help Manage accounts - Please wait + Please wait … Send Skip @@ -170,8 +170,14 @@ Once a day - + Create address book + Creating address book + Create calendar + Creating collection + Are you sure? + This collection (%s) and all its entries from the server will be removed from the server. + Deleting collection Android version update Android version updates may have an impact on how DAVdroid works. If there are problems, please delete your DAVdroid accounts and add them again. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 1acc47e6..a80afb1a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -92,10 +92,10 @@ - + \ No newline at end of file diff --git a/dav4android b/dav4android index 0e9522bb..fd19c653 160000 --- a/dav4android +++ b/dav4android @@ -1 +1 @@ -Subproject commit 0e9522bb3a2d4e33bac214883f8316b91be0960e +Subproject commit fd19c6531ad5c1cbe210a7e70f5781cbfd5744a7