content: /*
   * Copyright (C) 2007 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.content;
  
  import android.database.ContentObserver;
  import android.database.Cursor;
  import android.os.Handler;
  
  import java.util.HashMap;
  import java.util.Map;
  import java.util.Observable;
  
  /**
   * Caches the contents of a cursor into a Map of String->ContentValues and optionally
   * keeps the cache fresh by registering for updates on the content backing the cursor. The column of
   * the database that is to be used as the key of the map is user-configurable, and the
   * ContentValues contains all columns other than the one that is designated the key.
   * <p>
   * The cursor data is accessed by row key and column name via getValue().
   */
  public class ContentQueryMap extends Observable {
      private volatile Cursor mCursor;
      private String[] mColumnNames;
      private int mKeyColumn;
  
      private Handler mHandlerForUpdateNotifications = null;
      private boolean mKeepUpdated = false;
  
      private Map<String, ContentValues> mValues = null;
  
      private ContentObserver mContentObserver;
  
      /** Set when a cursor change notification is received and is cleared on a call to requery(). */
      private boolean mDirty = false;
  
      /**
       * Creates a ContentQueryMap that caches the content backing the cursor
       *
       * @param cursor the cursor whose contents should be cached
       * @param columnNameOfKey the column that is to be used as the key of the values map
       * @param keepUpdated true if the cursor's ContentProvider should be monitored for changes and 
       * the map updated when changes do occur
       * @param handlerForUpdateNotifications the Handler that should be used to receive
       *  notifications of changes (if requested). Normally you pass null here, but if
       *  you know that the thread that is creating this isn't a thread that can receive
       *  messages then you can create your own handler and use that here.
       */
      public ContentQueryMap(Cursor cursor, String columnNameOfKey, boolean keepUpdated,
              Handler handlerForUpdateNotifications) {
          mCursor = cursor;
          mColumnNames = mCursor.getColumnNames();
          mKeyColumn = mCursor.getColumnIndexOrThrow(columnNameOfKey);
          mHandlerForUpdateNotifications = handlerForUpdateNotifications;
          setKeepUpdated(keepUpdated);
  
          // If we aren't keeping the cache updated with the current state of the cursor's 
          // ContentProvider then read it once into the cache. Otherwise the cache will be filled 
          // automatically.
          if (!keepUpdated) {
              readCursorIntoCache(cursor);
          }
      }
  
      /**
       * Change whether or not the ContentQueryMap will register with the cursor's ContentProvider 
       * for change notifications. If you use a ContentQueryMap in an activity you should call this
       * with false in onPause(), which means you need to call it with true in onResume()
       * if want it to be kept updated.
       * @param keepUpdated if true the ContentQueryMap should be registered with the cursor's
       * ContentProvider, false otherwise
       */
      public void setKeepUpdated(boolean keepUpdated) {
          if (keepUpdated == mKeepUpdated) return;
          mKeepUpdated = keepUpdated;
  
          if (!mKeepUpdated) {
              mCursor.unregisterContentObserver(mContentObserver);
              mContentObserver = null;
          } else {
              if (mHandlerForUpdateNotifications == null) {
                  mHandlerForUpdateNotifications = new Handler();
              }
              if (mContentObserver == null) {
                  mContentObserver = new ContentObserver(mHandlerForUpdateNotifications) {
                      @Override
                      public void onChange(boolean selfChange) {
                          // If anyone is listening, we need to do this now to broadcast
                          // to the observers.  Otherwise, we'll just set mDirty and
                          // let it query lazily when they ask for the values.
                          if (countObservers() != 0) {
                              requery();
                          } else {
                              mDirty = true;
                          }
                      }
                  };
              }
              mCursor.registerContentObserver(mContentObserver);
              // mark dirty, since it is possible the cursor's backing data had changed before we 
              // registered for changes
              mDirty = true;
          }
      }
  
      /**
       * Access the ContentValues for the row specified by rowName
       * @param rowName which row to read
       * @return the ContentValues for the row, or null if the row wasn't present in the cursor
       */
      public synchronized ContentValues getValues(String rowName) {
          if (mDirty) requery();
          return mValues.get(rowName);
      }
  
      /** Requeries the cursor and reads the contents into the cache */
      public void requery() {
          final Cursor cursor = mCursor;
          if (cursor == null) {
              // If mCursor is null then it means there was a requery() in flight
              // while another thread called close(), which nulls out mCursor.
              // If this happens ignore the requery() since we are closed anyways.
              return;
          }
          mDirty = false;
          if (!cursor.requery()) {
              // again, don't do anything if the cursor is already closed
              return;
          }
          readCursorIntoCache(cursor);
          setChanged();
          notifyObservers();
      }
  
      private synchronized void readCursorIntoCache(Cursor cursor) {
          // Make a new map so old values returned by getRows() are undisturbed.
          int capacity = mValues != null ? mValues.size() : 0;
          mValues = new HashMap<String, ContentValues>(capacity);
          while (cursor.moveToNext()) {
              ContentValues values = new ContentValues();
              for (int i = 0; i < mColumnNames.length; i++) {
                  if (i != mKeyColumn) {
                      values.put(mColumnNames[i], cursor.getString(i));
                  }
              }
              mValues.put(cursor.getString(mKeyColumn), values);
          }
      }
  
      public synchronized Map<String, ContentValues> getRows() {
          if (mDirty) requery();
          return mValues;
      }
  
      public synchronized void close() {
          if (mContentObserver != null) {
              mCursor.unregisterContentObserver(mContentObserver);
              mContentObserver = null;
          }
          mCursor.close();
          mCursor = null;
      }
  
      @Override
      protected void finalize() throws Throwable {
          if (mCursor != null) close();
          super.finalize();
      }
  }
  