add research log ui control

- lets users flag a particular time in the research log
- lets users delete the log for this session

also makes the UsabilityLog setting control whether the ResearchLog logs or not.

multi-project commit with I89067e7d3b8daca7179333f1dbe82224c26920fe

Bug: 6188932
Change-Id: I89864ef3ab53b0efe1ea8d75247be08712f0c399
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index d51d378..d663b00 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -26,6 +26,8 @@
     <string name="english_ime_settings">Android keyboard settings</string>
     <!-- Title for Latin keyboard input options dialog [CHAR LIMIT=25] -->
     <string name="english_ime_input_options">Input options</string>
+    <!-- Title for Latin keyboard research log dialog, which contains special commands for users that contribute data for research. [CHAR LIMIT=25] -->
+    <string name="english_ime_research_log">Research Log Commands</string>
 
     <!-- Name of Android spell checker service -->
     <string name="spell_checker_service_name">Android spell checker</string>
@@ -233,6 +235,20 @@
     <!-- Title for input language selection screen -->
     <string name="language_selection_title">Input languages</string>
 
+    <!-- Title for dialog option that lets user mark a particular time in the log for later review by experts [CHAR LIMIT=25] -->
+    <string name="note_timestamp_for_researchlog">Note timestamp in log</string>
+    <!-- Toast notification message that the time has been marked for later review. [CHAR LIMIT=25] -->
+    <string name="notify_recorded_timestamp">Recorded timestamp</string>
+
+    <!-- Title for dialog option to let users cancel logging and delete log for this session [CHAR LIMIT=25] -->
+    <string name="do_not_log_this_session">Do not log this session</string>
+    <!-- Toast notification that the system is processing the request to delete the log for this session [CHAR LIMIT=25] -->
+    <string name="notify_session_log_deleting">Deleting session log</string>
+    <!-- Toast notification that the system has successfully deleted the log for this session [CHAR LIMIT=25] -->
+    <string name="notify_session_log_deleted">Session log deleted</string>
+    <!-- Toast notification that the system has failed to delete the log for this session [CHAR LIMIT=25] -->
+    <string name="notify_session_log_not_deleted">Session log NOT deleted</string>
+
     <!-- Preference for input language selection -->
     <string name="select_language">Input languages</string>
 
diff --git a/java/res/xml/key_styles_common.xml b/java/res/xml/key_styles_common.xml
index 819cdc6..7db1d29 100644
--- a/java/res/xml/key_styles_common.xml
+++ b/java/res/xml/key_styles_common.xml
@@ -22,23 +22,8 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
     <!-- Base key style for the key which may have settings or tab key as popup key. -->
-    <switch>
-        <case
-            latin:clobberSettingsKey="true"
-        >
-            <key-style
-                latin:styleName="f1MoreKeysStyle"
-                latin:backgroundType="functional" />
-        </case>
-        <!-- clobberSettingsKey="false" -->
-        <default>
-            <key-style
-                latin:styleName="f1MoreKeysStyle"
-                latin:keyLabelFlags="hasPopupHint"
-                latin:moreKeys="!text/settings_as_more_key"
-                latin:backgroundType="functional" />
-        </default>
-    </switch>
+    <include
+        latin:keyboardLayout="@xml/key_styles_f1" />
     <!-- Functional key styles -->
     <switch>
         <case
diff --git a/java/res/xml/key_styles_f1.xml b/java/res/xml/key_styles_f1.xml
new file mode 100644
index 0000000..8dfc3cb
--- /dev/null
+++ b/java/res/xml/key_styles_f1.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, 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.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <!-- Base key style for the key which may have settings or tab key as popup key. -->
+    <!-- Kept as a separate file for cleaner overriding by an overlay.  -->
+    <switch>
+        <case
+            latin:clobberSettingsKey="true"
+        >
+            <key-style
+                latin:styleName="f1MoreKeysStyle"
+                latin:backgroundType="functional" />
+        </case>
+        <!-- clobberSettingsKey="false" -->
+        <default>
+            <key-style
+                latin:styleName="f1MoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint"
+                latin:moreKeys="!text/settings_as_more_key"
+                latin:backgroundType="functional" />
+        </default>
+    </switch>
+</merge>
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 0be4cf3..43dbece 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -89,7 +89,8 @@
     private static final int MINIMUM_LETTER_CODE = CODE_TAB;
 
     /** Special keys code. Must be negative.
-     * These should be aligned with values/keycodes.xml
+     * These should be aligned with KeyboardCodesSet.ID_TO_NAME[],
+     * KeyboardCodesSet.DEFAULT[] and KeyboardCodesSet.RTL[]
      */
     public static final int CODE_SHIFT = -1;
     public static final int CODE_SWITCH_ALPHA_SYMBOL = -2;
@@ -101,8 +102,9 @@
     public static final int CODE_ACTION_NEXT = -8;
     public static final int CODE_ACTION_PREVIOUS = -9;
     public static final int CODE_LANGUAGE_SWITCH = -10;
+    public static final int CODE_RESEARCH = -11;
     // Code value representing the code is not specified.
-    public static final int CODE_UNSPECIFIED = -11;
+    public static final int CODE_UNSPECIFIED = -12;
 
     public final KeyboardId mId;
     public final int mThemeId;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
index 67cb74f..f7981a3 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
@@ -52,6 +52,7 @@
         "key_action_next",
         "key_action_previous",
         "key_language_switch",
+        "key_research",
         "key_unspecified",
         "key_left_parenthesis",
         "key_right_parenthesis",
@@ -86,6 +87,7 @@
         Keyboard.CODE_ACTION_NEXT,
         Keyboard.CODE_ACTION_PREVIOUS,
         Keyboard.CODE_LANGUAGE_SWITCH,
+        Keyboard.CODE_RESEARCH,
         Keyboard.CODE_UNSPECIFIED,
         CODE_LEFT_PARENTHESIS,
         CODE_RIGHT_PARENTHESIS,
@@ -112,6 +114,7 @@
         DEFAULT[11],
         DEFAULT[12],
         DEFAULT[13],
+        DEFAULT[14],
         CODE_RIGHT_PARENTHESIS,
         CODE_LEFT_PARENTHESIS,
         CODE_GREATER_THAN_SIGN,
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 98cf76c..69b044e 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1330,6 +1330,11 @@
         case Keyboard.CODE_LANGUAGE_SWITCH:
             handleLanguageSwitchKey();
             break;
+        case Keyboard.CODE_RESEARCH:
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.getInstance().presentResearchDialog(this);
+            }
+            break;
         default:
             if (primaryCode == Keyboard.CODE_TAB
                     && mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT) {
@@ -2444,10 +2449,10 @@
         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
                 .setItems(items, listener)
                 .setTitle(title);
-        showOptionDialogInternal(builder.create());
+        showOptionDialog(builder.create());
     }
 
-    private void showOptionDialogInternal(AlertDialog dialog) {
+    /* package */ void showOptionDialog(AlertDialog dialog) {
         final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken();
         if (windowToken == null) return;
 
diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java
index 1628509..bb003f7 100644
--- a/java/src/com/android/inputmethod/latin/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/latin/ResearchLogger.java
@@ -18,6 +18,8 @@
 
 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
 
+import android.app.AlertDialog;
+import android.content.DialogInterface;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
 import android.inputmethodservice.InputMethodService;
@@ -33,9 +35,9 @@
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
+import android.widget.Toast;
 
 import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardId;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
@@ -134,12 +136,16 @@
         }
         if (prefs != null) {
             sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
-            prefs.registerOnSharedPreferenceChangeListener(sInstance);
+            prefs.registerOnSharedPreferenceChangeListener(this);
         }
     }
 
     public synchronized void start() {
         Log.d(TAG, "start called");
+        if (!sIsLogging) {
+            // Log.w(TAG, "not in usability mode; not logging");
+            return;
+        }
         if (mFilesDir == null || !mFilesDir.exists()) {
             Log.w(TAG, "IME storage directory does not exist.  Cannot start logging.");
         } else {
@@ -192,16 +198,17 @@
                         e1.printStackTrace();
                     } catch (IOException e) {
                         e.printStackTrace();
-                    }
-                    mJsonWriter = NULL_JSON_WRITER;
-                    mFile = null;
-                    mLoggingState = LOGGING_STATE_OFF;
-                    if (DEBUG) {
-                        Log.d(TAG, "logfile closed");
-                    }
-                    Log.d(TAG, "finished stop(), notifying");
-                    synchronized (ResearchLogger.this) {
-                        ResearchLogger.this.notify();
+                    } finally {
+                        mJsonWriter = NULL_JSON_WRITER;
+                        mFile = null;
+                        mLoggingState = LOGGING_STATE_OFF;
+                        if (DEBUG) {
+                            Log.d(TAG, "logfile closed");
+                        }
+                        Log.d(TAG, "finished stop(), notifying");
+                        synchronized (ResearchLogger.this) {
+                            ResearchLogger.this.notify();
+                        }
                     }
                 }
             });
@@ -213,6 +220,38 @@
         }
     }
 
+    public synchronized boolean abort() {
+        Log.d(TAG, "abort called");
+        boolean isLogFileDeleted = false;
+        if (mLoggingHandler != null && mLoggingState == LOGGING_STATE_ON) {
+            mLoggingState = LOGGING_STATE_STOPPING;
+            try {
+                Log.d(TAG, "closing jsonwriter");
+                mJsonWriter.endArray();
+                mJsonWriter.close();
+            } catch (IllegalStateException e1) {
+                // assume that this is just the json not being terminated properly.
+                // ignore
+                e1.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            } finally {
+                mJsonWriter = NULL_JSON_WRITER;
+                // delete file
+                final boolean isDeleted = mFile.delete();
+                if (isDeleted) {
+                    isLogFileDeleted = true;
+                }
+                mFile = null;
+                mLoggingState = LOGGING_STATE_OFF;
+                if (DEBUG) {
+                    Log.d(TAG, "logfile closed");
+                }
+            }
+        }
+        return isLogFileDeleted;
+    }
+
     /* package */ synchronized void flush() {
         try {
             mJsonWriter.flush();
@@ -227,6 +266,50 @@
             return;
         }
         sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
+        if (sIsLogging == false) {
+            abort();
+        }
+    }
+
+    /* package */ void presentResearchDialog(final LatinIME latinIME) {
+        final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
+        final CharSequence[] items = new CharSequence[] {
+                latinIME.getString(R.string.note_timestamp_for_researchlog),
+                latinIME.getString(R.string.do_not_log_this_session),
+        };
+        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface di, int position) {
+                di.dismiss();
+                switch (position) {
+                    case 0:
+                        ResearchLogger.getInstance().userTimestamp();
+                        Toast.makeText(latinIME, R.string.notify_recorded_timestamp,
+                                Toast.LENGTH_LONG).show();
+                        break;
+                    case 1:
+                        Toast toast = Toast.makeText(latinIME,
+                                R.string.notify_session_log_deleting, Toast.LENGTH_LONG);
+                        toast.show();
+                        final ResearchLogger logger = ResearchLogger.getInstance();
+                        boolean isLogDeleted = logger.abort();
+                        toast.cancel();
+                        if (isLogDeleted) {
+                            Toast.makeText(latinIME, R.string.notify_session_log_deleted,
+                                    Toast.LENGTH_LONG).show();
+                        } else {
+                            Toast.makeText(latinIME,
+                                    R.string.notify_session_log_not_deleted, Toast.LENGTH_LONG)
+                                    .show();
+                        }
+                        break;
+                }
+            }
+        };
+        final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
+                .setItems(items, listener)
+                .setTitle(title);
+        latinIME.showOptionDialog(builder.create());
     }
 
     private static final String CURRENT_TIME_KEY = "_ct";
@@ -756,4 +839,11 @@
             getInstance().writeEvent(EVENTKEYS_SUGGESTIONSVIEW_SETSUGGESTIONS, values);
         }
     }
+
+    private static final String[] EVENTKEYS_USER_TIMESTAMP = {
+        "UserTimestamp"
+    };
+    public void userTimestamp() {
+        getInstance().writeEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES);
+    }
 }