diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ebcc45bb..b0600fb8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -184,6 +184,7 @@
android:parentActivityName=".ui.AccountsActivity">
+
diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.java
index e0d8de84..d436befe 100644
--- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.java
+++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.java
@@ -181,6 +181,11 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
return (LocalContact[])queryContacts(RawContacts.DIRTY + "!= 0 AND " + RawContacts.DELETED + "== 0", null);
}
+ @NonNull
+ public LocalContact[] getAll() throws ContactsStorageException {
+ return (LocalContact[])queryContacts(RawContacts.DELETED + "== 0", null);
+ }
+
@NonNull
public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
return (LocalGroup[])queryGroups(Groups.DELETED + "!= 0", null);
diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.java b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.java
index 11c11841..79e4443a 100644
--- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.java
+++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.java
@@ -130,6 +130,11 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
return (LocalEvent[])queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null);
}
+
+ public LocalEvent[] getAll() throws CalendarStorageException {
+ return (LocalEvent[])queryEvents(null, null);
+ }
+
@Override
public LocalEvent getByUid(String uid) throws CalendarStorageException {
LocalEvent[] ret = (LocalEvent[]) queryEvents(Events._SYNC_ID + " =? ", new String[]{uid});
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ImportFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/ImportFragment.java
index 72042f40..f0e3a9a3 100644
--- a/app/src/main/java/com/etesync/syncadapter/ui/ImportFragment.java
+++ b/app/src/main/java/com/etesync/syncadapter/ui/ImportFragment.java
@@ -8,7 +8,6 @@ import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.ContentProviderClient;
-import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@@ -17,10 +16,7 @@ import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
-import android.support.v7.app.AlertDialog;
-import android.widget.Toast;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.R;
@@ -29,6 +25,7 @@ import com.etesync.syncadapter.resource.LocalAddressBook;
import com.etesync.syncadapter.resource.LocalCalendar;
import com.etesync.syncadapter.resource.LocalContact;
import com.etesync.syncadapter.resource.LocalEvent;
+import com.etesync.syncadapter.ui.importlocal.ResultFragment;
import org.apache.commons.codec.Charsets;
@@ -36,7 +33,6 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
-import java.io.Serializable;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.Event;
@@ -44,10 +40,10 @@ import at.bitfire.ical4android.InvalidCalendarException;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
-import lombok.ToString;
import static com.etesync.syncadapter.Constants.KEY_ACCOUNT;
import static com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO;
+import static com.etesync.syncadapter.ui.importlocal.ResultFragment.*;
public class ImportFragment extends DialogFragment {
private static final int REQUEST_CODE = 6384; // onActivityResult request
@@ -85,9 +81,7 @@ public class ImportFragment extends DialogFragment {
} else {
ImportResult data = new ImportResult();
data.e = new Exception(getString(R.string.import_permission_required));
- getFragmentManager().beginTransaction()
- .add(ResultFragment.newInstance(data), null)
- .commitAllowingStateLoss();
+ ((ResultFragment.OnImportCallback) getActivity()).onImportResult(data);
dismissAllowingStateLoss();
}
@@ -166,9 +160,8 @@ public class ImportFragment extends DialogFragment {
} catch (ActivityNotFoundException e) {
ImportResult data = new ImportResult();
data.e = new Exception("Failed to open file chooser.\nPlease install one.");
- getFragmentManager().beginTransaction()
- .add(ResultFragment.newInstance(data), null)
- .commitAllowingStateLoss();
+
+ ((ResultFragment.OnImportCallback) getActivity()).onImportResult(data);
dismissAllowingStateLoss();
}
@@ -200,9 +193,7 @@ public class ImportFragment extends DialogFragment {
}
public void loadFinished(ImportResult data) {
- getFragmentManager().beginTransaction()
- .add(ResultFragment.newInstance(data), null)
- .commitAllowingStateLoss();
+ ((ResultFragment.OnImportCallback) getActivity()).onImportResult(data);
dismissAllowingStateLoss();
@@ -347,68 +338,4 @@ public class ImportFragment extends DialogFragment {
}
}
}
-
- @ToString
- static class ImportResult implements Serializable {
- long total;
- long added;
- long updated;
- Exception e;
-
- boolean isFailed() {
- return (e != null);
- }
-
- long getSkipped() {
- return total - (added + updated);
- }
- }
-
- public static class ResultFragment extends DialogFragment {
- private static final String KEY_RESULT = "result";
- private ImportResult result;
-
- private static ResultFragment newInstance(ImportResult result) {
- Bundle args = new Bundle();
- args.putSerializable(KEY_RESULT, result);
- ResultFragment fragment = new ResultFragment();
- fragment.setArguments(args);
- return fragment;
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- result = (ImportResult) getArguments().getSerializable(KEY_RESULT);
- }
-
- @Override
- @NonNull
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- int icon;
- int title;
- String msg;
- if (result.isFailed()) {
- icon = R.drawable.ic_error_dark;
- title = R.string.import_dialog_failed_title;
- msg = result.e.getLocalizedMessage();
- } else {
- icon = R.drawable.ic_import_export_black;
- title = R.string.import_dialog_title;
- msg = getString(R.string.import_dialog_success, result.total, result.added, result.updated, result.getSkipped());
- }
- return new AlertDialog.Builder(getActivity())
- .setTitle(title)
- .setIcon(icon)
- .setMessage(msg)
- .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- // dismiss
- }
- })
- .create();
- }
- }
}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java
index 77e952ed..bd7621b5 100644
--- a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java
+++ b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java
@@ -27,6 +27,7 @@ import com.etesync.syncadapter.model.EntryEntity;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.resource.LocalAddressBook;
import com.etesync.syncadapter.resource.LocalCalendar;
+import com.etesync.syncadapter.ui.importlocal.ImportActivity;
import com.etesync.syncadapter.ui.journalviewer.ListEntriesFragment;
import java.io.FileNotFoundException;
@@ -167,8 +168,6 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh
}
public void onImport(MenuItem item) {
- getSupportFragmentManager().beginTransaction()
- .add(ImportFragment.newInstance(account, info), null)
- .commit();
+ startActivity(ImportActivity.newIntent(ViewCollectionActivity.this, account, info));
}
}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.java
new file mode 100644
index 00000000..ae99c494
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/CalendarAccount.java
@@ -0,0 +1,120 @@
+package com.etesync.syncadapter.ui.importlocal;
+
+import android.accounts.Account;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.CalendarContract;
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Events;
+import android.util.Log;
+
+import com.etesync.syncadapter.App;
+import com.etesync.syncadapter.resource.LocalCalendar;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by tal on 27/03/17.
+ */
+
+public class CalendarAccount {
+ public String accountName;
+ public List calendars = new ArrayList<>();
+
+ private static final String[] CAL_COLS = new String[] {
+ Calendars._ID, Calendars.DELETED, Calendars.NAME, Calendars.CALENDAR_DISPLAY_NAME,
+ Calendars.ACCOUNT_NAME, Calendars.ACCOUNT_TYPE, Calendars.OWNER_ACCOUNT,
+ Calendars.VISIBLE, Calendars.CALENDAR_TIME_ZONE };
+
+ private static final String[] CAL_ID_COLS = new String[] { Events._ID };
+ private static final String CAL_ID_WHERE = Events.CALENDAR_ID + "=?";
+
+ protected CalendarAccount(String accountName) {
+ this.accountName = accountName;
+ }
+
+ // Load all available calendars.
+ // If an empty list is returned the caller probably needs to enable calendar
+ // read permissions in App Ops/XPrivacy etc.
+ public static List loadAll(ContentResolver resolver) {
+
+ if (missing(resolver, Calendars.CONTENT_URI) || missing(resolver, Events.CONTENT_URI))
+ return new ArrayList<>();
+
+ Cursor cur;
+ try {
+ cur = resolver.query(Calendars.CONTENT_URI,
+ CAL_COLS, null, null, Calendars.ACCOUNT_NAME + " ASC");
+ } catch (Exception except) {
+ App.log.warning("Calendar provider is missing columns, continuing anyway");
+ cur = resolver.query(Calendars.CONTENT_URI, null, null, null, null);
+ except.printStackTrace();
+ }
+ List calendarAccounts = new ArrayList<>(cur.getCount());
+
+ CalendarAccount calendarAccount = null;
+ ContentProviderClient contentProviderClient = resolver.acquireContentProviderClient(CalendarContract.CONTENT_URI);
+ while (cur.moveToNext()) {
+ if (getLong(cur, Calendars.DELETED) != 0)
+ continue;
+
+ String accountName = getString(cur, Calendars.ACCOUNT_NAME);
+ if (calendarAccount == null || !calendarAccount.accountName.equals(accountName)) {
+ calendarAccount = new CalendarAccount(accountName);
+ calendarAccounts.add(calendarAccount);
+ }
+
+ long id = getLong(cur, Calendars._ID);
+ if (id == -1) {
+ continue;
+ }
+
+ final String[] args = new String[] { String.valueOf(id) };
+ Cursor eventsCur = resolver.query(Events.CONTENT_URI, CAL_ID_COLS, CAL_ID_WHERE, args, null);
+ Account account = new Account(accountName, getString(cur, Calendars.ACCOUNT_TYPE));
+
+ try {
+ LocalCalendar localCalendar = LocalCalendar.findByName(account, contentProviderClient,
+ LocalCalendar.Factory.INSTANCE, getString(cur, Calendars.NAME));
+ if (localCalendar != null) calendarAccount.calendars.add(localCalendar);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+
+ eventsCur.close();
+ }
+ contentProviderClient.release();
+ cur.close();
+ return calendarAccounts;
+ }
+
+ private static int getColumnIndex(Cursor cur, String dbName) {
+ return dbName == null ? -1 : cur.getColumnIndex(dbName);
+ }
+
+ private static long getLong(Cursor cur, String dbName) {
+ int i = getColumnIndex(cur, dbName);
+ return i == -1 ? -1 : cur.getLong(i);
+ }
+
+ private static String getString(Cursor cur, String dbName) {
+ int i = getColumnIndex(cur, dbName);
+ return i == -1 ? null : cur.getString(i);
+ }
+
+ private static boolean missing(ContentResolver resolver, Uri uri) {
+ // Determine if a provider is missing
+ ContentProviderClient provider = resolver.acquireContentProviderClient(uri);
+ if (provider != null)
+ provider.release();
+ return provider == null;
+ }
+
+ @Override
+ public String toString() {
+ return accountName;
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.java
new file mode 100644
index 00000000..fd39382a
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.java
@@ -0,0 +1,192 @@
+package com.etesync.syncadapter.ui.importlocal;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.AppCompatActivity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.etesync.syncadapter.App;
+import com.etesync.syncadapter.R;
+import com.etesync.syncadapter.model.CollectionInfo;
+import com.etesync.syncadapter.ui.ImportFragment;
+
+public class ImportActivity extends AppCompatActivity implements SelectImportMethod, ResultFragment.OnImportCallback, DialogInterface {
+ public final static String EXTRA_ACCOUNT = "account",
+ EXTRA_COLLECTION_INFO = "collectionInfo";
+
+ private Account account;
+ protected CollectionInfo info;
+
+ public static Intent newIntent(Context context, Account account, CollectionInfo info) {
+ Intent intent = new Intent(context, ImportActivity.class);
+ intent.putExtra(ImportActivity.EXTRA_ACCOUNT, account);
+ intent.putExtra(ImportActivity.EXTRA_COLLECTION_INFO, info);
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_import);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ setTitle(getString(R.string.import_dialog_title));
+
+ account = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT);
+ info = (CollectionInfo) getIntent().getExtras().getSerializable(EXTRA_COLLECTION_INFO);
+
+ if (savedInstanceState == null)
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.fragment_container, new ImportActivity.SelectImportFragment())
+ .commit();
+ }
+
+ @Override
+ public void importFile() {
+ getSupportFragmentManager().beginTransaction()
+ .add(ImportFragment.newInstance(account, info), null)
+ .commit();
+
+ }
+
+ @Override
+ public void importAccount() {
+ if (info.type == CollectionInfo.Type.CALENDAR) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container,
+ LocalCalendarImportFragment.newInstance(account, info))
+ .addToBackStack(LocalCalendarImportFragment.class.getName())
+ .commit();
+ } else if (info.type == CollectionInfo.Type.ADDRESS_BOOK) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container,
+ LocalContactImportFragment.newInstance(account, info))
+ .addToBackStack(LocalContactImportFragment.class.getName())
+ .commit();
+ }
+ setTitle(getString(R.string.import_select_account));
+ }
+
+ private void popBackStack() {
+ if (!getSupportFragmentManager().popBackStackImmediate()) {
+ finish();
+ } else {
+ setTitle(getString(R.string.import_dialog_title));
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ popBackStack();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ popBackStack();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ App app = (App) getApplicationContext();
+ if (app.getCertManager() != null)
+ app.getCertManager().appInForeground = true;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ App app = (App) getApplicationContext();
+ if (app.getCertManager() != null)
+ app.getCertManager().appInForeground = false;
+ }
+
+ @Override
+ public void onImportResult(ResultFragment.ImportResult importResult) {
+ ResultFragment fragment = ResultFragment.newInstance(importResult);
+ fragment.show(getSupportFragmentManager(), null);
+ }
+
+ @Override
+ public void cancel() {
+ finish();
+ }
+
+ @Override
+ public void dismiss() {
+ finish();
+ }
+
+ public static class SelectImportFragment extends Fragment {
+
+ private SelectImportMethod mSelectImportMethod;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ // This makes sure that the container activity has implemented
+ // the callback interface. If not, it throws an exception
+ try {
+ mSelectImportMethod = (SelectImportMethod) getActivity();
+ } catch (ClassCastException e) {
+ throw new ClassCastException(getActivity().toString()
+ + " must implement MyInterface ");
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ // This makes sure that the container activity has implemented
+ // the callback interface. If not, it throws an exception
+ try {
+ mSelectImportMethod = (SelectImportMethod) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString()
+ + " must implement MyInterface ");
+ }
+ }
+
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_import, container, false);
+ v.findViewById(R.id.import_button_account).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View aView) {
+ mSelectImportMethod.importAccount();
+ }
+ });
+
+ v.findViewById(R.id.import_button_file).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View aView) {
+ mSelectImportMethod.importFile();
+ }
+ });
+ return v;
+ }
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.java
new file mode 100644
index 00000000..bf3dc046
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.java
@@ -0,0 +1,263 @@
+package com.etesync.syncadapter.ui.importlocal;
+
+import android.accounts.Account;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CalendarContract;
+import android.support.v4.app.ListFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.TextView;
+
+import com.etesync.syncadapter.R;
+import com.etesync.syncadapter.model.CollectionInfo;
+import com.etesync.syncadapter.resource.LocalCalendar;
+import com.etesync.syncadapter.resource.LocalEvent;
+
+import java.util.List;
+
+import at.bitfire.ical4android.CalendarStorageException;
+import at.bitfire.ical4android.Event;
+
+import static com.etesync.syncadapter.Constants.KEY_ACCOUNT;
+import static com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO;
+
+public class LocalCalendarImportFragment extends ListFragment {
+
+ private Account account;
+ private CollectionInfo info;
+
+ public static LocalCalendarImportFragment newInstance(Account account, CollectionInfo info) {
+ LocalCalendarImportFragment frag = new LocalCalendarImportFragment();
+ Bundle args = new Bundle(1);
+ args.putParcelable(KEY_ACCOUNT, account);
+ args.putSerializable(KEY_COLLECTION_INFO, info);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+
+ account = getArguments().getParcelable(KEY_ACCOUNT);
+ info = (CollectionInfo) getArguments().getSerializable(KEY_COLLECTION_INFO);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_local_calendar_import, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ importAccount();
+ }
+
+ protected void importAccount() {
+ final List calendarAccountList = CalendarAccount.loadAll(getContext().getContentResolver());
+
+ ExpandableListView listCalendar = (ExpandableListView) getListView();
+
+ final LocalCalendarImportFragment.ExpandableListAdapter adapter =
+ new LocalCalendarImportFragment.ExpandableListAdapter(getContext(), calendarAccountList);
+ listCalendar.setAdapter(adapter);
+
+ listCalendar.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
+ @Override
+ public boolean onChildClick(ExpandableListView aExpandableListView, View aView, int groupPosition, int childPosition, long aL) {
+ new ImportEvents().execute(calendarAccountList.get(groupPosition).calendars.get(childPosition));
+ return false;
+ }
+ });
+ }
+
+
+ private class ExpandableListAdapter extends BaseExpandableListAdapter {
+
+ private Context context;
+ private List calendarAccounts;
+
+ public ExpandableListAdapter(Context context, List calendarAccounts) {
+ this.context = context;
+ this.calendarAccounts = calendarAccounts;
+ }
+
+ private class ChildViewHolder {
+ TextView textView;
+ }
+
+ private class GroupViewHolder {
+ TextView titleTextView;
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosititon) {
+ return calendarAccounts.get(groupPosition).calendars
+ .get(childPosititon).getDisplayName();
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return childPosition;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+ final String childText = (String) getChild(groupPosition, childPosition);
+ ChildViewHolder viewHolder;
+ if (convertView == null) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.import_calendars_list_item, null);
+ }
+
+ if (convertView.getTag() != null) {
+ viewHolder = (ChildViewHolder) convertView.getTag();
+ } else {
+ viewHolder = new ChildViewHolder();
+ viewHolder.textView = (TextView) convertView
+ .findViewById(R.id.listItemText);
+ convertView.setTag(viewHolder);
+ }
+ viewHolder.textView.setText(childText);
+ return convertView;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ return calendarAccounts.get(groupPosition).calendars
+ .size();
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return calendarAccounts.get(groupPosition).toString();
+ }
+
+ @Override
+ public int getGroupCount() {
+ return calendarAccounts.size();
+ }
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return groupPosition;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+ String headerTitle = (String) getGroup(groupPosition);
+ GroupViewHolder viewHolder;
+ if (convertView == null) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.import_calendars_list_group, null);
+ }
+ if (convertView.getTag() != null) {
+ viewHolder = (GroupViewHolder) convertView.getTag();
+ } else {
+ viewHolder = new GroupViewHolder();
+ viewHolder.titleTextView = (TextView) convertView
+ .findViewById(R.id.title);
+ convertView.setTag(viewHolder);
+ }
+ viewHolder.titleTextView.setText(headerTitle);
+
+ return convertView;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+ }
+
+ protected class ImportEvents extends AsyncTask {
+ ProgressDialog progressDialog;
+
+ @Override
+ protected void onPreExecute() {
+ progressDialog = new ProgressDialog(getActivity());
+ progressDialog.setTitle(R.string.import_dialog_title);
+ progressDialog.setMessage(getString(R.string.import_dialog_adding_entries));
+ progressDialog.setCanceledOnTouchOutside(false);
+ progressDialog.setCancelable(false);
+ progressDialog.setIndeterminate(false);
+ progressDialog.setIcon(R.drawable.ic_import_export_black);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ progressDialog.show();
+ }
+
+ @Override
+ protected ResultFragment.ImportResult doInBackground(LocalCalendar... calendars) {
+ return importEvents(calendars[0]);
+ }
+
+ @Override
+ protected void onProgressUpdate(Integer... progress) {
+ if (progressDialog != null)
+ progressDialog.setProgress(progress[0]);
+ }
+
+ @Override
+ protected void onPostExecute(ResultFragment.ImportResult result) {
+ progressDialog.dismiss();
+ ((ResultFragment.OnImportCallback) getActivity()).onImportResult(result);
+ }
+
+ private ResultFragment.ImportResult importEvents(LocalCalendar fromCalendar) {
+ ResultFragment.ImportResult result = new ResultFragment.ImportResult();
+ try {
+ LocalCalendar localCalendar = LocalCalendar.findByName(account,
+ getContext().getContentResolver().acquireContentProviderClient(CalendarContract.CONTENT_URI),
+ LocalCalendar.Factory.INSTANCE, info.url);
+ LocalEvent[] localEvents = fromCalendar.getAll();
+ int total = localEvents.length;
+ progressDialog.setMax(total);
+ result.total = total;
+ int progress = 0;
+ for (LocalEvent currentLocalEvent : localEvents) {
+ Event event = currentLocalEvent.getEvent();
+ try {
+ LocalEvent localEvent = event.uid == null ? null :
+ localCalendar.getByUid(event.uid);
+
+ if (localEvent != null) {
+ localEvent.updateAsDirty(event);
+ result.updated++;
+ } else {
+ localEvent = new LocalEvent(localCalendar, event, event.uid, null);
+ localEvent.addAsDirty();
+ result.added++;
+ }
+ } catch (CalendarStorageException e) {
+ e.printStackTrace();
+
+ }
+ publishProgress(++progress);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ result.e = e;
+ }
+ return result;
+ }
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.java
new file mode 100644
index 00000000..501bf885
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.java
@@ -0,0 +1,299 @@
+package com.etesync.syncadapter.ui.importlocal;
+
+import android.accounts.Account;
+import android.app.ProgressDialog;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.support.v4.app.Fragment;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.etesync.syncadapter.R;
+import com.etesync.syncadapter.model.CollectionInfo;
+import com.etesync.syncadapter.resource.LocalAddressBook;
+import com.etesync.syncadapter.resource.LocalContact;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import at.bitfire.vcard4android.Contact;
+import at.bitfire.vcard4android.ContactsStorageException;
+
+import static android.content.ContentValues.TAG;
+import static com.etesync.syncadapter.Constants.KEY_ACCOUNT;
+import static com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO;
+
+public class LocalContactImportFragment extends Fragment {
+
+ private Account account;
+ private CollectionInfo info;
+ private RecyclerView recyclerView;
+
+ public static LocalContactImportFragment newInstance(Account account, CollectionInfo info) {
+ LocalContactImportFragment frag = new LocalContactImportFragment();
+ Bundle args = new Bundle(1);
+ args.putParcelable(KEY_ACCOUNT, account);
+ args.putSerializable(KEY_COLLECTION_INFO, info);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+
+ account = getArguments().getParcelable(KEY_ACCOUNT);
+ info = (CollectionInfo) getArguments().getSerializable(KEY_COLLECTION_INFO);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_local_contact_import, container, false);
+
+ recyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
+
+ recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
+ recyclerView.addItemDecoration(new DividerItemDecoration(getActivity()));
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ importAccount();
+ }
+
+ protected void importAccount() {
+ ContentProviderClient provider = getContext().getContentResolver().acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI);
+ Cursor cursor;
+ try {
+ cursor = provider.query(ContactsContract.RawContacts.CONTENT_URI,
+ new String[]{ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE}
+ , null, null, ContactsContract.RawContacts.ACCOUNT_NAME + " ASC");
+ } catch (Exception except) {
+ Log.w(TAG, "Calendar provider is missing columns, continuing anyway");
+
+ except.printStackTrace();
+ return;
+ }
+
+ final List localAddressBooks = new ArrayList<>();
+ Account account = null;
+ int accountNameIndex = cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME);
+ int accountTypeIndex = cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_TYPE);
+ while (cursor.moveToNext()) {
+ String accountName = cursor.getString(accountNameIndex);
+ String accountType = cursor.getString(accountTypeIndex);
+ if (account == null || (!account.name.equals(accountName) || !account.type.equals(accountType))) {
+ account = new Account(accountName, accountType);
+ localAddressBooks.add(new LocalAddressBook(account, provider));
+ }
+ }
+
+ recyclerView.setAdapter(new ImportContactAdapter(localAddressBooks, new OnAccountSelected() {
+ @Override
+ public void accountSelected(int index) {
+ new ImportContacts().execute(localAddressBooks.get(index));
+ }
+ }));
+ }
+
+ protected class ImportContacts extends AsyncTask {
+ ProgressDialog progressDialog;
+
+ @Override
+ protected void onPreExecute() {
+ progressDialog = new ProgressDialog(getActivity());
+ progressDialog.setTitle(R.string.import_dialog_title);
+ progressDialog.setMessage(getString(R.string.import_dialog_adding_entries));
+ progressDialog.setCanceledOnTouchOutside(false);
+ progressDialog.setCancelable(false);
+ progressDialog.setIndeterminate(false);
+ progressDialog.setIcon(R.drawable.ic_import_export_black);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ progressDialog.show();
+ }
+
+ @Override
+ protected ResultFragment.ImportResult doInBackground(LocalAddressBook... addressBooks) {
+ return importContacts(addressBooks[0]);
+ }
+
+ @Override
+ protected void onProgressUpdate(Integer... progress) {
+ if (progressDialog != null)
+ progressDialog.setProgress(progress[0]);
+ }
+
+ @Override
+ protected void onPostExecute(ResultFragment.ImportResult result) {
+ progressDialog.dismiss();
+ ((ResultFragment.OnImportCallback) getActivity()).onImportResult(result);
+ }
+
+ private ResultFragment.ImportResult importContacts(LocalAddressBook localAddressBook) {
+ ResultFragment.ImportResult result = new ResultFragment.ImportResult();
+ try {
+ LocalAddressBook addressBook = new LocalAddressBook(account,
+ getContext().getContentResolver().acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI));
+ LocalContact[] localContacts = localAddressBook.getAll();
+ int total = localContacts.length;
+ progressDialog.setMax(total);
+ result.total = total;
+ int progress = 0;
+ for (LocalContact currentLocalContact : localContacts) {
+ Contact contact = currentLocalContact.getContact();
+ (new LocalContact(addressBook, contact, contact.uid, null)).createAsDirty();
+
+ try {
+ LocalContact localContact = contact.uid == null ?
+ null : (LocalContact) addressBook.getByUid(contact.uid);
+ if (localContact != null) {
+ localContact.updateAsDirty(contact);
+ result.updated++;
+ } else {
+ localContact = new LocalContact(addressBook, contact, contact.uid, null);
+ localContact.createAsDirty();
+ result.added++;
+ }
+ } catch (ContactsStorageException e) {
+ e.printStackTrace();
+ result.e = e;
+ }
+ publishProgress(++progress);
+ }
+ } catch (Exception e) {
+ result.e = e;
+ }
+ return result;
+ }
+ }
+
+ public static class ImportContactAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "ImportContactAdapter";
+
+ private List mAddressBooks;
+ private OnAccountSelected mOnAccountSelected;
+
+ /**
+ * Provide a reference to the type of views that you are using (custom ViewHolder)
+ */
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ private final TextView titleTextView;
+ private final TextView descTextView;
+
+ public ViewHolder(View v, final OnAccountSelected onAccountSelected) {
+ super(v);
+ // Define click listener for the ViewHolder's View.
+ v.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onAccountSelected.accountSelected(getAdapterPosition());
+ }
+ });
+ titleTextView = (TextView) v.findViewById(R.id.title);
+ descTextView = (TextView) v.findViewById(R.id.description);
+ }
+
+ public TextView getTitleTextView() {
+ return titleTextView;
+ }
+
+ public TextView getDescriptionTextView() {
+ return descTextView;
+ }
+ }
+
+ /**
+ * Initialize the dataset of the Adapter.
+ *
+ * @param addressBooks containing the data to populate views to be used by RecyclerView.
+ */
+ public ImportContactAdapter(List addressBooks, OnAccountSelected onAccountSelected) {
+ mAddressBooks = addressBooks;
+ mOnAccountSelected = onAccountSelected;
+ }
+
+ // Create new views (invoked by the layout manager)
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
+ // Create a new view.
+ View v = LayoutInflater.from(viewGroup.getContext())
+ .inflate(R.layout.import_contacts_list_item, viewGroup, false);
+
+ return new ViewHolder(v, mOnAccountSelected);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, final int position) {
+ viewHolder.getTitleTextView().setText(mAddressBooks.get(position).account.name);
+ viewHolder.getDescriptionTextView().setText(mAddressBooks.get(position).account.type);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mAddressBooks.size();
+ }
+ }
+
+ private interface OnAccountSelected {
+ void accountSelected(int index);
+ }
+
+ public static class DividerItemDecoration extends RecyclerView.ItemDecoration {
+
+ private static final int[] ATTRS = new int[]{
+ android.R.attr.listDivider
+ };
+
+ private Drawable mDivider;
+
+ public DividerItemDecoration(Context context) {
+ final TypedArray a = context.obtainStyledAttributes(ATTRS);
+ mDivider = a.getDrawable(0);
+ a.recycle();
+ }
+
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ drawVertical(c, parent);
+ }
+
+ public void drawVertical(Canvas c, RecyclerView parent) {
+ final int left = parent.getPaddingLeft();
+ final int right = parent.getWidth() - parent.getPaddingRight();
+
+ final int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = parent.getChildAt(i);
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
+ .getLayoutParams();
+ final int top = child.getBottom() + params.bottomMargin;
+ final int bottom = top + mDivider.getIntrinsicHeight();
+ mDivider.setBounds(left, top, right, bottom);
+ mDivider.draw(c);
+ }
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
+ }
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.java
new file mode 100644
index 00000000..ef728ed8
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.java
@@ -0,0 +1,97 @@
+package com.etesync.syncadapter.ui.importlocal;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+
+import com.etesync.syncadapter.R;
+
+import java.io.Serializable;
+
+import lombok.ToString;
+
+/**
+ * Created by tal on 30/03/17.
+ */
+
+public class ResultFragment extends DialogFragment {
+ private static final String KEY_RESULT = "result";
+ private ImportResult result;
+
+ public static ResultFragment newInstance(ImportResult result) {
+ Bundle args = new Bundle();
+ args.putSerializable(KEY_RESULT, result);
+ ResultFragment fragment = new ResultFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ result = (ImportResult) getArguments().getSerializable(KEY_RESULT);
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ Activity activity = getActivity();
+ if (activity instanceof DialogInterface) {
+ ((DialogInterface)activity).dismiss();
+ }
+ }
+
+ @Override
+ @NonNull
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ int icon;
+ int title;
+ String msg;
+ if (result.isFailed()) {
+ icon = R.drawable.ic_error_dark;
+ title = R.string.import_dialog_failed_title;
+ msg = result.e.getLocalizedMessage();
+ } else {
+ icon = R.drawable.ic_import_export_black;
+ title = R.string.import_dialog_title;
+ msg = getString(R.string.import_dialog_success, result.total, result.added, result.updated, result.getSkipped());
+ }
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(title)
+ .setIcon(icon)
+ .setMessage(msg)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // dismiss
+ }
+ })
+ .create();
+ }
+
+ @ToString
+ public static class ImportResult implements Serializable {
+ public long total;
+ public long added;
+ public long updated;
+ public Exception e;
+
+ public boolean isFailed() {
+ return (e != null);
+ }
+
+ public long getSkipped() {
+ return total - (added + updated);
+ }
+ }
+
+ public interface OnImportCallback {
+ void onImportResult(ImportResult importResult);
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.java b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.java
new file mode 100644
index 00000000..01322a56
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/SelectImportMethod.java
@@ -0,0 +1,11 @@
+package com.etesync.syncadapter.ui.importlocal;
+
+/**
+ * Created by tal on 30/03/17.
+ */
+
+public interface SelectImportMethod {
+ void importFile();
+
+ void importAccount();
+}
diff --git a/app/src/main/res/layout/activity_import.xml b/app/src/main/res/layout/activity_import.xml
new file mode 100644
index 00000000..008f1127
--- /dev/null
+++ b/app/src/main/res/layout/activity_import.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/app/src/main/res/layout/fragment_import.xml b/app/src/main/res/layout/fragment_import.xml
new file mode 100644
index 00000000..bcd52965
--- /dev/null
+++ b/app/src/main/res/layout/fragment_import.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_local_calendar_import.xml b/app/src/main/res/layout/fragment_local_calendar_import.xml
new file mode 100644
index 00000000..04e195b4
--- /dev/null
+++ b/app/src/main/res/layout/fragment_local_calendar_import.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/fragment_local_contact_import.xml b/app/src/main/res/layout/fragment_local_contact_import.xml
new file mode 100644
index 00000000..b27bfb87
--- /dev/null
+++ b/app/src/main/res/layout/fragment_local_contact_import.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/layout/import_calendars_list_group.xml b/app/src/main/res/layout/import_calendars_list_group.xml
new file mode 100644
index 00000000..1156eb47
--- /dev/null
+++ b/app/src/main/res/layout/import_calendars_list_group.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/import_calendars_list_item.xml b/app/src/main/res/layout/import_calendars_list_item.xml
new file mode 100644
index 00000000..674dd284
--- /dev/null
+++ b/app/src/main/res/layout/import_calendars_list_item.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/import_contacts_list_item.xml b/app/src/main/res/layout/import_contacts_list_item.xml
new file mode 100644
index 00000000..82b598c6
--- /dev/null
+++ b/app/src/main/res/layout/import_contacts_list_item.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ 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 614a7bfd..1fb5eb68 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -248,4 +248,10 @@
Loading...
Loading Error
Refresh
+
+
+ Import from file
+ Import from local
+ Select Account
+