Merge "Work profile edu" into ub-launcher3-master
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index a1033f0..893d796 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -41,7 +41,7 @@
         android:paddingLeft="12dp"
         android:paddingRight="12dp" >
 
-        <com.android.launcher3.ExtendedEditText
+        <com.android.launcher3.folder.FolderNameEditText
             android:id="@+id/folder_name"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index 5b453c3..d64967b 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -21,15 +21,11 @@
 import android.view.DragEvent;
 import android.view.KeyEvent;
 import android.view.View;
-import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
 
-import com.android.launcher3.folder.FolderNameProvider;
 import com.android.launcher3.util.UiThreadHelper;
 
-import java.util.List;
-
 
 /**
  * The edit text that reports back when the back key has been pressed.
@@ -105,25 +101,6 @@
         UiThreadHelper.hideKeyboardAsync(getContext(), getWindowToken());
     }
 
-    @Override
-    public void onCommitCompletion(CompletionInfo text) {
-        setText(text.getText());
-        setSelection(text.getText().length());
-    }
-
-    /**
-     * Currently only used for folder name suggestion.
-     */
-    public void displayCompletions(List<String> suggestList) {
-        int cnt = Math.min(suggestList.size(), FolderNameProvider.SUGGEST_MAX);
-        CompletionInfo[] cInfo = new CompletionInfo[cnt];
-        for (int i = 0; i < cnt; i++) {
-            cInfo[i] = new CompletionInfo(i, i, suggestList.get(i));
-        }
-        post(() -> getContext().getSystemService(InputMethodManager.class)
-                .displayCompletions(this, cInfo));
-    }
-
     private boolean showSoftInput() {
         return requestFocus() &&
                 getContext().getSystemService(InputMethodManager.class)
diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java
index e2b7b68..0fea0dc 100644
--- a/src/com/android/launcher3/FolderInfo.java
+++ b/src/com/android/launcher3/FolderInfo.java
@@ -45,6 +45,8 @@
      */
     public static final int FLAG_MULTI_PAGE_ANIMATION = 0x00000004;
 
+    public static final int FLAG_MANUAL_FOLDER_NAME = 0x00000008;
+
     public int options;
 
     /**
@@ -140,4 +142,10 @@
             writer.updateItemInDatabase(this);
         }
     }
+
+    @Override
+    protected String dumpProperties() {
+        return super.dumpProperties()
+                + " manuallyTypedTitle=" + hasOption(FLAG_MANUAL_FOLDER_NAME);
+    }
 }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 844189f..90d8125 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -96,7 +96,7 @@
         View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener,
         View.OnFocusChangeListener, DragListener, ExtendedEditText.OnBackKeyListener {
     private static final String TAG = "Launcher.Folder";
-
+    private static final boolean DEBUG = false;
     /**
      * We avoid measuring {@link #mContent} with a 0 width or height, as this
      * results in CellLayout being measured as UNSPECIFIED, which it does not support.
@@ -146,7 +146,7 @@
     @Thunk FolderIcon mFolderIcon;
 
     @Thunk FolderPagedView mContent;
-    public ExtendedEditText mFolderName;
+    public FolderNameEditText mFolderName;
     private PageIndicatorDots mPageIndicator;
 
     protected View mFooter;
@@ -306,6 +306,7 @@
                     mFolderName.setText(suggestedNames[0]);
                     mFolderName.displayCompletions(Arrays.asList(suggestedNames).subList(1,
                             suggestedNames.length));
+                    mFolderName.setEnteredCompose(false);
                 }
             }
             mFolderName.setHint("");
@@ -318,7 +319,13 @@
         // Convert to a string here to ensure that no other state associated with the text field
         // gets saved.
         String newTitle = mFolderName.getText().toString();
+        if (DEBUG) {
+            Log.d(TAG, "onBackKey newTitle=" + newTitle);
+        }
+
         mInfo.title = newTitle;
+        mInfo.setOption(FolderInfo.FLAG_MANUAL_FOLDER_NAME, mFolderName.isEnteredCompose(),
+                mLauncher.getModelWriter());
         mFolderIcon.onTitleChanged(newTitle);
         mLauncher.getModelWriter().updateItemInDatabase(mInfo);
 
@@ -350,6 +357,10 @@
     }
 
     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (DEBUG) {
+            Log.d(TAG, "onEditorAction actionId=" + actionId + " key="
+                    + (event != null ? event.getKeyCode() : "null event"));
+        }
         if (actionId == EditorInfo.IME_ACTION_DONE) {
             mFolderName.dispatchBackKey();
             return true;
@@ -436,7 +447,8 @@
      */
     public void showSuggestedTitle(String[] suggestName) {
         if (FeatureFlags.FOLDER_NAME_SUGGEST.get()
-                && TextUtils.isEmpty(mFolderName.getText().toString())) {
+                && TextUtils.isEmpty(mFolderName.getText().toString())
+                && !mInfo.hasOption(FolderInfo.FLAG_MANUAL_FOLDER_NAME)) {
             if (suggestName.length > 0 && !TextUtils.isEmpty(suggestName[0])) {
                 mFolderName.setHint("");
                 mFolderName.setText(suggestName[0]);
@@ -552,9 +564,6 @@
             openFolder.close(true);
         }
 
-        if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-            mLauncher.getFolderNameProvider().load(getContext());
-        }
         mContent.bindItems(items);
         centerAboutIcon();
         mItemsInvalidated = true;
@@ -1495,6 +1504,9 @@
         return ContainerType.FOLDER;
     }
 
+    /**
+     * Navigation bar back key or hardware input back key has been issued.
+     */
     @Override
     public boolean onBackPressed() {
         if (isEditingName()) {
diff --git a/src/com/android/launcher3/folder/FolderNameEditText.java b/src/com/android/launcher3/folder/FolderNameEditText.java
new file mode 100644
index 0000000..7e3f847
--- /dev/null
+++ b/src/com/android/launcher3/folder/FolderNameEditText.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 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 com.android.launcher3.folder;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionWrapper;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.launcher3.ExtendedEditText;
+
+import java.util.List;
+
+/**
+ * Handles additional edit text functionality to better support folder name suggestion.
+ * First, makes suggestion to the InputMethodManager via {@link #displayCompletions(List)}
+ * Second, intercepts whether user accepted the suggestion or manually edited their
+ * folder names.
+ */
+public class FolderNameEditText extends ExtendedEditText {
+    private static final String TAG = "FolderNameEditText";
+    private static final boolean DEBUG = false;
+
+    private boolean mEnteredCompose = false;
+
+    public FolderNameEditText(Context context) {
+        super(context);
+    }
+
+    public FolderNameEditText(Context context, AttributeSet attrs) {
+        // ctor chaining breaks the touch handling
+        super(context, attrs);
+    }
+
+    public FolderNameEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        InputConnection con = super.onCreateInputConnection(outAttrs);
+        FolderNameEditTextInputConnection connectionWrapper =
+                new FolderNameEditTextInputConnection(con, true);
+        return connectionWrapper;
+    }
+
+    public void displayCompletions(List<String> suggestList) {
+        int cnt = Math.min(suggestList.size(), FolderNameProvider.SUGGEST_MAX);
+        CompletionInfo[] cInfo = new CompletionInfo[cnt];
+        for (int i = 0; i < cnt; i++) {
+            cInfo[i] = new CompletionInfo(i, i, suggestList.get(i));
+        }
+        post(() -> getContext().getSystemService(InputMethodManager.class)
+                .displayCompletions(this, cInfo));
+    }
+
+    /**
+     * Within 's', the 'count' characters beginning at 'start' have just replaced
+     * old text 'before'
+     */
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+        String reason = "unknown";
+        if (start == 0 && count == 0 && before > 0) {
+            reason = "suggestion was rejected";
+            mEnteredCompose = true;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "onTextChanged " + start + "," + before + "," + count
+                    + ", " + reason);
+        }
+    }
+
+    @Override
+    public void onCommitCompletion(CompletionInfo text) {
+        setText(text.getText());
+        setSelection(text.getText().length());
+        mEnteredCompose = false;
+    }
+
+    protected void setEnteredCompose(boolean value) {
+        mEnteredCompose = value;
+    }
+
+    protected boolean isEnteredCompose() {
+        if (DEBUG) {
+            Log.d(TAG, "isEnteredCompose " + mEnteredCompose);
+        }
+        return mEnteredCompose;
+    }
+
+    private class FolderNameEditTextInputConnection extends InputConnectionWrapper {
+
+        FolderNameEditTextInputConnection(InputConnection target, boolean mutable) {
+            super(target, mutable);
+        }
+
+        @Override
+        public boolean setComposingText(CharSequence cs, int newCursorPos) {
+            mEnteredCompose = true;
+            return super.setComposingText(cs, newCursorPos);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index 782b0e2..9ea292c 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -109,11 +109,14 @@
         if (contains(candidatesOut, candidate)) {
             return;
         }
+
         for (int i = 0; i < candidate.length(); i++) {
             if (TextUtils.isEmpty(candidatesOut[i])) {
                 candidatesOut[i] = candidate;
+                return;
             }
         }
+        candidatesOut[candidate.length() - 1] = candidate;
     }
 
     private boolean contains(CharSequence[] list, CharSequence key) {
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 0a3462f..5555eab 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -68,6 +68,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
+import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -93,10 +94,13 @@
     private static final String TAG = "Tapl";
     private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 20;
     private static final int GESTURE_STEP_MS = 16;
+    private static final SimpleDateFormat DATE_TIME_FORMAT =
+            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
     private static long START_TIME = System.currentTimeMillis();
 
     static final Pattern EVENT_LOG_ENTRY = Pattern.compile(
-            "[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9]"
+            "(?<time>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] "
+                    + "[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9])"
                     + ".*" + TestProtocol.TAPL_EVENTS_TAG + ": (?<event>.*)");
 
     private static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
@@ -167,7 +171,7 @@
     // Not null when we are collecting expected events to compare with actual ones.
     private List<Pattern> mExpectedEvents;
 
-    private String mTimeBeforeFirstLogEvent;
+    private Date mStartRecordingTime;
     private boolean mCheckEventsForSuccessfulGestures = false;
 
     private static Pattern getTouchEventPattern(String action) {
@@ -1187,32 +1191,38 @@
     private List<String> getEvents() {
         final ArrayList<String> events = new ArrayList<>();
         try {
-            final String logcatTimeParameter =
-                    mTimeBeforeFirstLogEvent != null ? " -t " + mTimeBeforeFirstLogEvent : "";
             final String logcatEvents = mDevice.executeShellCommand(
-                    "logcat -d --pid=" + getPid() + logcatTimeParameter
+                    "logcat -d -v year --pid=" + getPid() + " -t "
+                            + DATE_TIME_FORMAT.format(mStartRecordingTime).replaceAll(" ", "")
                             + " -s " + TestProtocol.TAPL_EVENTS_TAG);
             final Matcher matcher = EVENT_LOG_ENTRY.matcher(logcatEvents);
             while (matcher.find()) {
+                // Skip events before recording start time.
+                if (DATE_TIME_FORMAT.parse(matcher.group("time"))
+                        .compareTo(mStartRecordingTime) < 0) {
+                    continue;
+                }
+
                 events.add(matcher.group("event"));
             }
             return events;
         } catch (IOException e) {
             throw new RuntimeException(e);
+        } catch (ParseException e) {
+            throw new AssertionError(e);
         }
     }
 
     private void startRecordingEvents() {
         Assert.assertTrue("Already recording events", mExpectedEvents == null);
         mExpectedEvents = new ArrayList<>();
-        mTimeBeforeFirstLogEvent = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
-                .format(new Date())
-                .replaceAll(" ", "");
-        log("startRecordingEvents: " + mTimeBeforeFirstLogEvent);
+        mStartRecordingTime = new Date();
+        log("startRecordingEvents: " + DATE_TIME_FORMAT.format(mStartRecordingTime));
     }
 
     private void stopRecordingEvents() {
         mExpectedEvents = null;
+        mStartRecordingTime = null;
     }
 
     Closable eventsCheck() {