content: /*
   * 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.common.widget;
  
  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;
          }
  
          public boolean isEmpty() {
              return count == 0;
          }
      }
  
      private final Context mContext;
      private ArrayList<Partition> 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<Partition>();
      }
  
      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;
              if (cursor == null || cursor.isClosed()) {
                  count = 0;
              } else {
                  count = cursor.getCount();
              }
              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 && !cursor.isClosed()) {
                  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.isClosed() || 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;
                  if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) {
                      return null;
                  }
                  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;
          }
      }
  }
  