content: /*
   * Copyright (C) 2006 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 android.widget;
  
  import android.annotation.UnsupportedAppUsage;
  import android.annotation.WorkerThread;
  import android.content.Context;
  import android.content.res.Resources;
  import android.database.ContentObserver;
  import android.database.Cursor;
  import android.database.DataSetObserver;
  import android.os.Handler;
  import android.util.Log;
  import android.view.ContextThemeWrapper;
  import android.view.View;
  import android.view.ViewGroup;
  
  /**
   * Adapter that exposes data from a {@link android.database.Cursor Cursor} to a
   * {@link android.widget.ListView ListView} widget.
   * <p>
   * The Cursor must include a column named "_id" or this class will not work.
   * Additionally, using {@link android.database.MergeCursor} with this class will
   * not work if the merged Cursors have overlapping values in their "_id"
   * columns.
   */
  public abstract class CursorAdapter extends BaseAdapter implements Filterable,
          CursorFilter.CursorFilterClient, ThemedSpinnerAdapter {
      /**
       * This field should be made private, so it is hidden from the SDK.
       * {@hide}
       */
      @UnsupportedAppUsage
      protected boolean mDataValid;
      /**
       * This field should be made private, so it is hidden from the SDK.
       * {@hide}
       */
      protected boolean mAutoRequery;
      /**
       * This field should be made private, so it is hidden from the SDK.
       * {@hide}
       */
      @UnsupportedAppUsage
      protected Cursor mCursor;
      /**
       * This field should be made private, so it is hidden from the SDK.
       * {@hide}
       */
      @UnsupportedAppUsage
      protected Context mContext;
      /**
       * Context used for {@link #getDropDownView(int, View, ViewGroup)}.
       * {@hide}
       */
      protected Context mDropDownContext;
      /**
       * This field should be made private, so it is hidden from the SDK.
       * {@hide}
       */
      @UnsupportedAppUsage
      protected int mRowIDColumn;
      /**
       * This field should be made private, so it is hidden from the SDK.
       * {@hide}
       */
      @UnsupportedAppUsage
      protected ChangeObserver mChangeObserver;
      /**
       * This field should be made private, so it is hidden from the SDK.
       * {@hide}
       */
      @UnsupportedAppUsage
      protected DataSetObserver mDataSetObserver;
      /**
       * This field should be made private, so it is hidden from the SDK.
       * {@hide}
       */
      protected CursorFilter mCursorFilter;
      /**
       * This field should be made private, so it is hidden from the SDK.
       * {@hide}
       */
      protected FilterQueryProvider mFilterQueryProvider;
  
      /**
       * If set the adapter will call requery() on the cursor whenever a content change
       * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
       *
       * @deprecated This option is discouraged, as it results in Cursor queries
       * being performed on the application's UI thread and thus can cause poor
       * responsiveness or even Application Not Responding errors.  As an alternative,
       * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
       */
      @Deprecated
      public static final int FLAG_AUTO_REQUERY = 0x01;
  
      /**
       * If set the adapter will register a content observer on the cursor and will call
       * {@link #onContentChanged()} when a notification comes in.  Be careful when
       * using this flag: you will need to unset the current Cursor from the adapter
       * to avoid leaks due to its registered observers.  This flag is not needed
       * when using a CursorAdapter with a
       * {@link android.content.CursorLoader}.
       */
      public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
  
      /**
       * Constructor that always enables auto-requery.
       *
       * @deprecated This option is discouraged, as it results in Cursor queries
       * being performed on the application's UI thread and thus can cause poor
       * responsiveness or even Application Not Responding errors.  As an alternative,
       * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
       *
       * @param c The cursor from which to get the data.
       * @param context The context
       */
      @Deprecated
      public CursorAdapter(Context context, Cursor c) {
          init(context, c, FLAG_AUTO_REQUERY);
      }
  
      /**
       * Constructor that allows control over auto-requery.  It is recommended
       * you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}.
       * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
       * will always be set.
       *
       * @param c The cursor from which to get the data.
       * @param context The context
       * @param autoRequery If true the adapter will call requery() on the
       *                    cursor whenever it changes so the most recent
       *                    data is always displayed.  Using true here is discouraged.
       */
      public CursorAdapter(Context context, Cursor c, boolean autoRequery) {
          init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
      }
  
      /**
       * Recommended constructor.
       *
       * @param c The cursor from which to get the data.
       * @param context The context
       * @param flags Flags used to determine the behavior of the adapter; may
       * be any combination of {@link #FLAG_AUTO_REQUERY} and
       * {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
       */
      public CursorAdapter(Context context, Cursor c, int flags) {
          init(context, c, flags);
      }
  
      /**
       * @deprecated Don't use this, use the normal constructor.  This will
       * be removed in the future.
       */
      @Deprecated
      protected void init(Context context, Cursor c, boolean autoRequery) {
          init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
      }
  
      void init(Context context, Cursor c, int flags) {
          if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
              flags |= FLAG_REGISTER_CONTENT_OBSERVER;
              mAutoRequery = true;
          } else {
              mAutoRequery = false;
          }
          boolean cursorPresent = c != null;
          mCursor = c;
          mDataValid = cursorPresent;
          mContext = context;
          mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
          if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
              mChangeObserver = new ChangeObserver();
              mDataSetObserver = new MyDataSetObserver();
          } else {
              mChangeObserver = null;
              mDataSetObserver = null;
          }
  
          if (cursorPresent) {
              if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
              if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
          }
      }
  
      /**
       * Sets the {@link Resources.Theme} against which drop-down views are
       * inflated.
       * <p>
       * By default, drop-down views are inflated against the theme of the
       * {@link Context} passed to the adapter's constructor.
       *
       * @param theme the theme against which to inflate drop-down views or
       *              {@code null} to use the theme from the adapter's context
       * @see #newDropDownView(Context, Cursor, ViewGroup)
       */
      @Override
      public void setDropDownViewTheme(Resources.Theme theme) {
          if (theme == null) {
              mDropDownContext = null;
          } else if (theme == mContext.getTheme()) {
              mDropDownContext = mContext;
          } else {
              mDropDownContext = new ContextThemeWrapper(mContext, theme);
          }
      }
  
      @Override
      public Resources.Theme getDropDownViewTheme() {
          return mDropDownContext == null ? null : mDropDownContext.getTheme();
      }
  
      /**
       * Returns the cursor.
       * @return the cursor.
       */
      public Cursor getCursor() {
          return mCursor;
      }
  
      /**
       * @see android.widget.ListAdapter#getCount()
       */
      public int getCount() {
          if (mDataValid && mCursor != null) {
              return mCursor.getCount();
          } else {
              return 0;
          }
      }
      
      /**
       * @see android.widget.ListAdapter#getItem(int)
       */
      public Object getItem(int position) {
          if (mDataValid && mCursor != null) {
              mCursor.moveToPosition(position);
              return mCursor;
          } else {
              return null;
          }
      }
  
      /**
       * @see android.widget.ListAdapter#getItemId(int)
       */
      public long getItemId(int position) {
          if (mDataValid && mCursor != null) {
              if (mCursor.moveToPosition(position)) {
                  return mCursor.getLong(mRowIDColumn);
              } else {
                  return 0;
              }
          } else {
              return 0;
          }
      }
      
      @Override
      public boolean hasStableIds() {
          return true;
      }
  
      /**
       * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
       */
      public View getView(int position, View convertView, ViewGroup parent) {
          if (!mDataValid) {
              throw new IllegalStateException("this should only be called when the cursor is valid");
          }
          if (!mCursor.moveToPosition(position)) {
              throw new IllegalStateException("couldn't move cursor to position " + position);
          }
          View v;
          if (convertView == null) {
              v = newView(mContext, mCursor, parent);
          } else {
              v = convertView;
          }
          bindView(v, mContext, mCursor);
          return v;
      }
  
      @Override
      public View getDropDownView(int position, View convertView, ViewGroup parent) {
          if (mDataValid) {
              final Context context = mDropDownContext == null ? mContext : mDropDownContext;
              mCursor.moveToPosition(position);
              final View v;
              if (convertView == null) {
                  v = newDropDownView(context, mCursor, parent);
              } else {
                  v = convertView;
              }
              bindView(v, context, mCursor);
              return v;
          } else {
              return null;
          }
      }
  
      /**
       * Makes a new view to hold the data pointed to by cursor.
       * @param context Interface to application's global information
       * @param cursor The cursor from which to get the data. The cursor is already
       * moved to the correct position.
       * @param parent The parent to which the new view is attached to
       * @return the newly created view.
       */
      public abstract View newView(Context context, Cursor cursor, ViewGroup parent);
  
      /**
       * Makes a new drop down view to hold the data pointed to by cursor.
       * @param context Interface to application's global information
       * @param cursor The cursor from which to get the data. The cursor is already
       * moved to the correct position.
       * @param parent The parent to which the new view is attached to
       * @return the newly created view.
       */
      public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
          return newView(context, cursor, parent);
      }
  
      /**
       * Bind an existing view to the data pointed to by cursor
       * @param view Existing view, returned earlier by newView
       * @param context Interface to application's global information
       * @param cursor The cursor from which to get the data. The cursor is already
       * moved to the correct position.
       */
      public abstract void bindView(View view, Context context, Cursor cursor);
      
      /**
       * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
       * closed.
       * 
       * @param cursor The new cursor to be used
       */
      public void changeCursor(Cursor cursor) {
          Cursor old = swapCursor(cursor);
          if (old != null) {
              old.close();
          }
      }
  
      /**
       * Swap in a new Cursor, returning the old Cursor.  Unlike
       * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
       * closed.
       *
       * @param newCursor The new cursor to be used.
       * @return Returns the previously set Cursor, or null if there was not one.
       * If the given new Cursor is the same instance is the previously set
       * Cursor, null is also returned.
       */
      public Cursor swapCursor(Cursor newCursor) {
          if (newCursor == mCursor) {
              return null;
          }
          Cursor oldCursor = mCursor;
          if (oldCursor != null) {
              if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
              if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
          }
          mCursor = newCursor;
          if (newCursor != null) {
              if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
              if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
              mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
              mDataValid = true;
              // notify the observers about the new cursor
              notifyDataSetChanged();
          } else {
              mRowIDColumn = -1;
              mDataValid = false;
              // notify the observers about the lack of a data set
              notifyDataSetInvalidated();
          }
          return oldCursor;
      }
  
      /**
       * <p>Converts the cursor into a CharSequence. Subclasses should override this
       * method to convert their results. The default implementation returns an
       * empty String for null values or the default String representation of
       * the value.</p>
       *
       * @param cursor the cursor to convert to a CharSequence
       * @return a CharSequence representing the value
       */
      public CharSequence convertToString(Cursor cursor) {
          return cursor == null ? "" : cursor.toString();
      }
  
      /**
       * Runs a query with the specified constraint. This query is requested
       * by the filter attached to this adapter.
       *
       * The query is provided by a
       * {@link android.widget.FilterQueryProvider}.
       * If no provider is specified, the current cursor is not filtered and returned.
       *
       * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
       * and the previous cursor is closed.
       *
       * This method is always executed on a background thread, not on the
       * application's main thread (or UI thread.)
       * 
       * Contract: when constraint is null or empty, the original results,
       * prior to any filtering, must be returned.
       *
       * @param constraint the constraint with which the query must be filtered
       *
       * @return a Cursor representing the results of the new query
       *
       * @see #getFilter()
       * @see #getFilterQueryProvider()
       * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
       */
      @WorkerThread
      public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
          if (mFilterQueryProvider != null) {
              return mFilterQueryProvider.runQuery(constraint);
          }
  
          return mCursor;
      }
  
      public Filter getFilter() {
          if (mCursorFilter == null) {
              mCursorFilter = new CursorFilter(this);
          }
          return mCursorFilter;
      }
  
      /**
       * Returns the query filter provider used for filtering. When the
       * provider is null, no filtering occurs.
       *
       * @return the current filter query provider or null if it does not exist
       *
       * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
       * @see #runQueryOnBackgroundThread(CharSequence)
       */
      public FilterQueryProvider getFilterQueryProvider() {
          return mFilterQueryProvider;
      }
  
      /**
       * Sets the query filter provider used to filter the current Cursor.
       * The provider's
       * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}
       * method is invoked when filtering is requested by a client of
       * this adapter.
       *
       * @param filterQueryProvider the filter query provider or null to remove it
       *
       * @see #getFilterQueryProvider()
       * @see #runQueryOnBackgroundThread(CharSequence)
       */
      public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
          mFilterQueryProvider = filterQueryProvider;
      }
  
      /**
       * Called when the {@link ContentObserver} on the cursor receives a change notification.
       * The default implementation provides the auto-requery logic, but may be overridden by
       * sub classes.
       * 
       * @see ContentObserver#onChange(boolean)
       */
      protected void onContentChanged() {
          if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
              if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
              mDataValid = mCursor.requery();
          }
      }
  
      private class ChangeObserver extends ContentObserver {
          public ChangeObserver() {
              super(new Handler());
          }
  
          @Override
          public boolean deliverSelfNotifications() {
              return true;
          }
  
          @Override
          public void onChange(boolean selfChange) {
              onContentChanged();
          }
      }
  
      private class MyDataSetObserver extends DataSetObserver {
          @Override
          public void onChanged() {
              mDataValid = true;
              notifyDataSetChanged();
          }
  
          @Override
          public void onInvalidated() {
              mDataValid = false;
              notifyDataSetInvalidated();
          }
      }
  
  }
  