/* * 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 com.android.im.app; 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.BaseExpandableListAdapter; import android.widget.CursorTreeAdapter; import android.widget.TextView; import android.widget.AbsListView; import android.widget.AbsListView.OnScrollListener; import com.android.im.IImConnection; import com.android.im.R; import com.android.im.plugin.BrandingResourceIDs; import com.android.im.provider.Imps; import java.util.ArrayList; import java.util.Observable; import java.util.Observer; 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 mOngoingConversations; 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_SUBSCRITPTION = -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 CONTACTS_SELECTION = Imps.Contacts.CONTACTLIST + "=? AND " + NON_CHAT_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_ONGOING_CONVERSATION) { setOngoingConversations(c); notifyDataSetChanged(); } else if (token == TOKEN_SUBSCRITPTION) { 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_ONGOING_CONVERSATION); mQueryHandler.cancelOperation(TOKEN_SUBSCRITPTION); mQueryHandler.cancelOperation(TOKEN_CONTACT_LISTS); synchronized (this) { if (mOngoingConversations != null) { mOngoingConversations.close(); mOngoingConversations = null; } if (mSubscriptions != null) { mSubscriptions.close(); mSubscriptions = null; } if (mOnlineContactsCountMap != null) { mOnlineContactsCountMap.close(); } } mAdapter.notifyDataSetChanged(); if (conn != null) { try { mProviderId = conn.getProviderId(); mAccountId = conn.getAccountId(); startQueryOngoingConversations(); 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 startQueryOngoingConversations() { if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ log("startQueryOngoingConversations()"); } Uri uri = Imps.Contacts.CONTENT_URI_CHAT_CONTACTS_BY; uri = ContentUris.withAppendedId(uri, mProviderId); uri = ContentUris.withAppendedId(uri, mAccountId); mQueryHandler.startQuery(TOKEN_ONGOING_CONVERSATION, null, uri, ContactView.CONTACT_PROJECTION, null, null, Imps.Contacts.DEFAULT_SORT_ORDER); } void startQuerySubscriptions() { if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ log("startQuerySubscriptions()"); } Uri uri = Imps.Contacts.CONTENT_URI_CONTACTS_BY; uri = ContentUris.withAppendedId(uri, mProviderId); uri = ContentUris.withAppendedId(uri, mAccountId); mQueryHandler.startQuery(TOKEN_SUBSCRITPTION, null, uri, ContactView.CONTACT_PROJECTION, String.format("%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 (isPosForOngoingConversation(groupPosition)) { // No cursor exists for the "Empty" TextView item if (getOngoingConversationCount() == 0) return null; return moveTo(getOngoingConversations(), childPosition); } else if (isPosForSubscription(groupPosition)) { return moveTo(getSubscriptions(), childPosition); } else { return mAdapter.getChild(getChildAdapterPosition(groupPosition), childPosition); } } public long getChildId(int groupPosition, int childPosition) { if (isPosForOngoingConversation(groupPosition)) { // No cursor id exists for the "Empty" TextView item if (getOngoingConversationCount() == 0) return 0; return getId(getOngoingConversations(), childPosition); } else 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) { boolean isOngoingConversation = isPosForOngoingConversation(groupPosition); boolean displayEmpty = isOngoingConversation && (getOngoingConversationCount() == 0); if (isOngoingConversation || isPosForSubscription(groupPosition)) { View view = null; if (convertView != null) { // use the convert view if it matches the type required by displayEmpty if (displayEmpty && (convertView instanceof TextView)) { view = convertView; ((TextView) view).setText(mActivity.getText(R.string.empty_conversation_group)); } else if (!displayEmpty && (convertView instanceof ContactView)) { view = convertView; } } if (view == null) { if (displayEmpty) { view = newEmptyView(parent); } else { view = newChildView(parent); } } if (!displayEmpty) { Cursor cursor = isPosForOngoingConversation(groupPosition) ? getOngoingConversations() : getSubscriptions(); cursor.moveToPosition(childPosition); ((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 (isPosForOngoingConversation(groupPosition)) { // if there are no ongoing conversations, we want to display "empty" textview int count = getOngoingConversationCount(); if (count == 0) { count = 1; } return count; } else 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 (isPosForOngoingConversation(groupPosition) || isPosForSubscription(groupPosition)) { return null; } else { return mAdapter.getGroup(getChildAdapterPosition(groupPosition)); } } public int getGroupCount() { if (!mDataValid) { return 0; } int count = mAdapter.getGroupCount(); // ongoing conversations count++; if (getSubscriptionCount() > 0) { count++; } return count; } public long getGroupId(int groupPosition) { if (isPosForOngoingConversation(groupPosition) || isPosForSubscription(groupPosition)) { return 0; } else { return mAdapter.getGroupId(getChildAdapterPosition(groupPosition)); } } public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { if (isPosForOngoingConversation(groupPosition) || 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(); ImApp app = ImApp.getApplication(mActivity); BrandingResources brandingRes = app.getBrandingResource(mProviderId); String text = isPosForOngoingConversation(groupPosition) ? brandingRes.getString( BrandingResourceIDs.STRING_ONGOING_CONVERSATION, getOngoingConversationCount()) : 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 (isPosForOngoingConversation(groupPosition)) { // "Empty" TextView is not selectable if (getOngoingConversationCount()==0) return false; return true; } 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 getOngoingConversations() { if (mOngoingConversations == null) { startQueryOngoingConversations(); } return mOngoingConversations; } synchronized void setOngoingConversations(Cursor c) { if (mOngoingConversations != null) { mOngoingConversations.unregisterContentObserver(mContentObserver); mOngoingConversations.unregisterDataSetObserver(mDataSetObserver); mOngoingConversations.close(); } c.registerContentObserver(mContentObserver); c.registerDataSetObserver(mDataSetObserver); mOngoingConversations = c; } private int getOngoingConversationCount() { Cursor c = getOngoingConversations(); return c == null ? 0 : c.getCount(); } private synchronized Cursor getSubscriptions() { if (mSubscriptions == null) { startQuerySubscriptions(); } return mSubscriptions; } synchronized void setSubscriptions(Cursor c) { if (mSubscriptions != null) { mSubscriptions.close(); } // we don't need to register observers on mSubscriptions because // we already have observers on mOngoingConversations and they // will be notified if there is any changes of subscription // since the two cursors come from the same table. mSubscriptions = c; } private int getSubscriptionCount() { Cursor c = getSubscriptions(); return c == null ? 0 : c.getCount(); } public boolean isPosForOngoingConversation(int groupPosition) { return groupPosition == 0; } public boolean isPosForSubscription(int groupPosition) { return groupPosition == 1 && getSubscriptionCount() > 0; } private int getChildAdapterPosition(int groupPosition) { if (getSubscriptionCount() > 0) { return groupPosition - 2; } else { return groupPosition - 1; } } 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; } }