diff --git a/astrid/.classpath b/astrid/.classpath index cc10d291f..a20a47fca 100644 --- a/astrid/.classpath +++ b/astrid/.classpath @@ -30,5 +30,6 @@ + diff --git a/astrid/libs/CWAC-SackOfViewsAdapter.jar b/astrid/libs/CWAC-SackOfViewsAdapter.jar new file mode 100644 index 000000000..0b43a064f Binary files /dev/null and b/astrid/libs/CWAC-SackOfViewsAdapter.jar differ diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java b/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java index 43b88da6b..5d081320c 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java @@ -40,6 +40,7 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; +import com.commonsware.cwac.merge.MergeAdapter; import com.timsu.astrid.R; import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.service.Autowired; @@ -114,8 +115,6 @@ public class EditPeopleControlSet extends PopupControlSet { private final View assignedClear; - private final ArrayList listValues = new ArrayList(); - private final int loginRequestCode; private boolean assignedToMe = false; @@ -351,16 +350,17 @@ public class EditPeopleControlSet extends PopupControlSet { HashSet emails = new HashSet(); HashMap names = new HashMap(); + ArrayList coreUsers = new ArrayList(); + ArrayList listUsers = new ArrayList(); + ArrayList astridUsers = new ArrayList(); + int assignedIndex = 0; try { - if(t.getValue(Task.USER_ID) > 0) { - JSONObject user = new JSONObject(t.getValue(Task.USER)); - sharedPeople.add(0, user); - } - + ArrayList coreUsersJson = new ArrayList(); JSONObject myself = new JSONObject(); myself.put("id", Task.USER_ID_SELF); - sharedPeople.add(0, myself); + myself.put("picture", ActFmPreferenceService.thisUser().optString("picture")); + coreUsersJson.add(myself); boolean hasTags = t.getTransitory("tags") != null && ((HashSet)t.getTransitory("tags")).size() > 0; @@ -368,106 +368,154 @@ public class EditPeopleControlSet extends PopupControlSet { if (addUnassigned) { JSONObject unassigned = new JSONObject(); unassigned.put("id", Task.USER_ID_UNASSIGNED); - sharedPeople.add(1, unassigned); + unassigned.put("default_picture", R.drawable.icn_anyone); + coreUsersJson.add(unassigned); } - addAstridFriends(sharedPeople); - - // de-duplicate by user id and/or email - listValues.clear(); - for(int i = 0; i < sharedPeople.size(); i++) { - JSONObject person = sharedPeople.get(i); - if(person == null) - continue; - long id = person.optLong("id", -2); - if(id == ActFmPreferenceService.userId() || (id >= -1 && userIds.contains(id))) - continue; - userIds.add(id); - - String email = person.optString("email"); - if(!TextUtils.isEmpty(email) && emails.contains(email)) - continue; - emails.add(email); - - String name = person.optString("name"); - if(id == 0) - name = activity.getString(R.string.actfm_EPA_assign_me); - if (id == -1) - name = activity.getString(R.string.actfm_EPA_unassigned); - - AssignedToUser atu = new AssignedToUser(name, person); - listValues.add(atu); - if(names.containsKey(name)) { - AssignedToUser user = names.get(name); - if(user != null && user.user.has("email")) { - user.label += " (" + user.user.optString("email") + ")"; - names.put(name, null); - } - if(!TextUtils.isEmpty("email")) - atu.label += " (" + email + ")"; - } else if(TextUtils.isEmpty(name)) { - if(!TextUtils.isEmpty("email")) - atu.label = email; - else - listValues.remove(atu); - } else - names.put(name, atu); + if(t.getValue(Task.USER_ID) > 0) { + JSONObject user = new JSONObject(t.getValue(Task.USER)); + coreUsersJson.add(0, user); } - String assignedStr = t.getValue(Task.USER); - if (!TextUtils.isEmpty(assignedStr)) { - JSONObject assigned = new JSONObject(assignedStr); - long assignedId = assigned.optLong("id", -2); - String assignedEmail = assigned.optString("email"); - for (int i = 0; i < listValues.size(); i++) { - JSONObject user = listValues.get(i).user; - if (user != null) { - if (user.optLong("id") == assignedId || - (user.optString("email").equals(assignedEmail) && - !(TextUtils.isEmpty(assignedEmail)))) - assignedIndex = i; - } - } - } + ArrayList astridFriends = getAstridFriends(); + + // de-duplicate by user id and/or email + coreUsers = convertJsonUsersToAssignedUsers(coreUsersJson, userIds, emails, names); + listUsers = convertJsonUsersToAssignedUsers(sharedPeople, userIds, emails, names); + astridUsers = convertJsonUsersToAssignedUsers(astridFriends, userIds, emails, names); contactPickerUser = new AssignedToUser(activity.getString(R.string.actfm_EPA_choose_contact), new JSONObject().put("default_picture", R.drawable.icn_friends) - .put(CONTACT_CHOOSER_USER, true)); + .put(CONTACT_CHOOSER_USER, true)); int contactsIndex = addUnassigned ? 2 : 1; - listValues.add(contactsIndex, contactPickerUser); - if (assignedIndex >= contactsIndex) - assignedIndex++; + coreUsers.add(contactsIndex, contactPickerUser); for (AssignedChangedListener l : listeners) { if (l.shouldShowTaskRabbit()) { taskRabbitUser = new AssignedToUser(activity.getString(R.string.actfm_EPA_task_rabbit), new JSONObject().put("default_picture", R.drawable.task_rabbit_image)); int taskRabbitIndex = addUnassigned ? 3 : 2; - listValues.add(taskRabbitIndex, taskRabbitUser); + coreUsers.add(taskRabbitIndex, taskRabbitUser); if(l.didPostToTaskRabbit()){ assignedIndex = taskRabbitIndex; - } else if (assignedIndex >= taskRabbitIndex) { - assignedIndex++; } } } + + if (assignedIndex == 0) { + assignedIndex = findAssignedIndex(t, coreUsers, listUsers, astridUsers); + } + } catch (JSONException e) { exceptionService.reportError("json-reading-data", e); } selected = assignedIndex; - final AssignedUserAdapter usersAdapter = new AssignedUserAdapter(activity, listValues); + final MergeAdapter mergeAdapter = new MergeAdapter(); + AssignedUserAdapter coreUserAdapter = new AssignedUserAdapter(activity, coreUsers, 0); + AssignedUserAdapter listUserAdapter = new AssignedUserAdapter(activity, listUsers, coreUserAdapter.getCount() + 1); + int offsetForAstridUsers = listUserAdapter.getCount() > 0 ? 2 : 1; + AssignedUserAdapter astridUserAdapter = new AssignedUserAdapter(activity, astridUsers, coreUserAdapter.getCount() + listUserAdapter.getCount() + offsetForAstridUsers); + + LayoutInflater inflater = activity.getLayoutInflater(); + TextView header1 = (TextView) inflater.inflate(R.layout.list_header, null); + header1.setText(R.string.actfm_EPA_assign_header_members); + TextView header2 = (TextView) inflater.inflate(R.layout.list_header, null); + header2.setText(R.string.actfm_EPA_assign_header_friends); + + mergeAdapter.addAdapter(coreUserAdapter); + if (listUserAdapter.getCount() > 0) { + mergeAdapter.addView(header1); + mergeAdapter.addAdapter(listUserAdapter); + } + if (astridUserAdapter.getCount() > 0) { + mergeAdapter.addView(header2); + mergeAdapter.addAdapter(astridUserAdapter); + } + activity.runOnUiThread(new Runnable() { @Override public void run() { - assignedList.setAdapter(usersAdapter); + assignedList.setAdapter(mergeAdapter); assignedList.setItemChecked(selected, true); refreshDisplayView(); } }); } - private void addAstridFriends(ArrayList sharedPeople) { + @SuppressWarnings("nls") + private ArrayList convertJsonUsersToAssignedUsers(ArrayList jsonUsers, + HashSet userIds, HashSet emails, HashMap names) { + ArrayList users = new ArrayList(); + for(int i = 0; i < jsonUsers.size(); i++) { + JSONObject person = jsonUsers.get(i); + if(person == null) + continue; + long id = person.optLong("id", -2); + if(id == ActFmPreferenceService.userId() || (id >= -1 && userIds.contains(id))) + continue; + userIds.add(id); + + String email = person.optString("email"); + if(!TextUtils.isEmpty(email) && emails.contains(email)) + continue; + emails.add(email); + + String name = person.optString("name"); + if(id == 0) + name = activity.getString(R.string.actfm_EPA_assign_me); + if (id == -1) + name = activity.getString(R.string.actfm_EPA_unassigned); + + AssignedToUser atu = new AssignedToUser(name, person); + users.add(atu); + if(names.containsKey(name)) { + AssignedToUser user = names.get(name); + if(user != null && user.user.has("email")) { + user.label += " (" + user.user.optString("email") + ")"; + names.put(name, null); + } + if(!TextUtils.isEmpty("email")) + atu.label += " (" + email + ")"; + } else if(TextUtils.isEmpty(name)) { + if(!TextUtils.isEmpty("email")) + atu.label = email; + else + users.remove(atu); + } else + names.put(name, atu); + } + return users; + } + + @SuppressWarnings("nls") + private int findAssignedIndex(Task t, ArrayList... userLists) throws JSONException { + String assignedStr = t.getValue(Task.USER); + if (!TextUtils.isEmpty(assignedStr)) { + JSONObject assigned = new JSONObject(assignedStr); + long assignedId = assigned.optLong("id", -2); + String assignedEmail = assigned.optString("email"); + + int index = 0; + for (ArrayList userList : userLists) { + for (int i = 0; i < userList.size(); i++) { + JSONObject user = userList.get(i).user; + if (user != null) { + if (user.optLong("id") == assignedId || + (user.optString("email").equals(assignedEmail) && + !(TextUtils.isEmpty(assignedEmail)))) + return index; + } + index++; + } + index++; // Add one for headers separating user lists + } + } + return 0; + } + + private ArrayList getAstridFriends() { + ArrayList astridFriends = new ArrayList(); TodorooCursor users = userDao.query(Query.select(User.PROPERTIES).orderBy(Order.asc(User.NAME))); try { User user = new User(); @@ -476,7 +524,7 @@ public class EditPeopleControlSet extends PopupControlSet { JSONObject userJson = new JSONObject(); try { ActFmSyncService.JsonHelper.jsonFromUser(userJson, user); - sharedPeople.add(userJson); + astridFriends.add(userJson); } catch (JSONException e) { // Ignored } @@ -484,14 +532,18 @@ public class EditPeopleControlSet extends PopupControlSet { } finally { users.close(); } + return astridFriends; } private class AssignedUserAdapter extends ArrayAdapter { - public AssignedUserAdapter(Context context, ArrayList people) { + private final int positionOffset; + + public AssignedUserAdapter(Context context, ArrayList people, int positionOffset) { super(context, R.layout.assigned_adapter_row, people); + this.positionOffset = positionOffset; } @SuppressWarnings("nls") @@ -501,21 +553,14 @@ public class EditPeopleControlSet extends PopupControlSet { convertView = activity.getLayoutInflater().inflate(R.layout.assigned_adapter_row, parent, false); CheckedTextView ctv = (CheckedTextView) convertView.findViewById(android.R.id.text1); super.getView(position, ctv, parent); - if (assignedList.getCheckedItemPosition() == position) { + if (assignedList.getCheckedItemPosition() == position + positionOffset) { ctv.setChecked(true); } else { ctv.setChecked(false); } AsyncImageView image = (AsyncImageView) convertView.findViewById(R.id.person_image); image.setDefaultImageResource(R.drawable.icn_default_person_image); - if (position == 0) { - image.setUrl(ActFmPreferenceService.thisUser().optString("picture")); - } else if (position == 1) { - image.setUrl(""); - image.setDefaultImageResource(R.drawable.icn_anyone); - } else { - image.setUrl(getItem(position).user.optString("picture")); - } + image.setUrl(getItem(position).user.optString("picture")); if (getItem(position).user.optInt("default_picture", 0) > 0) { image.setDefaultImageResource(getItem(position).user.optInt("default_picture")); } diff --git a/astrid/res/layout/list_header.xml b/astrid/res/layout/list_header.xml new file mode 100644 index 000000000..10f6ab8ca --- /dev/null +++ b/astrid/res/layout/list_header.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/astrid/res/values/strings-actfm.xml b/astrid/res/values/strings-actfm.xml index c2256c781..0015d6ecd 100644 --- a/astrid/res/values/strings-actfm.xml +++ b/astrid/res/values/strings-actfm.xml @@ -169,6 +169,12 @@ Help me get this done! + + List Members + + + Astrid Friends + Create a shared tag? @@ -199,6 +205,7 @@ Log in Make private + diff --git a/astrid/res/values/styles.xml b/astrid/res/values/styles.xml index 5c99490fa..1881b52da 100644 --- a/astrid/res/values/styles.xml +++ b/astrid/res/values/styles.xml @@ -329,6 +329,13 @@ bold ?attr/asTextColor + + diff --git a/astrid/src/com/commonsware/cwac/merge/MergeAdapter.java b/astrid/src/com/commonsware/cwac/merge/MergeAdapter.java new file mode 100644 index 000000000..846299d17 --- /dev/null +++ b/astrid/src/com/commonsware/cwac/merge/MergeAdapter.java @@ -0,0 +1,379 @@ +/*** + Copyright (c) 2008-2009 CommonsWare, LLC + Portions (c) 2009 Google, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package com.commonsware.cwac.merge; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListAdapter; +import android.widget.SectionIndexer; +import java.util.ArrayList; +import java.util.List; +import com.commonsware.cwac.sacklist.SackOfViewsAdapter; + +/** + * Adapter that merges multiple child adapters and views + * into a single contiguous whole. + * + * Adapters used as pieces within MergeAdapter must + * have view type IDs monotonically increasing from 0. Ideally, + * adapters also have distinct ranges for their row ids, as + * returned by getItemId(). + * + */ +public class MergeAdapter extends BaseAdapter implements SectionIndexer { + protected ArrayList pieces=new ArrayList(); + + /** + * Stock constructor, simply chaining to the superclass. + */ + public MergeAdapter() { + super(); + } + + /** + * Adds a new adapter to the roster of things to appear + * in the aggregate list. + * @param adapter Source for row views for this section + */ + public void addAdapter(ListAdapter adapter) { + pieces.add(adapter); + adapter.registerDataSetObserver(new CascadeDataSetObserver()); + } + + /** + * Adds a new View to the roster of things to appear + * in the aggregate list. + * @param view Single view to add + */ + public void addView(View view) { + addView(view, false); + } + + /** + * Adds a new View to the roster of things to appear + * in the aggregate list. + * @param view Single view to add + * @param enabled false if views are disabled, true if enabled + */ + public void addView(View view, boolean enabled) { + ArrayList list=new ArrayList(1); + + list.add(view); + + addViews(list, enabled); + } + + /** + * Adds a list of views to the roster of things to appear + * in the aggregate list. + * @param views List of views to add + */ + public void addViews(List views) { + addViews(views, false); + } + + /** + * Adds a list of views to the roster of things to appear + * in the aggregate list. + * @param views List of views to add + * @param enabled false if views are disabled, true if enabled + */ + public void addViews(List views, boolean enabled) { + if (enabled) { + addAdapter(new EnabledSackAdapter(views)); + } + else { + addAdapter(new SackOfViewsAdapter(views)); + } + } + + /** + * Get the data item associated with the specified + * position in the data set. + * @param position Position of the item whose data we want + */ + @Override + public Object getItem(int position) { + for (ListAdapter piece : pieces) { + int size=piece.getCount(); + + if (position sections=new ArrayList(); + + for (ListAdapter piece : pieces) { + if (piece instanceof SectionIndexer) { + Object[] curSections=((SectionIndexer)piece).getSections(); + + if (curSections!=null) { + for (Object section : curSections) { + sections.add(section); + } + } + } + } + + if (sections.size()==0) { + return(null); + } + + return(sections.toArray(new Object[0])); + } + + private static class EnabledSackAdapter extends SackOfViewsAdapter { + public EnabledSackAdapter(List views) { + super(views); + } + + @Override + public boolean areAllItemsEnabled() { + return(true); + } + + @Override + public boolean isEnabled(int position) { + return(true); + } + } + + private class CascadeDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + notifyDataSetInvalidated(); + } + } +} \ No newline at end of file diff --git a/astrid/src/com/commonsware/cwac/merge/MergeSpinnerAdapter.java b/astrid/src/com/commonsware/cwac/merge/MergeSpinnerAdapter.java new file mode 100644 index 000000000..b1f059add --- /dev/null +++ b/astrid/src/com/commonsware/cwac/merge/MergeSpinnerAdapter.java @@ -0,0 +1,102 @@ +package com.commonsware.cwac.merge; + +import java.util.List; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListAdapter; +import android.widget.SpinnerAdapter; + +/** + * Adapter that merges multiple child adapters into a single + * contiguous whole to be consumed by a Spinner. + * + * Adapters used as pieces within MergeSpinnerAdapter must + * have view type IDs monotonically increasing from 0. + * Ideally, adapters also have distinct ranges for their row + * ids, as returned by getItemId(). + * + * All Adapters used as pieces within MergeSpinnerAdapter + * must be properly-configured implementations of + * SpinnerAdapter (e.g., ArrayAdapter, CursorAdapter). + */ +public class MergeSpinnerAdapter extends MergeAdapter { + /** + * Stock constructor, simply chaining to the superclass. + */ + public MergeSpinnerAdapter() { + super(); + } + + /* + * Returns the drop-down View for a given position, by + * iterating over the pieces. Assumes that all pieces are + * implementations of SpinnerAdapter. + * + * @see android.widget.BaseAdapter#getDropDownView(int, + * android.view.View, android.view.ViewGroup) + */ + public View getDropDownView(int position, View convertView, + ViewGroup parent) { + for (ListAdapter piece : pieces) { + int size=piece.getCount(); + + if (position views) { + throw new RuntimeException("Not supported with MergeSpinnerAdapter"); + } + + /** + * Adds a list of views to the roster of things to appear + * in the aggregate list. + * + * @param views + * List of views to add + * @param enabled + * false if views are disabled, true if enabled + */ + public void addViews(List views, boolean enabled) { + throw new RuntimeException("Not supported with MergeSpinnerAdapter"); + } +}