Many fixes to detached mode

1. Unselect the top conversation in list (by un-setting activated
   state), before entering detached mode.
2. Set choice mode to NONE to avoid changes to the selected state in
   detached mode.
3. Allow for recovering from detached mode by tapping on a valid
   conversation in the list.
4. Bypass most of the action in CPA.notifyDataSetChanged() in detached
   mode.
5. Pop out of conversation view if there are no messages (when
   detached and the detached conversation is expunged).

Bug: 8026749 Detached mode triggered even when not detached

Bug: 8025487 In detached mode, CPA.notifyDataSetChanged() shouldn't do
             much

Bug: 7337160 Subject and message body mismatch

Change-Id: I7b578ab929ee6a7d18002567b1cf4aaac54b0480
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
index 28df1fc..6d0daec 100644
--- a/src/com/android/mail/browse/ConversationCursor.java
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -276,7 +276,7 @@
         }
 
         public Set<Long> conversationIds() {
-            return mConversationIdPositionMap.keySet()   ;
+            return mConversationIdPositionMap.keySet();
         }
 
         public int getPosition(long conversationId) {
@@ -378,11 +378,11 @@
     private void resetCursor(UnderlyingCursorWrapper newCursorWrapper) {
         synchronized (mCacheMapLock) {
             // Walk through the cache
-            final Iterator<HashMap.Entry<String, ContentValues>> iter =
+            final Iterator<Map.Entry<String, ContentValues>> iter =
                     mCacheMap.entrySet().iterator();
             final long now = System.currentTimeMillis();
             while (iter.hasNext()) {
-                HashMap.Entry<String, ContentValues> entry = iter.next();
+                Map.Entry<String, ContentValues> entry = iter.next();
                 final ContentValues values = entry.getValue();
                 final String key = entry.getKey();
                 boolean withinTimeWindow = false;
@@ -441,10 +441,10 @@
         synchronized (mCacheMapLock) {
             // Walk through the cache and return the list of uris that have been deleted
             final Set<String> deletedItems = Sets.newHashSet();
-            final Iterator<HashMap.Entry<String, ContentValues>> iter =
+            final Iterator<Map.Entry<String, ContentValues>> iter =
                     mCacheMap.entrySet().iterator();
             while (iter.hasNext()) {
-                final HashMap.Entry<String, ContentValues> entry = iter.next();
+                final Map.Entry<String, ContentValues> entry = iter.next();
                 final ContentValues values = entry.getValue();
                 if (values.containsKey(DELETED_COLUMN)) {
                     // Since clients of the conversation cursor see conversation ConversationCursor
@@ -472,10 +472,10 @@
         // position, decrement the position
         synchronized (mCacheMapLock) {
             int updatedPosition = underlyingPosition;
-            final Iterator<HashMap.Entry<String, ContentValues>> iter =
+            final Iterator<Map.Entry<String, ContentValues>> iter =
                     mCacheMap.entrySet().iterator();
             while (iter.hasNext()) {
-                final HashMap.Entry<String, ContentValues> entry = iter.next();
+                final Map.Entry<String, ContentValues> entry = iter.next();
                 final ContentValues values = entry.getValue();
                 if (values.containsKey(DELETED_COLUMN)) {
                     // Since clients of the conversation cursor see conversation ConversationCursor
diff --git a/src/com/android/mail/browse/ConversationPagerAdapter.java b/src/com/android/mail/browse/ConversationPagerAdapter.java
index 7512f93..a32b0a3 100644
--- a/src/com/android/mail/browse/ConversationPagerAdapter.java
+++ b/src/com/android/mail/browse/ConversationPagerAdapter.java
@@ -128,6 +128,11 @@
     }
 
     private ConversationCursor getCursor() {
+        if (mDetachedMode) {
+            // In detached mode, the pager is decoupled from the cursor. Nothing should rely on the
+            // cursor at this point.
+            return null;
+        }
         if (mController == null) {
             // Happens when someone calls setActivityController(null) on us. This is done in
             // ConversationPagerController.stopListening() to indicate that the Conversation View
@@ -277,6 +282,9 @@
             return;
         }
 
+        // If we are in detached mode, changes to the cursor are of no interest to us, but they may
+        // be to parent classes.
+
         // when the currently visible item disappears from the dataset:
         //   if the new version of the currently visible item has zero messages:
         //     notify the list controller so it can handle this 'current conversation gone' case
@@ -284,13 +292,14 @@
         //   else
         //     'detach' the conversation view from the cursor, keeping the current item as-is but
         //     disabling swipe (effectively the same as singleton mode)
-        if (mController != null) {
+        if (mController != null && !mDetachedMode) {
             final Conversation currConversation = mController.getCurrentConversation();
             final int pos = getConversationPosition(currConversation);
             if (pos == POSITION_NONE && getCursor() != null && currConversation != null) {
                 // enable detached mode and do no more here. the fragment itself will figure out
                 // if the conversation is empty (using message list cursor) and back out if needed.
                 mDetachedMode = true;
+                mController.setDetachedMode();
                 LogUtils.i(LOG_TAG, "CPA: current conv is gone, reverting to detached mode. c=%s",
                         currConversation.uri);
             } else {
@@ -331,10 +340,6 @@
 
     public int getConversationPosition(Conversation conv) {
         if (isPagingDisabled()) {
-            if (getCursor() == null) {
-                return POSITION_NONE;
-            }
-
             if (conv != getDefaultConversation()) {
                 LogUtils.d(LOG_TAG, "unable to find conversation in singleton mode. c=%s", conv);
                 return POSITION_NONE;
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index 96027e0..1e3886b 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -140,6 +140,8 @@
     private static final String SAVED_ACTION = "saved-action";
     /** Tag for {@link #mDialogFromSelectedSet} */
     private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected";
+    /** Tag for {@link #mDetachedConvUri} */
+    private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri";
 
     /** Tag  used when loading a wait fragment */
     protected static final String TAG_WAIT = "wait-fragment";
@@ -159,6 +161,10 @@
     protected final RecentFolderList mRecentFolderList;
     protected ConversationListContext mConvListContext;
     protected Conversation mCurrentConversation;
+    /**
+     * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode.
+     */
+    private Uri mDetachedConvUri;
 
     /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
     private SuppressNotificationReceiver mNewEmailReceiver = null;
@@ -842,9 +848,7 @@
             if (savedState.containsKey(SAVED_ACTION)) {
                 mDialogAction = savedState.getInt(SAVED_ACTION);
             }
-            if (savedState.containsKey(SAVED_ACTION_FROM_SELECTED)) {
-                mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED);
-            }
+            mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
             mViewMode.handleRestore(savedState);
         } else if (intent != null) {
             handleIntent(intent);
@@ -1429,6 +1433,9 @@
             outState.putInt(SAVED_ACTION, mDialogAction);
             outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
         }
+        if (mDetachedConvUri != null) {
+            outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
+        }
         mSafeToModifyFragments = false;
         outState.putString(SAVED_HIERARCHICAL_FOLDER,
                 (mFolderListFolder != null) ? Folder.toString(mFolderListFolder) : null);
@@ -1563,7 +1570,7 @@
      */
     @Override
     public void onRestoreInstanceState(Bundle savedState) {
-        LogUtils.d(LOG_TAG, "IN AAC.onRestoreInstanceState");
+        mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
         if (savedState.containsKey(SAVED_CONVERSATION)) {
             // Open the conversation.
             final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
@@ -1822,7 +1829,14 @@
      */
     @Override
     public void setCurrentConversation(Conversation conversation) {
-        // Must be the first call because this sets conversation.position if a cursor is available.
+        // The controller should come out of detached mode if a new conversation is viewed, or if
+        if (conversation == null || (mDetachedConvUri != null
+                && !mDetachedConvUri.equals(conversation.uri))) {
+            clearDetachedMode();
+        }
+
+        // Must happen *before* setting mCurrentConversation because this sets
+        // conversation.position if a cursor is available.
         mTracker.initialize(conversation);
         mCurrentConversation = conversation;
 
@@ -3218,7 +3232,7 @@
         // Clear the cache of objects.
         ConversationItemViewModel.onAccessibilityUpdated();
         // Re-render the list if it exists.
-        ConversationListFragment frag = getConversationListFragment();
+        final ConversationListFragment frag = getConversationListFragment();
         if (frag != null) {
             AnimatedAdapter adapter = frag.getAnimatedAdapter();
             if (adapter != null) {
@@ -3269,5 +3283,31 @@
     @Override
     public VeiledAddressMatcher getVeiledAddressMatcher() {
         return mVeiledMatcher;
-    };
+    }
+
+    @Override
+    public void setDetachedMode() {
+        // Tell the conversation list not to select anything.
+        final ConversationListFragment frag = getConversationListFragment();
+        if (frag != null) {
+            frag.setChoiceNone();
+        } else {
+            // How did we ever land here? Detached mode, and no CLF???
+            LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
+        }
+        mDetachedConvUri = mCurrentConversation.uri;
+    }
+
+    private void clearDetachedMode() {
+        // Tell the conversation list to go back to its usual selection behavior.
+        final ConversationListFragment frag = getConversationListFragment();
+        if (frag != null) {
+            frag.revertChoiceMode();
+        } else {
+            // How did we ever land here? Detached mode, and no CLF???
+            LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null!");
+        }
+        mDetachedConvUri = null;
+    }
+
 }
diff --git a/src/com/android/mail/ui/AbstractConversationViewFragment.java b/src/com/android/mail/ui/AbstractConversationViewFragment.java
index fd51d2f..74a3d9a 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -63,6 +63,7 @@
 import com.android.mail.providers.Folder;
 import com.android.mail.providers.ListParams;
 import com.android.mail.providers.UIProvider;
+import com.android.mail.providers.UIProvider.CursorStatus;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.Utils;
@@ -464,9 +465,9 @@
                     LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
                 }
 
-                // When the last cursor had message(s), and the new version has
-                // no messages, we need to exit conversation view.
-                if (messageCursor.getCount() == 0 && mCursor != null) {
+                // We have no messages: exit conversation view.
+                if (messageCursor.getCount() == 0
+                        && !CursorStatus.isWaitingForResults(messageCursor.getStatus())) {
                     if (mUserVisible) {
                         onError();
                     } else {
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index d456783..abb1dd6 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -127,10 +127,10 @@
     private static final String LOG_TAG = LogTag.getLogTag();
     private static final int INCREASE_WAIT_COUNT = 2;
 
-    public AnimatedAdapter(Context context, int textViewResourceId, ConversationCursor cursor,
-            ConversationSelectionSet batch,
-            ControllableActivity activity, SwipeableListView listView) {
-        super(context, textViewResourceId, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
+    public AnimatedAdapter(Context context, ConversationCursor cursor,
+            ConversationSelectionSet batch, ControllableActivity activity,
+            SwipeableListView listView) {
+        super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
         mContext = context;
         mBatchConversations = batch;
         setAccount(mAccountListener.initialize(activity.getAccountController()));
diff --git a/src/com/android/mail/ui/ConversationListCallbacks.java b/src/com/android/mail/ui/ConversationListCallbacks.java
index 974de55..40e75cb 100644
--- a/src/com/android/mail/ui/ConversationListCallbacks.java
+++ b/src/com/android/mail/ui/ConversationListCallbacks.java
@@ -73,4 +73,9 @@
      * Detect if there are any animations occuring in the conversation list.
      */
     boolean isAnimating();
+
+    /**
+     * Tell the controller that the conversation view has entered detached mode.
+     */
+    void setDetachedMode();
 }
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index fa9f595..070bd53 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -69,6 +69,8 @@
     private static final String LIST_STATE_KEY = "list-state";
 
     private static final String LOG_TAG = LogTag.getLogTag();
+    /** Key used to save the ListView choice mode, since ListView doesn't save it automatically! */
+    private static final String CHOICE_MODE_KEY = "choice-mode-key";
 
     // True if we are on a tablet device
     private static boolean mTabletDevice;
@@ -116,7 +118,7 @@
     private View mEmptyView;
     private ErrorListener mErrorListener;
     private DataSetObserver mFolderObserver;
-    private DataSetObserver mConversationListStatusObserver;
+    private DataSetObserver mConversationCursorObserver;
 
     private ConversationSelectionSet mSelectedSet;
     private final AccountObserver mAccountObserver = new AccountObserver() {
@@ -153,7 +155,7 @@
         }
     }
 
-    private class ConversationListStatusObserver extends DataSetObserver {
+    private class ConversationCursorObserver extends DataSetObserver {
         @Override
         public void onChanged() {
             onConversationListStatusUpdated();
@@ -262,7 +264,7 @@
                 null);
         mFooterView.setClickListener(mActivity);
         final ConversationCursor conversationCursor = getConversationListCursor();
-        mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), -1,
+        mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(),
                 conversationCursor, mActivity.getSelectedSet(), mActivity, mListView);
         mListAdapter.addFooter(mFooterView);
         mListView.setAdapter(mListAdapter);
@@ -271,9 +273,9 @@
         mListAdapter.hideFooter();
         mFolderObserver = new FolderObserver();
         mActivity.getFolderController().registerFolderObserver(mFolderObserver);
-        mConversationListStatusObserver = new ConversationListStatusObserver();
+        mConversationCursorObserver = new ConversationCursorObserver();
         mUpdater = mActivity.getConversationUpdater();
-        mUpdater.registerConversationListObserver(mConversationListStatusObserver);
+        mUpdater.registerConversationListObserver(mConversationCursorObserver);
         mTabletDevice = Utils.useTabletUI(mActivity.getApplicationContext().getResources());
         initializeUiForFirstDisplay();
         configureSearchResultHeader();
@@ -337,22 +339,61 @@
         mEmptyView = rootView.findViewById(R.id.empty_view);
         mListView = (SwipeableListView) rootView.findViewById(android.R.id.list);
         mListView.setHeaderDividersEnabled(false);
-        // Choice mode here represents the current conversation only. CAB mode does not rely on the
-        // platform: it is a local variable within conversation items.
-        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
         mListView.setOnItemLongClickListener(this);
         mListView.enableSwipe(mAccount.supportsCapability(AccountCapabilities.UNDO));
         mListView.setSwipedListener(this);
 
-        // Restore the list state
-        if (savedState != null && savedState.containsKey(LIST_STATE_KEY)) {
-            mListView.onRestoreInstanceState(savedState.getParcelable(LIST_STATE_KEY));
-            // TODO: find a better way to unset the selected item when restoring
-            mListView.clearChoices();
+        final int choiceMode;
+        if (savedState != null) {
+            // Restore the choice mode if it was set earlier, or SINGLE if creating a fresh view.
+            // Choice mode here represents the current conversation only. CAB mode does not rely on
+            // the platform: it is a local variable within conversation items.
+            choiceMode = savedState.getInt(CHOICE_MODE_KEY, ListView.CHOICE_MODE_SINGLE);
+            if (savedState.containsKey(LIST_STATE_KEY)) {
+                mListView.onRestoreInstanceState(savedState.getParcelable(LIST_STATE_KEY));
+                // TODO: find a better way to unset the selected item when restoring
+                mListView.clearChoices();
+            }
+        } else {
+            choiceMode = ListView.CHOICE_MODE_SINGLE;
         }
+        setChoiceMode(choiceMode);
         return rootView;
     }
 
+    /**
+     * Sets the choice mode of the list view
+     * @param choiceMode ListView#
+     */
+    private final void setChoiceMode(int choiceMode) {
+        mListView.setChoiceMode(choiceMode);
+    }
+
+    /**
+     * Tell the list to select nothing.
+     */
+    public final void setChoiceNone() {
+        final int currentSelected = mListView.getCheckedItemPosition();
+        mListView.clearChoices();
+        // We use the activated state to show the blue highlight on tablet. Clearing the choices
+        // removes the checked state, but doesn't do anything to the activated state.  We must
+        // manually clear that.
+        if (currentSelected != ListView.INVALID_POSITION) {
+            final View v = mListView.getChildAt(currentSelected);
+            if (v != null) {
+                v.setActivated(false);
+            }
+        }
+        setChoiceMode(ListView.CHOICE_MODE_NONE);
+    }
+
+    /**
+     * Tell the list to get out of selecting none.
+     */
+    public final void revertChoiceMode() {
+        setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+    }
+
     @Override
     public void onDestroy() {
         super.onDestroy();
@@ -370,9 +411,9 @@
             mActivity.getFolderController().unregisterFolderObserver(mFolderObserver);
             mFolderObserver = null;
         }
-        if (mConversationListStatusObserver != null) {
-            mUpdater.unregisterConversationListObserver(mConversationListStatusObserver);
-            mConversationListStatusObserver = null;
+        if (mConversationCursorObserver != null) {
+            mUpdater.unregisterConversationListObserver(mConversationCursorObserver);
+            mConversationCursorObserver = null;
         }
         mAccountObserver.unregisterAndDestroy();
         super.onDestroyView();
@@ -457,6 +498,7 @@
         super.onSaveInstanceState(outState);
         if (mListView != null) {
             outState.putParcelable(LIST_STATE_KEY, mListView.onSaveInstanceState());
+            outState.putInt(CHOICE_MODE_KEY, mListView.getChoiceMode());
         }
     }
 
@@ -510,7 +552,7 @@
      */
     protected void viewConversation(int position) {
         LogUtils.d(LOG_TAG, "ConversationListFragment.viewConversation(%d)", position);
-        setSelected(position);
+        setSelected(position, true);
         final ConversationCursor cursor = getConversationListCursor();
         if (cursor != null && cursor.moveToPosition(position)) {
             final Conversation conv = new Conversation(cursor);
@@ -519,8 +561,17 @@
         }
     }
 
-
+    /**
+     * Sets the selected conversation to the position given here.
+     * @param position
+     * @param different if the currently selected conversation is different from the one provided
+     * here.  This is a difference in conversations, not a difference in positions. For example, a
+     * conversation at position 2 can move to position 4 as a result of new mail.
+     */
     public void setSelected(int position, boolean different) {
+        if (mListView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
+            return;
+        }
         if (different) {
             mListView.smoothScrollToPosition(position);
         }
@@ -528,14 +579,9 @@
     }
 
     /**
-     * Sets the selected position (the highlighted conversation) to the position
-     * provided here.
-     * @param position
+     * Returns the cursor associated with the conversation list.
+     * @return
      */
-    protected final void setSelected(int position) {
-        setSelected(position, true);
-    }
-
     private ConversationCursor getConversationListCursor() {
         return mCallbacks != null ? mCallbacks.getConversationListCursor() : null;
     }
@@ -693,8 +739,9 @@
         if (conv == null) {
             return;
         }
-        if (mListView.getCheckedItemPosition() == -1) {
-            setSelected(conv.position);
+        if (mListView.getChoiceMode() != ListView.CHOICE_MODE_NONE
+                && mListView.getCheckedItemPosition() == -1) {
+            setSelected(conv.position, true);
         }
     }
 
diff --git a/src/com/android/mail/ui/ConversationPositionTracker.java b/src/com/android/mail/ui/ConversationPositionTracker.java
index 3d4f86f..46ca0a0 100644
--- a/src/com/android/mail/ui/ConversationPositionTracker.java
+++ b/src/com/android/mail/ui/ConversationPositionTracker.java
@@ -17,16 +17,12 @@
 
 package com.android.mail.ui;
 
-import android.database.Cursor;
-
 import com.android.mail.browse.ConversationCursor;
 import com.android.mail.providers.Conversation;
 import com.android.mail.providers.Settings;
 import com.android.mail.providers.UIProvider.AutoAdvance;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
-import com.android.mail.utils.Utils;
-
 import java.util.Collection;
 
 /**
@@ -132,7 +128,7 @@
     public void initialize(Conversation conversation) {
         mConversation = conversation;
         mCursorDirty = true;
-        calculatePosition();
+        calculatePosition(); // Return value discarded. Running for side effects.
     }
 
     /** @return whether or not we have a valid cursor to check the position of. */
@@ -179,7 +175,7 @@
             return invalidPosition;
         }
         mCursorDirty = false;
-        final int listSize = (cursor == null) ? 0 : cursor.getCount();
+        final int listSize = cursor.getCount();
         if (!isDataLoaded(cursor) || listSize == 0) {
             return invalidPosition;
         }
@@ -198,21 +194,24 @@
         // If the conversation is no longer found in the list, try to save the same position if
         // it is still a valid position. Otherwise, go back to a valid position until we can
         // find a valid one.
-        int newPosition = foundPosition;
-        if (mConversation.position >= listSize || foundPosition >= listSize) {
+        final int newPosition;
+        if (foundPosition >= listSize) {
             // Go to the last position since our expected position is past this somewhere.
-            newPosition = cursor.getCount() - 1;
+            newPosition = listSize - 1;
+        } else {
+            newPosition = foundPosition;
         }
 
-        // Did not keep the same conversation, but could still be a valid conversation.
-        if (isDataLoaded(cursor)){
+        // Did not keep the current conversation, so let's try to load the conversation from the
+        // new position.
+        if (isDataLoaded(cursor) && newPosition >= 0){
             LogUtils.d(LOG_TAG, "ConversationPositionTracker: Could not find conversation %s" +
                     " in the cursor. Moving to position %d ", mConversation.toString(),
                     newPosition);
             cursor.moveToPosition(newPosition);
             mConversation = new Conversation(cursor);
+            mConversation.position = newPosition;
         }
-
         return newPosition;
     }
 
@@ -221,10 +220,10 @@
      * conversations available in the folder. If no next conversation can be found, this method
      * returns null.
      * @param autoAdvance the auto advance preference for the user as an
-     * {@link Settings#autoAdvance} value.
+     * {@link Settings#getAutoAdvanceSetting()} value.
      * @param mTarget conversations to overlook while finding the next conversation. (These are
      * usually the conversations to be deleted.)
-     * @return
+     * @return the next conversation to be shown, or null if no next conversation exists.
      */
     public Conversation getNextConversation(int autoAdvance, Collection<Conversation> mTarget) {
         final boolean getNewer = autoAdvance == AutoAdvance.NEWER;