UI for memory and functionality for M+ and M-

Bug: 31686717
Test: manual - long-press on result and formula and proper context
menus appear, as well as on results in history.

Change-Id: I88b25ed9d0402e03b420ab984a6b086dff6859e5
diff --git a/res/layout/pad_operator_two_col.xml b/res/layout/pad_operator_two_col.xml
index 0d9308d..e056ba5 100644
--- a/res/layout/pad_operator_two_col.xml
+++ b/res/layout/pad_operator_two_col.xml
@@ -58,14 +58,6 @@
         app:layout_column="0" />
 
     <Button
-        android:id="@+id/memory_store"
-        style="@style/PadButtonStyle.Operator.Text"
-        android:contentDescription="@string/desc_memory_store"
-        android:text="@string/memory_store"
-        app:layout_row="1"
-        app:layout_column="1" />
-
-    <Button
         android:id="@+id/op_sub"
         style="@style/PadButtonStyle.Operator"
         android:contentDescription="@string/desc_op_sub"
@@ -74,14 +66,6 @@
         app:layout_column="0" />
 
     <Button
-        android:id="@+id/memory_recall"
-        style="@style/PadButtonStyle.Operator.Text"
-        android:contentDescription="@string/desc_memory_recall"
-        android:text="@string/memory_recall"
-        app:layout_row="2"
-        app:layout_column="1" />
-
-    <Button
         android:id="@+id/op_add"
         style="@style/PadButtonStyle.Operator"
         android:contentDescription="@string/desc_op_add"
diff --git a/res/menu/copy.xml b/res/menu/copy.xml
deleted file mode 100644
index 5897f88..0000000
--- a/res/menu/copy.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright (C) 2011, 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.
- */
--->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item android:id="@+id/menu_copy"
-        android:title="@android:string/copy"/>
-
-</menu>
diff --git a/res/menu/paste.xml b/res/menu/menu_formula.xml
similarity index 84%
rename from res/menu/paste.xml
rename to res/menu/menu_formula.xml
index 964be0d..8882c8a 100644
--- a/res/menu/paste.xml
+++ b/res/menu/menu_formula.xml
@@ -19,7 +19,10 @@
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
+    <item android:id="@+id/memory_recall"
+          android:title="@string/memory_recall"/>
+
     <item android:id="@+id/menu_paste"
-        android:title="@android:string/paste"/>
+          android:title="@android:string/paste"/>
 
 </menu>
diff --git a/res/menu/paste.xml b/res/menu/menu_result.xml
similarity index 65%
copy from res/menu/paste.xml
copy to res/menu/menu_result.xml
index 964be0d..15e76cd 100644
--- a/res/menu/paste.xml
+++ b/res/menu/menu_result.xml
@@ -19,7 +19,20 @@
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <item android:id="@+id/menu_paste"
-        android:title="@android:string/paste"/>
+    <item
+        android:id="@+id/memory_store"
+        android:title="@string/memory_store" />
 
-</menu>
+    <item
+        android:id="@+id/memory_add"
+        android:title="@string/memory_add" />
+
+    <item
+        android:id="@+id/memory_subtract"
+        android:title="@string/memory_subtract" />
+
+    <item
+        android:id="@+id/menu_copy"
+        android:title="@android:string/copy" />
+
+</menu>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 24ef68c..e4345cd 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -127,10 +127,21 @@
     <!-- Toggle button to show/hide inverse functions. [CHAR_LIMIT=4] -->
     <string name="inv" translatable="false">inv</string>
 
-    <!-- Memory store button. [CHAR_LIMIT=2] -->
-    <string name="memory_store">MS</string>
-    <!-- Memory recall button. [CHAR_LIMIT=2] -->
-    <string name="memory_recall">MR</string>
+    <!--
+      Item on Formula context menu used to paste from the Memory into the edit field. [CHAR_LIMIT=2]
+      -->
+    <string name="memory_recall" translatable="false">MR</string>
+    <!-- Item on Result context menu used to store the result in memory. [CHAR_LIMIT=2]
+      -->
+    <string name="memory_store" translatable="false">MS</string>
+    <!-- Item on Result context menu, which subtracts the current result from the number in memory.
+      [CHAR_LIMIT=2]
+      -->
+    <string name="memory_subtract" translatable="false">M-</string>
+    <!-- Item on Result context menu, which adds the current result to the number in memory.
+      [CHAR_LIMIT=2]
+      -->
+    <string name="memory_add" translatable="false">M+</string>
 
     <!-- Content description for 'e' button. [CHAR_LIMIT=NONE] -->
     <string name="desc_const_e">Euler\'s number</string>
@@ -218,11 +229,6 @@
     <!-- Content description for "inv" button to hide inverse functions. [CHAR_LIMIT=NONE] -->
     <string name="desc_inv_on">hide inverse functions</string>
 
-    <!-- Content description for memory store button. [CHAR_LIMIT=NONE] -->
-    <string name="desc_memory_store">memory store</string>
-    <!-- Content description for memory recall button. [CHAR_LIMIT=NONE] -->
-    <string name="desc_memory_recall">memory recall</string>
-
     <!-- Content description for formula field when it is empty. [CHAR_LIMIT=NONE] -->
     <string name="desc_formula">No formula</string>
     <!-- Content description for result field when it is empty. [CHAR_LIMIT=NONE] -->
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index f41d87e..ea1d445 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -80,8 +80,10 @@
 import java.io.ObjectOutput;
 import java.io.ObjectOutputStream;
 
+import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
+
 public class Calculator extends Activity
-        implements OnTextSizeChangeListener, OnLongClickListener, CalculatorFormula.OnPasteListener,
+        implements OnTextSizeChangeListener, OnLongClickListener,
         AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */ {
 
     /**
@@ -158,6 +160,48 @@
         }
     };
 
+    public final OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener =
+            new OnDisplayMemoryOperationsListener() {
+        @Override
+        public boolean shouldDisplayMemory() {
+            return mEvaluator.getMemoryIndex() != 0;
+        }
+    };
+
+    public final OnFormulaContextMenuClickListener mOnFormulaContextMenuClickListener =
+            new OnFormulaContextMenuClickListener() {
+        @Override
+        public boolean onPaste(ClipData clip) {
+            final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
+            if (item == null) {
+                // nothing to paste, bail early...
+                return false;
+            }
+
+            // Check if the item is a previously copied result, otherwise paste as raw text.
+            final Uri uri = item.getUri();
+            if (uri != null && mEvaluator.isLastSaved(uri)) {
+                clearIfNotInputState();
+                mEvaluator.appendExpr(mEvaluator.getSavedIndex());
+                redisplayAfterFormulaChange();
+            } else {
+                addChars(item.coerceToText(Calculator.this).toString(), false);
+            }
+            return true;
+        }
+
+        @Override
+        public void onMemoryRecall() {
+            clearIfNotInputState();
+            long memoryIndex = mEvaluator.getMemoryIndex();
+            if (memoryIndex != 0) {
+                mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
+                redisplayAfterFormulaChange();
+            }  // FIXME: Avoid the 0 case, e.g. by graying out button when memory is unavailable.
+        }
+    };
+
+
     private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
         @Override
         public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
@@ -340,8 +384,10 @@
             mEvaluator.clearMain();
         }
 
+        mFormulaText.setOnContextMenuClickListener(mOnFormulaContextMenuClickListener);
+        mFormulaText.setOnDisplayMemoryOperationsListener(mOnDisplayMemoryOperationsListener);
+
         mFormulaText.setOnTextSizeChangeListener(this);
-        mFormulaText.setOnPasteListener(this);
         mFormulaText.addTextChangedListener(mFormulaTextWatcher);
         mDeleteButton.setOnLongClickListener(this);
 
@@ -765,12 +811,6 @@
                     mEvaluator.evaluateAndNotify(mEvaluator.MAIN_INDEX, this, mResultText);
                 }
                 return;
-            case R.id.memory_store:
-                mResultText.onMemoryStore();
-                return;
-            case R.id.memory_recall:
-                onMemoryRecall();
-                return;
             default:
                 cancelIfEvaluating(false);
                 if (haveUnprocessed()) {
@@ -920,15 +960,6 @@
         redisplayAfterFormulaChange();
     }
 
-    private void onMemoryRecall() {
-        clearIfNotInputState();
-        long memoryIndex = mEvaluator.getMemoryIndex();
-        if (memoryIndex != 0) {
-            mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
-            redisplayAfterFormulaChange();
-        }  // FIXME: Avoid the 0 case, e.g. by graying out button when memory is unavailable.
-    }
-
     private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
         final ViewGroupOverlay groupOverlay =
                 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
@@ -1319,26 +1350,6 @@
         return mHitRect.contains((int) event.getX(), (int) event.getY());
     }
 
-    @Override
-    public boolean onPaste(ClipData clip) {
-        final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
-        if (item == null) {
-            // nothing to paste, bail early...
-            return false;
-        }
-
-        // Check if the item is a previously copied result, otherwise paste as raw text.
-        final Uri uri = item.getUri();
-        if (uri != null && mEvaluator.isLastSaved(uri)) {
-            clearIfNotInputState();
-            mEvaluator.appendExpr(mEvaluator.getSavedIndex());
-            redisplayAfterFormulaChange();
-        } else {
-            addChars(item.coerceToText(this).toString(), false);
-        }
-        return true;
-    }
-
     /**
      * Clean up animation for context menu.
      */
@@ -1346,4 +1357,8 @@
     public void onContextMenuClosed(Menu menu) {
         stopActionModeOrContextMenu();
     }
+
+    public interface OnDisplayMemoryOperationsListener {
+        boolean shouldDisplayMemory();
+    }
 }
diff --git a/src/com/android/calculator2/CalculatorFormula.java b/src/com/android/calculator2/CalculatorFormula.java
index 8a7e4c5..210372c 100644
--- a/src/com/android/calculator2/CalculatorFormula.java
+++ b/src/com/android/calculator2/CalculatorFormula.java
@@ -58,8 +58,9 @@
     private ActionMode mActionMode;
     private ActionMode.Callback mPasteActionModeCallback;
     private ContextMenu mContextMenu;
-    private OnPasteListener mOnPasteListener;
     private OnTextSizeChangeListener mOnTextSizeChangeListener;
+    private OnFormulaContextMenuClickListener mOnContextMenuClickListener;
+    private Calculator.OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener;
 
     public CalculatorFormula(Context context) {
         this(context, null /* attrs */);
@@ -243,8 +244,13 @@
         mOnTextSizeChangeListener = listener;
     }
 
-    public void setOnPasteListener(OnPasteListener listener) {
-        mOnPasteListener = listener;
+    public void setOnContextMenuClickListener(OnFormulaContextMenuClickListener listener) {
+        mOnContextMenuClickListener = listener;
+    }
+
+    public void setOnDisplayMemoryOperationsListener(
+            Calculator.OnDisplayMemoryOperationsListener listener) {
+        mOnDisplayMemoryOperationsListener = listener;
     }
 
     /**
@@ -268,7 +274,7 @@
             public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                 mode.setTag(TAG_ACTION_MODE);
                 final MenuInflater inflater = mode.getMenuInflater();
-                return createPasteMenu(inflater, menu);
+                return createContextMenu(inflater, menu);
             }
 
             @Override
@@ -309,7 +315,7 @@
             public void onCreateContextMenu(ContextMenu contextMenu, View view,
                     ContextMenu.ContextMenuInfo contextMenuInfo) {
                 final MenuInflater inflater = new MenuInflater(getContext());
-                createPasteMenu(inflater, contextMenu);
+                createContextMenu(inflater, contextMenu);
                 mContextMenu = contextMenu;
                 for(int i = 0; i < contextMenu.size(); i++) {
                     contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorFormula.this);
@@ -324,41 +330,52 @@
         });
     }
 
-    private boolean createPasteMenu(MenuInflater inflater, Menu menu) {
+    private boolean createContextMenu(MenuInflater inflater, Menu menu) {
         final ClipboardManager clipboard = (ClipboardManager) getContext()
                 .getSystemService(Context.CLIPBOARD_SERVICE);
-        if (clipboard.hasPrimaryClip()) {
-            bringPointIntoView(length());
-            inflater.inflate(R.menu.paste, menu);
-            return true;
+        final boolean isPasteEnabled = clipboard.hasPrimaryClip();
+        final boolean isMemoryEnabled = isMemoryEnabled();
+        if (!isPasteEnabled && !isMemoryEnabled) {
+            return false;
         }
-        // Prevents the selection action mode on double tap.
-        return false;
+
+        bringPointIntoView(length());
+        inflater.inflate(R.menu.menu_formula, menu);
+        final MenuItem pasteItem = menu.findItem(R.id.menu_paste);
+        final MenuItem memoryRecallItem = menu.findItem(R.id.memory_recall);
+        pasteItem.setEnabled(isPasteEnabled);
+        memoryRecallItem.setEnabled(isMemoryEnabled);
+        return true;
     }
 
     private void paste() {
         final ClipboardManager clipboard = (ClipboardManager) getContext()
                 .getSystemService(Context.CLIPBOARD_SERVICE);
         final ClipData primaryClip = clipboard.getPrimaryClip();
-        if (primaryClip != null && mOnPasteListener != null) {
-            mOnPasteListener.onPaste(primaryClip);
+        if (primaryClip != null && mOnContextMenuClickListener != null) {
+            mOnContextMenuClickListener.onPaste(primaryClip);
         }
     }
 
     @Override
     public boolean onMenuItemClick(MenuItem item) {
-        if (item.getItemId() == R.id.menu_paste) {
-            paste();
-            return true;
+        switch (item.getItemId()) {
+            case R.id.memory_recall:
+                mOnContextMenuClickListener.onMemoryRecall();
+                return true;
+            case R.id.menu_paste:
+                paste();
+                return true;
+            default:
+                return false;
         }
-        return false;
     }
 
     @Override
     public void onPrimaryClipChanged() {
         final ClipData clip = mClipboardManager.getPrimaryClip();
         if (clip == null || clip.getItemCount() == 0) {
-            setLongClickable(false);
+            setLongClickable(isMemoryEnabled());
             return;
         }
         CharSequence clipText = null;
@@ -367,14 +384,20 @@
         } catch (Exception e) {
             Log.i("Calculator", "Error reading clipboard:", e);
         }
-        setLongClickable(!TextUtils.isEmpty(clipText));
+        setLongClickable(!TextUtils.isEmpty(clipText) || isMemoryEnabled());
+    }
+
+    private boolean isMemoryEnabled() {
+        return !(mOnDisplayMemoryOperationsListener == null || mOnContextMenuClickListener == null)
+                && mOnDisplayMemoryOperationsListener.shouldDisplayMemory();
     }
 
     public interface OnTextSizeChangeListener {
         void onTextSizeChanged(TextView textView, float oldSize);
     }
 
-    public interface OnPasteListener {
+    public interface OnFormulaContextMenuClickListener {
         boolean onPaste(ClipData clip);
+        void onMemoryRecall();
     }
 }
diff --git a/src/com/android/calculator2/CalculatorResult.java b/src/com/android/calculator2/CalculatorResult.java
index e001bee..0b399c4 100644
--- a/src/com/android/calculator2/CalculatorResult.java
+++ b/src/com/android/calculator2/CalculatorResult.java
@@ -410,6 +410,20 @@
     }
 
     /**
+     * Add the result to the value currently in memory.
+     */
+    public void onMemoryAdd() {
+        mEvaluator.addToMemory(Evaluator.MAIN_INDEX);
+    }
+
+    /**
+     * Subtract the result from the value currently in memory.
+     */
+    public void onMemorySubtract() {
+        mEvaluator.subtractFromMemory(Evaluator.MAIN_INDEX);
+    }
+
+    /**
      * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
      * scrollable, based on the supplied information about the result.
      * This is unfortunately complicated because we need to predict whether trailing digits
@@ -920,7 +934,7 @@
     }
 
     /**
-     * Use ActionMode for copy support on M and higher.
+     * Use ActionMode for copy/memory support on M and higher.
      */
     @TargetApi(Build.VERSION_CODES.M)
     private void setupActionMode() {
@@ -995,7 +1009,7 @@
     }
 
     /**
-     * Use ContextMenu for copy support on L and lower.
+     * Use ContextMenu for copy/memory support on L and lower.
      */
     private void setupContextMenu() {
         setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
@@ -1022,7 +1036,12 @@
     }
 
     private boolean createCopyMenu(MenuInflater inflater, Menu menu) {
-        inflater.inflate(R.menu.copy, menu);
+        inflater.inflate(R.menu.menu_result, menu);
+        final boolean displayMemory = mEvaluator.getMemoryIndex() != 0;
+        final MenuItem memoryAddItem = menu.findItem(R.id.memory_add);
+        final MenuItem memorySubtractItem = menu.findItem(R.id.memory_subtract);
+        memoryAddItem.setEnabled(displayMemory);
+        memorySubtractItem.setEnabled(displayMemory);
         highlightResult();
         return true;
     }
@@ -1072,6 +1091,15 @@
     @Override
     public boolean onMenuItemClick(MenuItem item) {
         switch (item.getItemId()) {
+            case R.id.memory_add:
+                onMemoryAdd();
+                return true;
+            case R.id.memory_subtract:
+                onMemorySubtract();
+                return true;
+            case R.id.memory_store:
+                onMemoryStore();
+                return true;
             case R.id.menu_copy:
                 if (mEvaluator.evaluationInProgress(mIndex)) {
                     // Refuse to copy placeholder characters.
@@ -1085,4 +1113,4 @@
                 return false;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index 82a4d86..77c762d 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -1362,6 +1362,22 @@
     }
 
     /**
+     * Return an ExprInfo corresponding to the subtraction of the value at the subtrahend index
+     * from value at the minuend index (minuend - subtrahend = result). Both are presumed to have
+     * been previously evaluated. The result is unevaluated.
+     */
+    private ExprInfo difference(long minuendIndex, long subtrahendIndex) {
+        final CalculatorExpr resultExpr = new CalculatorExpr();
+        resultExpr.append(getCollapsedExpr(minuendIndex));
+        resultExpr.add(R.id.op_sub);
+        resultExpr.append(getCollapsedExpr(subtrahendIndex));
+        final ExprInfo result = new ExprInfo(resultExpr, false /* angular measure irrelevant */);
+        result.mLongTimeout = mExprs.get(minuendIndex).mLongTimeout
+                || mExprs.get(subtrahendIndex).mLongTimeout;
+        return result;
+    }
+
+    /**
      * Add the expression described by the argument to the database.
      * Returns the new row id in the database.
      * If in_history is true, add it with a positive index, so it will appear in the history.
@@ -1576,6 +1592,17 @@
     }
 
     /**
+     * Save an an expression representing the subtraction of the expression with the given index
+     * from "memory." Make mMemoryIndex point to it when we complete evaluating.
+     */
+    public void subtractFromMemory(long index) {
+        ExprInfo newEi = difference(mMemoryIndex, index);
+        long newIndex = addToDB(false, newEi);
+        mMemoryIndex = 0;  // Invalidate while we're evaluating.
+        setMemoryIndexWhenEvaluated(newIndex, true /* persist */);
+    }
+
+    /**
      * Return index of "saved" expression, or 0.
      */
     public long getSavedIndex() {