/* * Copyright (C) 2008 Esmertec AG. Copyright (C) 2008 The Android Open Source * Project * * 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 info.guardianproject.otr.app.im.app; import info.guardianproject.otr.app.im.provider.Imps; import java.util.ArrayList; import java.util.Locale; import java.util.Observable; import java.util.Observer; import info.guardianproject.otr.app.im.R; import info.guardianproject.otr.app.im.IImConnection; import android.app.Activity; import android.content.AsyncQueryHandler; import android.content.ContentQueryMap; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.res.Resources; import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.net.Uri; import android.os.RemoteException; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseExpandableListAdapter; import android.widget.CursorTreeAdapter; import android.widget.TextView; import android.widget.AbsListView.OnScrollListener; public class ContactListTreeAdapter extends BaseExpandableListAdapter implements AbsListView.OnScrollListener { private static final String[] CONTACT_LIST_PROJECTION = { Imps.ContactList._ID, Imps.ContactList.NAME, }; private static final int COLUMN_CONTACT_LIST_ID = 0; private static final int COLUMN_CONTACT_LIST_NAME = 1; Activity mActivity; SimpleAlertHandler mHandler; private LayoutInflater mInflate; private long mProviderId; long mAccountId; Cursor mSubscriptions; boolean mDataValid; ListTreeAdapter mAdapter; private boolean mHideOfflineContacts; final MyContentObserver mContentObserver; final MyDataSetObserver mDataSetObserver; private ArrayList<Integer> mExpandedGroups; private static final int TOKEN_CONTACT_LISTS = -1; // private static final int TOKEN_ONGOING_CONVERSATION = -2; private static final int TOKEN_SUBSCRIPTION = -3; // private static final String NON_CHAT_AND_BLOCKED_CONTACTS = "(" // + Imps.Contacts.LAST_MESSAGE_DATE // + " IS NULL) AND (" // + Imps.Contacts.TYPE + "!=" // + Imps.Contacts.TYPE_BLOCKED + ")"; private static final String BLOCKED_CONTACTS = "(" + Imps.Contacts.TYPE + "!=" + Imps.Contacts.TYPE_BLOCKED + ")"; private static final String CONTACTS_SELECTION = Imps.Contacts.CONTACTLIST + "=? AND " + BLOCKED_CONTACTS; private static final String ONLINE_CONTACT_SELECTION = CONTACTS_SELECTION + " AND " + Imps.Contacts.PRESENCE_STATUS + " != " + Imps.Presence.OFFLINE; static final void log(String msg) { Log.d(ImApp.LOG_TAG, "<ContactListAdapter>" + msg); } static final String[] CONTACT_COUNT_PROJECTION = { Imps.Contacts.CONTACTLIST, Imps.Contacts._COUNT, }; ContentQueryMap mOnlineContactsCountMap; // Async QueryHandler private final class QueryHandler extends AsyncQueryHandler { public QueryHandler(Context context) { super(context.getContentResolver()); } @Override protected void onQueryComplete(int token, Object cookie, Cursor c) { if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) { log("onQueryComplete:token=" + token); } if (token == TOKEN_CONTACT_LISTS) { mDataValid = true; mAdapter.setGroupCursor(c); } else if (token == TOKEN_SUBSCRIPTION) { setSubscriptions(c); notifyDataSetChanged(); } else { int count = mAdapter.getGroupCount(); for (int pos = 0; pos < count; pos++) { long listId = mAdapter.getGroupId(pos); if (listId == token) { mAdapter.setChildrenCursor(pos, c); break; } } } } } private QueryHandler mQueryHandler; private int mScrollState; private boolean mAutoRequery; private boolean mRequeryPending; public ContactListTreeAdapter(IImConnection conn, Activity activity) { mActivity = activity; mInflate = activity.getLayoutInflater(); mHandler = new SimpleAlertHandler(activity); mAdapter = new ListTreeAdapter(null); mContentObserver = new MyContentObserver(); mDataSetObserver = new MyDataSetObserver(); mExpandedGroups = new ArrayList<Integer>(); mQueryHandler = new QueryHandler(activity); changeConnection(conn); } public void changeConnection(IImConnection conn) { mQueryHandler.cancelOperation(TOKEN_SUBSCRIPTION); mQueryHandler.cancelOperation(TOKEN_CONTACT_LISTS); synchronized (this) { if (mSubscriptions != null) { mSubscriptions.close(); mSubscriptions = null; } if (mOnlineContactsCountMap != null) { mOnlineContactsCountMap.close(); } } mAdapter.notifyDataSetChanged(); if (conn != null) { try { mProviderId = conn.getProviderId(); mAccountId = conn.getAccountId(); startQueryContactLists(); startQuerySubscriptions(); } catch (RemoteException e) { // Service died! } } } public void setHideOfflineContacts(boolean hide) { if (mHideOfflineContacts != hide) { mHideOfflineContacts = hide; mAdapter.notifyDataSetChanged(); } } public void startAutoRequery() { if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) { log("startAutoRequery()"); } mAutoRequery = true; if (mRequeryPending) { mRequeryPending = false; // startQueryOngoingConversations(); } } private void startQueryContactLists() { if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) { log("startQueryContactLists()"); } Uri uri = Imps.ContactList.CONTENT_URI; uri = ContentUris.withAppendedId(uri, mProviderId); uri = ContentUris.withAppendedId(uri, mAccountId); mQueryHandler.startQuery(TOKEN_CONTACT_LISTS, null, uri, CONTACT_LIST_PROJECTION, null, null, Imps.ContactList.DEFAULT_SORT_ORDER); } void startQuerySubscriptions() { if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) { log("startQuerySubscriptions()"); } Uri.Builder builder = Imps.Contacts.CONTENT_URI_CONTACTS_BY.buildUpon(); ContentUris.appendId(builder, mProviderId); ContentUris.appendId(builder, mAccountId); Uri uri = builder.build(); mQueryHandler.startQuery(TOKEN_SUBSCRIPTION, null, uri, ContactView.CONTACT_PROJECTION, String.format(Locale.US, "%s=%d AND %s=%d", Imps.Contacts.SUBSCRIPTION_STATUS, Imps.Contacts.SUBSCRIPTION_STATUS_SUBSCRIBE_PENDING, Imps.Contacts.SUBSCRIPTION_TYPE, Imps.Contacts.SUBSCRIPTION_TYPE_FROM), null, Imps.Contacts.DEFAULT_SORT_ORDER); } void startQueryContacts(long listId) { if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) { log("startQueryContacts - listId=" + listId); } String selection = mHideOfflineContacts ? ONLINE_CONTACT_SELECTION : CONTACTS_SELECTION; String[] args = { Long.toString(listId) }; int token = (int) listId; mQueryHandler.startQuery(token, null, Imps.Contacts.CONTENT_URI, ContactView.CONTACT_PROJECTION, selection, args, Imps.Contacts.DEFAULT_SORT_ORDER); } public Object getChild(int groupPosition, int childPosition) { if (isPosForSubscription(groupPosition)) { return moveTo(getSubscriptions(), childPosition); } else { return mAdapter.getChild(getChildAdapterPosition(groupPosition), childPosition); } } public long getChildId(int groupPosition, int childPosition) { if (isPosForSubscription(groupPosition)) { return getId(getSubscriptions(), childPosition); } else { return mAdapter.getChildId(getChildAdapterPosition(groupPosition), childPosition); } } public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { if (isPosForSubscription(groupPosition)) { View view = null; if (convertView != null) { // use the convert view if it matches the type required by displayEmpty if ((convertView instanceof TextView)) { view = convertView; ((TextView) view).setText(mActivity.getText(R.string.empty_conversation_group)); } else if ((convertView instanceof ContactView)) { view = convertView; } } if (view == null) { view = newChildView(parent); } Cursor cursor = getSubscriptions(); cursor.moveToPosition(childPosition); if (view instanceof ContactView) ((ContactView) view).bind(cursor, null, isScrolling()); return view; } else { return mAdapter.getChildView(getChildAdapterPosition(groupPosition), childPosition, isLastChild, convertView, parent); } } public int getChildrenCount(int groupPosition) { if (!mDataValid) { return 0; } if (isPosForSubscription(groupPosition)) { return getSubscriptionCount(); } else { // XXX getChildrenCount() may be called with an invalid groupPosition that is larger // than the total number of all groups. int position = getChildAdapterPosition(groupPosition); if (position >= mAdapter.getGroupCount()) { Log.w(ImApp.LOG_TAG, "getChildrenCount out of range"); return 0; } return mAdapter.getChildrenCount(position); } } public Object getGroup(int groupPosition) { if (isPosForSubscription(groupPosition)) { return null; } else { return mAdapter.getGroup(getChildAdapterPosition(groupPosition)); } } public int getGroupCount() { if (!mDataValid) { return 0; } int count = mAdapter.getGroupCount(); if (getSubscriptionCount() > 0) { count++; } return count; } public long getGroupId(int groupPosition) { if (isPosForSubscription(groupPosition)) { return 0; } else { return mAdapter.getGroupId(getChildAdapterPosition(groupPosition)); } } public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { if (isPosForSubscription(groupPosition)) { View v; if (convertView != null) { v = convertView; } else { v = newGroupView(parent); } TextView text1 = (TextView) v.findViewById(R.id.text1); TextView text2 = (TextView) v.findViewById(R.id.text2); Resources r = v.getResources(); String text = r.getString(R.string.subscriptions); text1.setText(text); text2.setVisibility(View.GONE); return v; } else { return mAdapter.getGroupView(getChildAdapterPosition(groupPosition), isExpanded, convertView, parent); } } public boolean isChildSelectable(int groupPosition, int childPosition) { if (isPosForSubscription(groupPosition)) return true; return mAdapter.isChildSelectable(getChildAdapterPosition(groupPosition), childPosition); } public boolean stableIds() { return true; } @Override public void registerDataSetObserver(DataSetObserver observer) { mAdapter.registerDataSetObserver(observer); super.registerDataSetObserver(observer); } @Override public void unregisterDataSetObserver(DataSetObserver observer) { mAdapter.unregisterDataSetObserver(observer); super.unregisterDataSetObserver(observer); } public boolean hasStableIds() { return true; } @Override public void onGroupCollapsed(int groupPosition) { super.onGroupCollapsed(groupPosition); mExpandedGroups.remove(Integer.valueOf(groupPosition)); int pos = getChildAdapterPosition(groupPosition); if (pos >= 0) { mAdapter.onGroupCollapsed(pos); } } @Override public void onGroupExpanded(int groupPosition) { super.onGroupExpanded(groupPosition); mExpandedGroups.add(groupPosition); int pos = getChildAdapterPosition(groupPosition); if (pos >= 0) { mAdapter.onGroupExpanded(pos); } } public int[] getExpandedGroups() { ArrayList<Integer> expandedGroups = mExpandedGroups; int size = expandedGroups.size(); int[] res = new int[size]; for (int i = 0; i < size; i++) { res[i] = expandedGroups.get(i); } return res; } View newChildView(ViewGroup parent) { return mInflate.inflate(R.layout.contact_view, parent, false); } View newEmptyView(ViewGroup parent) { return mInflate.inflate(R.layout.empty_conversation_group_view, parent, false); } View newGroupView(ViewGroup parent) { return mInflate.inflate(R.layout.group_view, parent, false); } private synchronized Cursor getSubscriptions() { if (mSubscriptions == null) { startQuerySubscriptions(); } return mSubscriptions; } synchronized void setSubscriptions(Cursor c) { if (mSubscriptions != null) { mSubscriptions.unregisterContentObserver(mContentObserver); mSubscriptions.unregisterDataSetObserver(mDataSetObserver); mSubscriptions.close(); } c.registerContentObserver(mContentObserver); c.registerDataSetObserver(mDataSetObserver); mSubscriptions = c; } private int getSubscriptionCount() { Cursor c = getSubscriptions(); return c == null ? 0 : c.getCount(); } public boolean isPosForSubscription(int groupPosition) { return groupPosition == 0 && getSubscriptionCount() > 0; } private int getChildAdapterPosition(int groupPosition) { if (getSubscriptionCount() > 0) { return groupPosition - 1; } else { return groupPosition; } } private Cursor moveTo(Cursor cursor, int position) { if (cursor.moveToPosition(position)) { return cursor; } return null; } private long getId(Cursor cursor, int position) { if (cursor.moveToPosition(position)) { return cursor.getLong(ContactView.COLUMN_CONTACT_ID); } return 0; } class ListTreeAdapter extends CursorTreeAdapter { public ListTreeAdapter(Cursor cursor) { super(cursor, mActivity); } @Override protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) { // binding when child is text view for an empty group if (view instanceof TextView) { ((TextView) view).setText(mActivity.getText(R.string.empty_contact_group)); } else { ((ContactView) view).bind(cursor, null, isScrolling()); } } @Override protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) { TextView text1 = (TextView) view.findViewById(R.id.text1); TextView text2 = (TextView) view.findViewById(R.id.text2); Resources r = view.getResources(); text1.setText(cursor.getString(COLUMN_CONTACT_LIST_NAME)); text2.setVisibility(View.VISIBLE); text2.setText(r.getString(R.string.online_count, getOnlineChildCount(cursor))); } View newEmptyView(ViewGroup parent) { return mInflate.inflate(R.layout.empty_contact_group_view, parent, false); } // if the group is empty, provide a text view. The infrastructure provides a "convertView" // as a possible suggestion to reuse an existing view's data. It may be null, it may be a // TextView, or it may be a ContactView, so we need to test the possible cases. @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { // Provide a TextView if the group is empty if (super.getChildrenCount(groupPosition) == 0) { if (convertView != null) { if (convertView instanceof TextView) { ((TextView) convertView).setText(mActivity .getText(R.string.empty_contact_group)); return convertView; } } return newEmptyView(parent); } if (!(convertView instanceof ContactView)) { convertView = null; } return super.getChildView(groupPosition, childPosition, isLastChild, convertView, parent); } @Override protected Cursor getChildrenCursor(Cursor groupCursor) { long listId = groupCursor.getLong(COLUMN_CONTACT_LIST_ID); startQueryContacts(listId); return null; } // return a TextView for empty groups @Override protected View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) { if (cursor.getCount() == 0) { return newEmptyView(parent); } else { return ContactListTreeAdapter.this.newChildView(parent); } } @Override protected View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) { return ContactListTreeAdapter.this.newGroupView(parent); } private int getOnlineChildCount(Cursor groupCursor) { long listId = groupCursor.getLong(COLUMN_CONTACT_LIST_ID); if (mOnlineContactsCountMap == null) { String where = Imps.Contacts.ACCOUNT + "=" + mAccountId; ContentResolver cr = mActivity.getContentResolver(); Cursor c = cr.query(Imps.Contacts.CONTENT_URI_ONLINE_COUNT, CONTACT_COUNT_PROJECTION, where, null, null); mOnlineContactsCountMap = new ContentQueryMap(c, Imps.Contacts.CONTACTLIST, true, mHandler); mOnlineContactsCountMap.addObserver(new Observer() { public void update(Observable observable, Object data) { notifyDataSetChanged(); } }); } ContentValues value = mOnlineContactsCountMap.getValues(String.valueOf(listId)); return value == null ? 0 : value.getAsInteger(Imps.Contacts._COUNT); } @Override public int getChildrenCount(int groupPosition) { int children = super.getChildrenCount(groupPosition); if (children == 0) { // Count the empty group text item as a child return 1; } return children; } // Don't allow the empty group text item to be selected @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return (super.getChildrenCount(groupPosition) > 0); } } private class MyContentObserver extends ContentObserver { public MyContentObserver() { super(mHandler); } @Override public void onChange(boolean selfChange) { if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) { log("MyContentObserver.onChange() autoRequery=" + mAutoRequery); } // Don't requery when fling. We will schedule a requery when the fling is complete. if (isScrolling()) { return; } if (mAutoRequery) { // startQueryOngoingConversations(); } else { mRequeryPending = true; } } } private class MyDataSetObserver extends DataSetObserver { public MyDataSetObserver() { } @Override public void onChanged() { if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) { log("MyDataSetObserver.onChanged()"); } mDataValid = true; notifyDataSetChanged(); } @Override public void onInvalidated() { if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) { log("MyDataSetObserver.onInvalidated()"); } mDataValid = false; notifyDataSetInvalidated(); } } public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // no op } public void onScrollStateChanged(AbsListView view, int scrollState) { int oldState = mScrollState; mScrollState = scrollState; // If we just finished a fling then some items may not have an icon // So force a full redraw now that the fling is complete if (oldState == OnScrollListener.SCROLL_STATE_FLING) { notifyDataSetChanged(); } } public boolean isScrolling() { return mScrollState == OnScrollListener.SCROLL_STATE_FLING; } }