/* * Copyright (C) 2010 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.launcher3.list; import android.content.Context; import android.database.Cursor; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import java.util.ArrayList; /** * A general purpose adapter that is composed of multiple cursors. It just * appends them in the order they are added. */ public abstract class CompositeCursorAdapter extends BaseAdapter { private static final int INITIAL_CAPACITY = 2; public static class Partition { boolean showIfEmpty; boolean hasHeader; Cursor cursor; int idColumnIndex; int count; public Partition(boolean showIfEmpty, boolean hasHeader) { this.showIfEmpty = showIfEmpty; this.hasHeader = hasHeader; } /** * True if the directory should be shown even if no contacts are found. */ public boolean getShowIfEmpty() { return showIfEmpty; } public boolean getHasHeader() { return hasHeader; } } private final Context mContext; private ArrayList mPartitions; private int mCount = 0; private boolean mCacheValid = true; private boolean mNotificationsEnabled = true; private boolean mNotificationNeeded; public CompositeCursorAdapter(Context context) { this(context, INITIAL_CAPACITY); } public CompositeCursorAdapter(Context context, int initialCapacity) { mContext = context; mPartitions = new ArrayList(); } public Context getContext() { return mContext; } /** * Registers a partition. The cursor for that partition can be set later. * Partitions should be added in the order they are supposed to appear in the * list. */ public void addPartition(boolean showIfEmpty, boolean hasHeader) { addPartition(new Partition(showIfEmpty, hasHeader)); } public void addPartition(Partition partition) { mPartitions.add(partition); invalidate(); notifyDataSetChanged(); } public void addPartition(int location, Partition partition) { mPartitions.add(location, partition); invalidate(); notifyDataSetChanged(); } public void removePartition(int partitionIndex) { Cursor cursor = mPartitions.get(partitionIndex).cursor; if (cursor != null && !cursor.isClosed()) { cursor.close(); } mPartitions.remove(partitionIndex); invalidate(); notifyDataSetChanged(); } /** * Removes cursors for all partitions. */ // TODO: Is this really what this is supposed to do? Just remove the cursors? Not close them? // Not remove the partitions themselves? Isn't this leaking? public void clearPartitions() { for (Partition partition : mPartitions) { partition.cursor = null; } invalidate(); notifyDataSetChanged(); } /** * Closes all cursors and removes all partitions. */ public void close() { for (Partition partition : mPartitions) { Cursor cursor = partition.cursor; if (cursor != null && !cursor.isClosed()) { cursor.close(); } } mPartitions.clear(); invalidate(); notifyDataSetChanged(); } public void setHasHeader(int partitionIndex, boolean flag) { mPartitions.get(partitionIndex).hasHeader = flag; invalidate(); } public void setShowIfEmpty(int partitionIndex, boolean flag) { mPartitions.get(partitionIndex).showIfEmpty = flag; invalidate(); } public Partition getPartition(int partitionIndex) { return mPartitions.get(partitionIndex); } protected void invalidate() { mCacheValid = false; } public int getPartitionCount() { return mPartitions.size(); } protected void ensureCacheValid() { if (mCacheValid) { return; } mCount = 0; for (Partition partition : mPartitions) { Cursor cursor = partition.cursor; int count = cursor != null ? cursor.getCount() : 0; if (partition.hasHeader) { if (count != 0 || partition.showIfEmpty) { count++; } } partition.count = count; mCount += count; } mCacheValid = true; } /** * Returns true if the specified partition was configured to have a header. */ public boolean hasHeader(int partition) { return mPartitions.get(partition).hasHeader; } /** * Returns the total number of list items in all partitions. */ public int getCount() { ensureCacheValid(); return mCount; } /** * Returns the cursor for the given partition */ public Cursor getCursor(int partition) { return mPartitions.get(partition).cursor; } /** * Changes the cursor for an individual partition. */ public void changeCursor(int partition, Cursor cursor) { Cursor prevCursor = mPartitions.get(partition).cursor; if (prevCursor != cursor) { if (prevCursor != null && !prevCursor.isClosed()) { prevCursor.close(); } mPartitions.get(partition).cursor = cursor; if (cursor != null) { mPartitions.get(partition).idColumnIndex = cursor.getColumnIndex("_id"); } invalidate(); notifyDataSetChanged(); } } /** * Returns true if the specified partition has no cursor or an empty cursor. */ public boolean isPartitionEmpty(int partition) { Cursor cursor = mPartitions.get(partition).cursor; return cursor == null || cursor.getCount() == 0; } /** * Given a list position, returns the index of the corresponding partition. */ public int getPartitionForPosition(int position) { ensureCacheValid(); int start = 0; for (int i = 0, n = mPartitions.size(); i < n; i++) { int end = start + mPartitions.get(i).count; if (position >= start && position < end) { return i; } start = end; } return -1; } /** * Given a list position, return the offset of the corresponding item in its * partition. The header, if any, will have offset -1. */ public int getOffsetInPartition(int position) { ensureCacheValid(); int start = 0; for (Partition partition : mPartitions) { int end = start + partition.count; if (position >= start && position < end) { int offset = position - start; if (partition.hasHeader) { offset--; } return offset; } start = end; } return -1; } /** * Returns the first list position for the specified partition. */ public int getPositionForPartition(int partition) { ensureCacheValid(); int position = 0; for (int i = 0; i < partition; i++) { position += mPartitions.get(i).count; } return position; } @Override public int getViewTypeCount() { return getItemViewTypeCount() + 1; } /** * Returns the overall number of item view types across all partitions. An * implementation of this method needs to ensure that the returned count is * consistent with the values returned by {@link #getItemViewType(int,int)}. */ public int getItemViewTypeCount() { return 1; } /** * Returns the view type for the list item at the specified position in the * specified partition. */ protected int getItemViewType(int partition, int position) { return 1; } @Override public int getItemViewType(int position) { ensureCacheValid(); int start = 0; for (int i = 0, n = mPartitions.size(); i < n; i++) { int end = start + mPartitions.get(i).count; if (position >= start && position < end) { int offset = position - start; if (mPartitions.get(i).hasHeader) { offset--; } if (offset == -1) { return IGNORE_ITEM_VIEW_TYPE; } else { return getItemViewType(i, offset); } } start = end; } throw new ArrayIndexOutOfBoundsException(position); } public View getView(int position, View convertView, ViewGroup parent) { ensureCacheValid(); int start = 0; for (int i = 0, n = mPartitions.size(); i < n; i++) { int end = start + mPartitions.get(i).count; if (position >= start && position < end) { int offset = position - start; if (mPartitions.get(i).hasHeader) { offset--; } View view; if (offset == -1) { view = getHeaderView(i, mPartitions.get(i).cursor, convertView, parent); } else { if (!mPartitions.get(i).cursor.moveToPosition(offset)) { throw new IllegalStateException("Couldn't move cursor to position " + offset); } view = getView(i, mPartitions.get(i).cursor, offset, convertView, parent); } if (view == null) { throw new NullPointerException("View should not be null, partition: " + i + " position: " + offset); } return view; } start = end; } throw new ArrayIndexOutOfBoundsException(position); } /** * Returns the header view for the specified partition, creating one if needed. */ protected View getHeaderView(int partition, Cursor cursor, View convertView, ViewGroup parent) { View view = convertView != null ? convertView : newHeaderView(mContext, partition, cursor, parent); bindHeaderView(view, partition, cursor); return view; } /** * Creates the header view for the specified partition. */ protected View newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent) { return null; } /** * Binds the header view for the specified partition. */ protected void bindHeaderView(View view, int partition, Cursor cursor) { } /** * Returns an item view for the specified partition, creating one if needed. */ protected View getView(int partition, Cursor cursor, int position, View convertView, ViewGroup parent) { View view; if (convertView != null) { view = convertView; } else { view = newView(mContext, partition, cursor, position, parent); } bindView(view, partition, cursor, position); return view; } /** * Creates an item view for the specified partition and position. Position * corresponds directly to the current cursor position. */ protected abstract View newView(Context context, int partition, Cursor cursor, int position, ViewGroup parent); /** * Binds an item view for the specified partition and position. Position * corresponds directly to the current cursor position. */ protected abstract void bindView(View v, int partition, Cursor cursor, int position); /** * Returns a pre-positioned cursor for the specified list position. */ public Object getItem(int position) { ensureCacheValid(); int start = 0; for (Partition mPartition : mPartitions) { int end = start + mPartition.count; if (position >= start && position < end) { int offset = position - start; if (mPartition.hasHeader) { offset--; } if (offset == -1) { return null; } Cursor cursor = mPartition.cursor; cursor.moveToPosition(offset); return cursor; } start = end; } return null; } /** * Returns the item ID for the specified list position. */ public long getItemId(int position) { ensureCacheValid(); int start = 0; for (Partition mPartition : mPartitions) { int end = start + mPartition.count; if (position >= start && position < end) { int offset = position - start; if (mPartition.hasHeader) { offset--; } if (offset == -1) { return 0; } if (mPartition.idColumnIndex == -1) { return 0; } Cursor cursor = mPartition.cursor; if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) { return 0; } return cursor.getLong(mPartition.idColumnIndex); } start = end; } return 0; } /** * Returns false if any partition has a header. */ @Override public boolean areAllItemsEnabled() { for (Partition mPartition : mPartitions) { if (mPartition.hasHeader) { return false; } } return true; } /** * Returns true for all items except headers. */ @Override public boolean isEnabled(int position) { ensureCacheValid(); int start = 0; for (int i = 0, n = mPartitions.size(); i < n; i++) { int end = start + mPartitions.get(i).count; if (position >= start && position < end) { int offset = position - start; if (mPartitions.get(i).hasHeader && offset == 0) { return false; } else { return isEnabled(i, offset); } } start = end; } return false; } /** * Returns true if the item at the specified offset of the specified * partition is selectable and clickable. */ protected boolean isEnabled(int partition, int position) { return true; } /** * Enable or disable data change notifications. It may be a good idea to * disable notifications before making changes to several partitions at once. */ public void setNotificationsEnabled(boolean flag) { mNotificationsEnabled = flag; if (flag && mNotificationNeeded) { notifyDataSetChanged(); } } @Override public void notifyDataSetChanged() { if (mNotificationsEnabled) { mNotificationNeeded = false; super.notifyDataSetChanged(); } else { mNotificationNeeded = true; } } }