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.database;
  
  import android.annotation.NonNull;
  import android.annotation.UnsupportedAppUsage;
  import android.content.ContentResolver;
  import android.net.Uri;
  import android.os.Build;
  import android.os.Bundle;
  import android.util.Log;
  
  import com.android.internal.util.Preconditions;
  
  import java.lang.ref.WeakReference;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  
  /**
   * This is an abstract cursor class that handles a lot of the common code
   * that all cursors need to deal with and is provided for convenience reasons.
   */
  public abstract class AbstractCursor implements CrossProcessCursor {
      private static final String TAG = "Cursor";
  
      /**
       * @removed This field should not be used.
       */
      protected HashMap<Long, Map<String, Object>> mUpdatedRows;
  
      /**
       * @removed This field should not be used.
       */
      protected int mRowIdColumnIndex;
  
      /**
       * @removed This field should not be used.
       */
      protected Long mCurrentRowID;
  
      /**
       * @deprecated Use {@link #getPosition()} instead.
       */
      @Deprecated
      protected int mPos;
  
      /**
       * @deprecated Use {@link #isClosed()} instead.
       */
      @Deprecated
      protected boolean mClosed;
  
      /**
       * @deprecated Do not use.
       */
      @Deprecated
      protected ContentResolver mContentResolver;
  
      @UnsupportedAppUsage
      private Uri mNotifyUri;
      private List<Uri> mNotifyUris;
  
      private final Object mSelfObserverLock = new Object();
      private ContentObserver mSelfObserver;
      private boolean mSelfObserverRegistered;
  
      private final DataSetObservable mDataSetObservable = new DataSetObservable();
      private final ContentObservable mContentObservable = new ContentObservable();
  
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      private Bundle mExtras = Bundle.EMPTY;
  
      /* -------------------------------------------------------- */
      /* These need to be implemented by subclasses */
      @Override
      abstract public int getCount();
  
      @Override
      abstract public String[] getColumnNames();
  
      @Override
      abstract public String getString(int column);
      @Override
      abstract public short getShort(int column);
      @Override
      abstract public int getInt(int column);
      @Override
      abstract public long getLong(int column);
      @Override
      abstract public float getFloat(int column);
      @Override
      abstract public double getDouble(int column);
      @Override
      abstract public boolean isNull(int column);
  
      @Override
      public int getType(int column) {
          // Reflects the assumption that all commonly used field types (meaning everything
          // but blobs) are convertible to strings so it should be safe to call
          // getString to retrieve them.
          return FIELD_TYPE_STRING;
      }
  
      // TODO implement getBlob in all cursor types
      @Override
      public byte[] getBlob(int column) {
          throw new UnsupportedOperationException("getBlob is not supported");
      }
      /* -------------------------------------------------------- */
      /* Methods that may optionally be implemented by subclasses */
  
      /**
       * If the cursor is backed by a {@link CursorWindow}, returns a pre-filled
       * window with the contents of the cursor, otherwise null.
       *
       * @return The pre-filled window that backs this cursor, or null if none.
       */
      @Override
      public CursorWindow getWindow() {
          return null;
      }
  
      @Override
      public int getColumnCount() {
          return getColumnNames().length;
      }
  
      @Override
      public void deactivate() {
          onDeactivateOrClose();
      }
  
      /** @hide */
      protected void onDeactivateOrClose() {
          if (mSelfObserver != null) {
              mContentResolver.unregisterContentObserver(mSelfObserver);
              mSelfObserverRegistered = false;
          }
          mDataSetObservable.notifyInvalidated();
      }
  
      @Override
      public boolean requery() {
          if (mSelfObserver != null && mSelfObserverRegistered == false) {
              final int size = mNotifyUris.size();
              for (int i = 0; i < size; ++i) {
                  final Uri notifyUri = mNotifyUris.get(i);
                  mContentResolver.registerContentObserver(notifyUri, true, mSelfObserver);
              }
              mSelfObserverRegistered = true;
          }
          mDataSetObservable.notifyChanged();
          return true;
      }
  
      @Override
      public boolean isClosed() {
          return mClosed;
      }
  
      @Override
      public void close() {
          mClosed = true;
          mContentObservable.unregisterAll();
          onDeactivateOrClose();
      }
  
      /**
       * This function is called every time the cursor is successfully scrolled
       * to a new position, giving the subclass a chance to update any state it
       * may have. If it returns false the move function will also do so and the
       * cursor will scroll to the beforeFirst position.
       *
       * @param oldPosition the position that we're moving from
       * @param newPosition the position that we're moving to
       * @return true if the move is successful, false otherwise
       */
      @Override
      public boolean onMove(int oldPosition, int newPosition) {
          return true;
      }
  
  
      @Override
      public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
          // Default implementation, uses getString
          String result = getString(columnIndex);
          if (result != null) {
              char[] data = buffer.data;
              if (data == null || data.length < result.length()) {
                  buffer.data = result.toCharArray();
              } else {
                  result.getChars(0, result.length(), data, 0);
              }
              buffer.sizeCopied = result.length();
          } else {
              buffer.sizeCopied = 0;
          }
      }
  
      /* -------------------------------------------------------- */
      /* Implementation */
      public AbstractCursor() {
          mPos = -1;
      }
  
      @Override
      public final int getPosition() {
          return mPos;
      }
  
      @Override
      public final boolean moveToPosition(int position) {
          // Make sure position isn't past the end of the cursor
          final int count = getCount();
          if (position >= count) {
              mPos = count;
              return false;
          }
  
          // Make sure position isn't before the beginning of the cursor
          if (position < 0) {
              mPos = -1;
              return false;
          }
  
          // Check for no-op moves, and skip the rest of the work for them
          if (position == mPos) {
              return true;
          }
  
          boolean result = onMove(mPos, position);
          if (result == false) {
              mPos = -1;
          } else {
              mPos = position;
          }
  
          return result;
      }
  
      @Override
      public void fillWindow(int position, CursorWindow window) {
          DatabaseUtils.cursorFillWindow(this, position, window);
      }
  
      @Override
      public final boolean move(int offset) {
          return moveToPosition(mPos + offset);
      }
  
      @Override
      public final boolean moveToFirst() {
          return moveToPosition(0);
      }
  
      @Override
      public final boolean moveToLast() {
          return moveToPosition(getCount() - 1);
      }
  
      @Override
      public final boolean moveToNext() {
          return moveToPosition(mPos + 1);
      }
  
      @Override
      public final boolean moveToPrevious() {
          return moveToPosition(mPos - 1);
      }
  
      @Override
      public final boolean isFirst() {
          return mPos == 0 && getCount() != 0;
      }
  
      @Override
      public final boolean isLast() {
          int cnt = getCount();
          return mPos == (cnt - 1) && cnt != 0;
      }
  
      @Override
      public final boolean isBeforeFirst() {
          if (getCount() == 0) {
              return true;
          }
          return mPos == -1;
      }
  
      @Override
      public final boolean isAfterLast() {
          if (getCount() == 0) {
              return true;
          }
          return mPos == getCount();
      }
  
      @Override
      public int getColumnIndex(String columnName) {
          // Hack according to bug 903852
          final int periodIndex = columnName.lastIndexOf('.');
          if (periodIndex != -1) {
              Exception e = new Exception();
              Log.e(TAG, "requesting column name with table name -- " + columnName, e);
              columnName = columnName.substring(periodIndex + 1);
          }
  
          String columnNames[] = getColumnNames();
          int length = columnNames.length;
          for (int i = 0; i < length; i++) {
              if (columnNames[i].equalsIgnoreCase(columnName)) {
                  return i;
              }
          }
  
          if (false) {
              if (getCount() > 0) {
                  Log.w("AbstractCursor", "Unknown column " + columnName);
              }
          }
          return -1;
      }
  
      @Override
      public int getColumnIndexOrThrow(String columnName) {
          final int index = getColumnIndex(columnName);
          if (index < 0) {
              String availableColumns = "";
              try {
                  availableColumns = Arrays.toString(getColumnNames());
              } catch (Exception e) {
                  Log.d(TAG, "Cannot collect column names for debug purposes", e);
              }
              throw new IllegalArgumentException("column '" + columnName
                      + "' does not exist. Available columns: " + availableColumns);
          }
          return index;
      }
  
      @Override
      public String getColumnName(int columnIndex) {
          return getColumnNames()[columnIndex];
      }
  
      @Override
      public void registerContentObserver(ContentObserver observer) {
          mContentObservable.registerObserver(observer);
      }
  
      @Override
      public void unregisterContentObserver(ContentObserver observer) {
          // cursor will unregister all observers when it close
          if (!mClosed) {
              mContentObservable.unregisterObserver(observer);
          }
      }
  
      @Override
      public void registerDataSetObserver(DataSetObserver observer) {
          mDataSetObservable.registerObserver(observer);
      }
  
      @Override
      public void unregisterDataSetObserver(DataSetObserver observer) {
          mDataSetObservable.unregisterObserver(observer);
      }
  
      /**
       * Subclasses must call this method when they finish committing updates to notify all
       * observers.
       *
       * @param selfChange
       */
      protected void onChange(boolean selfChange) {
          synchronized (mSelfObserverLock) {
              mContentObservable.dispatchChange(selfChange, null);
              if (mNotifyUris != null && selfChange) {
                  final int size = mNotifyUris.size();
                  for (int i = 0; i < size; ++i) {
                      final Uri notifyUri = mNotifyUris.get(i);
                      mContentResolver.notifyChange(notifyUri, mSelfObserver);
                  }
              }
          }
      }
  
      /**
       * Specifies a content URI to watch for changes.
       *
       * @param cr The content resolver from the caller's context.
       * @param notifyUri The URI to watch for changes. This can be a
       * specific row URI, or a base URI for a whole class of content.
       */
      @Override
      public void setNotificationUri(ContentResolver cr, Uri notifyUri) {
          setNotificationUris(cr, Arrays.asList(notifyUri));
      }
  
      @Override
      public void setNotificationUris(@NonNull ContentResolver cr, @NonNull List<Uri> notifyUris) {
          Preconditions.checkNotNull(cr);
          Preconditions.checkNotNull(notifyUris);
  
          setNotificationUris(cr, notifyUris, cr.getUserId(), true);
      }
  
      /**
       * Set the notification uri but with an observer for a particular user's view. Also allows
       * disabling the use of a self observer, which is sensible if either
       * a) the cursor's owner calls {@link #onChange(boolean)} whenever the content changes, or
       * b) the cursor is known not to have any content observers.
       * @hide
       */
      public void setNotificationUris(ContentResolver cr, List<Uri> notifyUris, int userHandle,
              boolean registerSelfObserver) {
          synchronized (mSelfObserverLock) {
              mNotifyUris = notifyUris;
              mNotifyUri = mNotifyUris.get(0);
              mContentResolver = cr;
              if (mSelfObserver != null) {
                  mContentResolver.unregisterContentObserver(mSelfObserver);
                  mSelfObserverRegistered = false;
              }
              if (registerSelfObserver) {
                  mSelfObserver = new SelfContentObserver(this);
                  final int size = mNotifyUris.size();
                  for (int i = 0; i < size; ++i) {
                      final Uri notifyUri = mNotifyUris.get(i);
                      mContentResolver.registerContentObserver(
                              notifyUri, true, mSelfObserver, userHandle);
                  }
                  mSelfObserverRegistered = true;
              }
          }
      }
  
      @Override
      public Uri getNotificationUri() {
          synchronized (mSelfObserverLock) {
              return mNotifyUri;
          }
      }
  
      @Override
      public List<Uri> getNotificationUris() {
          synchronized (mSelfObserverLock) {
              return mNotifyUris;
          }
      }
  
      @Override
      public boolean getWantsAllOnMoveCalls() {
          return false;
      }
  
      @Override
      public void setExtras(Bundle extras) {
          mExtras = (extras == null) ? Bundle.EMPTY : extras;
      }
  
      @Override
      public Bundle getExtras() {
          return mExtras;
      }
  
      @Override
      public Bundle respond(Bundle extras) {
          return Bundle.EMPTY;
      }
  
      /**
       * @deprecated Always returns false since Cursors do not support updating rows
       */
      @Deprecated
      protected boolean isFieldUpdated(int columnIndex) {
          return false;
      }
  
      /**
       * @deprecated Always returns null since Cursors do not support updating rows
       */
      @Deprecated
      protected Object getUpdatedField(int columnIndex) {
          return null;
      }
  
      /**
       * This function throws CursorIndexOutOfBoundsException if
       * the cursor position is out of bounds. Subclass implementations of
       * the get functions should call this before attempting
       * to retrieve data.
       *
       * @throws CursorIndexOutOfBoundsException
       */
      protected void checkPosition() {
          if (-1 == mPos || getCount() == mPos) {
              throw new CursorIndexOutOfBoundsException(mPos, getCount());
          }
      }
  
      @Override
      protected void finalize() {
          if (mSelfObserver != null && mSelfObserverRegistered == true) {
              mContentResolver.unregisterContentObserver(mSelfObserver);
          }
          try {
              if (!mClosed) close();
          } catch(Exception e) { }
      }
  
      /**
       * Cursors use this class to track changes others make to their URI.
       */
      protected static class SelfContentObserver extends ContentObserver {
          WeakReference<AbstractCursor> mCursor;
  
          public SelfContentObserver(AbstractCursor cursor) {
              super(null);
              mCursor = new WeakReference<AbstractCursor>(cursor);
          }
  
          @Override
          public boolean deliverSelfNotifications() {
              return false;
          }
  
          @Override
          public void onChange(boolean selfChange) {
              AbstractCursor cursor = mCursor.get();
              if (cursor != null) {
                  cursor.onChange(false);
              }
          }
      }
  }
  