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