diff options
author | 2018-12-12 11:10:01 +0800 | |
---|---|---|
committer | 2018-12-21 18:14:13 +0800 | |
commit | f0ea0ed73a15a57136668519f7ed24df937e015c (patch) | |
tree | c24216ebe2240324df260fd2a1aff68636018a56 | |
parent | 74e97e0f92d7fab09d7112622a5d7b118f103d61 (diff) |
Add chips to support mime type query
* Add SearchChipViewManager
* Implement mime type query with checked chips
* Filter chips base on the derivedMimeTypes of the root
If there is only one matched chip, hide the chip row.
* Add chip move animation
Fix: 111862054
Fix: 120895178
Test: atest DocumentsUITests
Change-Id: If37a7c092eee101306963e4aa05810a86930c693
24 files changed, 900 insertions, 53 deletions
diff --git a/res/color/search_chip_background_color.xml b/res/color/search_chip_background_color.xml new file mode 100644 index 000000000..b45ebdf23 --- /dev/null +++ b/res/color/search_chip_background_color.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018 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 + + https://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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_selected="true" android:color="@color/chip_selected_background_color"/> + <item android:state_enabled="true" android:color="?attr/colorBackgroundFloating"/> + <item android:state_enabled="false" android:color="@color/g_light_grey"/> +</selector>
\ No newline at end of file diff --git a/res/color/search_chip_ripple_color.xml b/res/color/search_chip_ripple_color.xml new file mode 100644 index 000000000..acfb86655 --- /dev/null +++ b/res/color/search_chip_ripple_color.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018 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 + + https://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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Selected. --> + <item android:state_pressed="true" android:state_selected="true" + android:alpha="0.16" android:color="@color/chip_ripple_color"/> + <item android:state_focused="true" android:state_hovered="true" android:state_selected="true" + android:alpha="0.16" android:color="@color/chip_ripple_color"/> + <item android:state_focused="true" android:state_selected="true" + android:alpha="0.12" android:color="@color/chip_ripple_color"/> + <item android:state_hovered="true" android:state_selected="true" + android:alpha="0.04" android:color="@color/chip_ripple_color"/> + <item android:state_selected="true" + android:alpha="0.00" android:color="@color/chip_ripple_color"/> + + <!-- Unselected. --> + <item android:state_pressed="true" android:alpha="0.16" android:color="@color/text_hint"/> + <item android:state_focused="true" android:state_hovered="true" + android:alpha="0.16" android:color="@color/text_hint"/> + <item android:state_focused="true" android:alpha="0.12" android:color="@color/text_hint"/> + <item android:state_hovered="true" android:alpha="0.04" android:color="@color/text_hint"/> + <item android:alpha="0.00" android:color="@color/text_hint"/> +</selector>
\ No newline at end of file diff --git a/res/color/search_chip_stroke_color.xml b/res/color/search_chip_stroke_color.xml new file mode 100644 index 000000000..b5e5d7886 --- /dev/null +++ b/res/color/search_chip_stroke_color.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018 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 + + https://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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_selected="true" android:color="@android:color/transparent"/> + <item android:state_pressed="true" android:color="@android:color/transparent"/> + <item android:state_enabled="true" android:color="@color/chip_stroke_color"/> + <item android:state_enabled="false" android:color="@android:color/transparent"/> +</selector>
\ No newline at end of file diff --git a/res/color/search_chip_text_color.xml b/res/color/search_chip_text_color.xml new file mode 100644 index 000000000..b9893eda8 --- /dev/null +++ b/res/color/search_chip_text_color.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018 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 + + https://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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_selected="true" android:color="?attr/colorAccent"/> + <item android:state_enabled="true" android:color="?attr/colorOnSurface"/> + <item android:state_enabled="false" android:color="@color/text_hint_dark"/> +</selector>
\ No newline at end of file diff --git a/res/drawable/ic_check.xml b/res/drawable/ic_check.xml new file mode 100644 index 000000000..fd378eeb0 --- /dev/null +++ b/res/drawable/ic_check.xml @@ -0,0 +1,25 @@ +<!-- +Copyright (C) 2018 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?attr/colorAccent"> + <path + android:fillColor="@android:color/white" + android:pathData="vM9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/> +</vector> diff --git a/res/layout/directory_header.xml b/res/layout/directory_header.xml index 3a7c1e68d..de1da718f 100644 --- a/res/layout/directory_header.xml +++ b/res/layout/directory_header.xml @@ -21,6 +21,9 @@ <!-- used for top padding. --> <include layout="@layout/action_bar_space"/> + <!-- used for search chip. --> + <include layout="@layout/search_chip_row"/> + <TextView android:id="@+id/header_title" android:layout_width="wrap_content" diff --git a/res/layout/search_chip_item.xml b/res/layout/search_chip_item.xml new file mode 100644 index 000000000..d80d382a1 --- /dev/null +++ b/res/layout/search_chip_item.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> + +<com.google.android.material.chip.Chip + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:checkable="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/search_chip_text_color" + app:checkedIcon="@drawable/ic_check" + app:chipBackgroundColor="@color/search_chip_background_color" + app:chipCornerRadius="8dp" + app:chipStrokeColor="@color/search_chip_stroke_color" + app:chipStrokeWidth="1dp" + app:iconStartPadding="@dimen/search_chip_icon_padding" + app:rippleColor="@color/search_chip_ripple_color" +/>
\ No newline at end of file diff --git a/res/layout/search_chip_row.xml b/res/layout/search_chip_row.xml new file mode 100644 index 000000000..24202b676 --- /dev/null +++ b/res/layout/search_chip_row.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> + +<HorizontalScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:scrollbars="none"> + <com.google.android.material.chip.ChipGroup + android:id="@+id/search_chip_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/search_chip_group_margin" + android:layout_marginBottom="@dimen/search_chip_group_margin" + android:paddingStart="@dimen/search_chip_spacing" + android:paddingEnd="@dimen/search_chip_spacing" + android:gravity="center" + app:chipSpacing="@dimen/search_chip_spacing" + app:singleLine="true"/> +</HorizontalScrollView>
\ No newline at end of file diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml new file mode 100644 index 000000000..9acc28059 --- /dev/null +++ b/res/values-night/colors.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> + +<resources> + <color name="chip_stroke_color">@android:color/transparent</color> + <color name="chip_selected_background_color">#FF3D4657</color> + <color name="chip_ripple_color">#FF5195EA</color> +</resources> diff --git a/res/values/colors.xml b/res/values/colors.xml index a55081547..9cd64c1f5 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -92,4 +92,8 @@ <color name="inspector_section_divider">#E0E0E0</color> <color name="inspector_title_background">#40000000</color> <color name="inspector_debug_mode_color">#607d8b</color> + + <color name="chip_stroke_color">#FFDADCE0</color> + <color name="chip_selected_background_color">#FFE8F0FE</color> + <color name="chip_ripple_color">#FF4285f4</color> </resources> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 9a885d174..bc3ab5f65 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -79,4 +79,9 @@ <dimen name="root_info_header_height">48dp</dimen> <dimen name="root_info_header_horizontal_padding">24dp</dimen> + + <dimen name="search_chip_group_margin">8dp</dimen> + <dimen name="search_chip_spacing">8dp</dimen> + <dimen name="search_chip_icon_padding">4dp</dimen> + </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index 6106682bd..b568a553c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -412,4 +412,13 @@ <string name="root_info_header_app">Files from <xliff:g id="label" example="Drive">%1$s</xliff:g></string> <!-- Header title for list of documents 3 party provider root eg. Drive, Box. with root summary.[CHAR_LIMIT=60] --> <string name="root_info_header_app_with_summary">Files from <xliff:g id="label" example="Drive">%1$s</xliff:g> / <xliff:g id="summary" example="example@com">%2$s</xliff:g></string> + + <!-- Title for images chip. [CHAR_LIMIT=25] --> + <string name="chip_title_images">Images</string> + <!-- Title for audio chip. [CHAR_LIMIT=25] --> + <string name="chip_title_audio">Audio</string> + <!-- Title for videos chip. [CHAR_LIMIT=25] --> + <string name="chip_title_videos">Videos</string> + <!-- Title for image chip. [CHAR_LIMIT=25] --> + <string name="chip_title_documents">Documents</string> </resources> diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java index a956b1717..b6ea20e6b 100644 --- a/src/com/android/documentsui/AbstractActionHandler.java +++ b/src/com/android/documentsui/AbstractActionHandler.java @@ -566,19 +566,17 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA Context context = mActivity; if (mState.stack.isRecents()) { - if (mSearchMgr.isSearching()) { if (DEBUG) { - Log.d(TAG, "Creating new GlobalSearchloader."); + Log.d(TAG, "Creating new GlobalSearchLoader."); } - return new GlobalSearchLoader( context, mProviders, mState, mExecutors, mInjector.fileTypeLookup, - mSearchMgr.getCurrentSearch()); + mSearchMgr.buildQueryArgs()); } else { if (DEBUG) { Log.d(TAG, "Creating new loader recents."); @@ -591,7 +589,6 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA mInjector.fileTypeLookup); } } else { - Uri contentsUri = mSearchMgr.isSearching() ? DocumentsContract.buildSearchDocumentsUri( mState.stack.getRoot().authority, @@ -601,6 +598,10 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA mState.stack.peek().authority, mState.stack.peek().documentId); + final Bundle queryArgs = mSearchMgr.isSearching() + ? mSearchMgr.buildQueryArgs() + : null; + if (mInjector.config.managedModeEnabled(mState.stack)) { contentsUri = DocumentsContract.setManageMode(contentsUri); } @@ -618,7 +619,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA mState.sortModel, mInjector.fileTypeLookup, mContentLock, - mSearchMgr.isSearching()); + queryArgs); } } diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java index 8b8b02979..20d8f0d6f 100644 --- a/src/com/android/documentsui/BaseActivity.java +++ b/src/com/android/documentsui/BaseActivity.java @@ -46,6 +46,7 @@ import androidx.fragment.app.Fragment; import com.android.documentsui.AbstractActionHandler.CommonAddons; import com.android.documentsui.Injector.Injected; import com.android.documentsui.NavigationViewManager.Breadcrumb; +import com.android.documentsui.R; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.EventHandler; import com.android.documentsui.base.RootInfo; @@ -61,13 +62,13 @@ import com.android.documentsui.prefs.ScopedPreferences; import com.android.documentsui.queries.CommandInterceptor; import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.queries.SearchViewManager.SearchManagerListener; -import com.android.documentsui.R; import com.android.documentsui.roots.ProvidersCache; import com.android.documentsui.sidebar.RootsFragment; import com.android.documentsui.sorting.SortController; import com.android.documentsui.sorting.SortModel; import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.chip.ChipGroup; import java.util.ArrayList; import java.util.Date; @@ -188,7 +189,12 @@ public abstract class BaseActivity mInjector.features, mInjector.debugHelper::toggleDebugMode, cmdInterceptor); - mSearchManager = new SearchViewManager(searchListener, queryInterceptor, icicle); + + ChipGroup chipGroup = findViewById(R.id.search_chip_group); + mSearchManager = new SearchViewManager(searchListener, queryInterceptor, + chipGroup, icicle); + mSearchManager.updateChips(getCurrentRoot().derivedMimeTypes); + mSortController = SortController.create(this, mState.derivedMode, mState.sortModel); mPreferencesMonitor = new PreferencesMonitor( @@ -327,7 +333,10 @@ public abstract class BaseActivity if (appBarLayout != null) { appBarLayout.setExpanded(true); } + updateHeaderTitle(); + + mSearchManager.updateChips(root.derivedMimeTypes); } @Override diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java index 233d4d138..4f12c4678 100644 --- a/src/com/android/documentsui/DirectoryLoader.java +++ b/src/com/android/documentsui/DirectoryLoader.java @@ -21,20 +21,21 @@ import static com.android.documentsui.base.SharedMinimal.VERBOSE; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; -import android.content.res.Resources; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; +import android.os.FileUtils; import android.os.Handler; import android.os.Looper; import android.os.OperationCanceledException; import android.os.RemoteException; -import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.util.Log; +import androidx.loader.content.AsyncTaskLoader; + import com.android.documentsui.archives.ArchivesProvider; import com.android.documentsui.base.DebugFlags; import com.android.documentsui.base.DocumentInfo; @@ -45,10 +46,6 @@ import com.android.documentsui.base.RootInfo; import com.android.documentsui.roots.RootCursorWrapper; import com.android.documentsui.sorting.SortModel; -import android.os.FileUtils; - -import androidx.loader.content.AsyncTaskLoader; - import java.util.concurrent.Executor; public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { @@ -63,6 +60,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { private final SortModel mModel; private final Lookup<String, String> mFileTypeLookup; private final boolean mSearchMode; + private final Bundle mQueryArgs; private DocumentInfo mDoc; private CancellationSignal mSignal; @@ -79,7 +77,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { SortModel model, Lookup<String, String> fileTypeLookup, ContentLock lock, - boolean inSearchMode) { + Bundle queryArgs) { super(context); mFeatures = features; @@ -88,7 +86,8 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { mModel = model; mDoc = doc; mFileTypeLookup = fileTypeLookup; - mSearchMode = inSearchMode; + mSearchMode = queryArgs != null; + mQueryArgs = queryArgs; mObserver = new LockingContentObserver(lock, this::onContentChanged); } @@ -121,15 +120,11 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { } result.client = client; - Resources resources = getContext().getResources(); - final Bundle queryArgs = new Bundle(); mModel.addQuerySortArgs(queryArgs); if (mSearchMode) { - // add the search string into the query bundle. - queryArgs.putString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, - DocumentsContract.getSearchDocumentsQuery(mUri)); + queryArgs.putAll(mQueryArgs); } if (mFeatures.isContentPagingEnabled()) { diff --git a/src/com/android/documentsui/GlobalSearchLoader.java b/src/com/android/documentsui/GlobalSearchLoader.java index 364d5ebc4..073b3d0f3 100644 --- a/src/com/android/documentsui/GlobalSearchLoader.java +++ b/src/com/android/documentsui/GlobalSearchLoader.java @@ -39,7 +39,7 @@ import java.util.concurrent.Executor; * {@link android.provider.DocumentsProvider}. */ public class GlobalSearchLoader extends MultiRootDocumentsLoader { - private final String mSearchString; + private final Bundle mQueryArgs; /* * Create the loader to query multiple roots support @@ -52,14 +52,14 @@ public class GlobalSearchLoader extends MultiRootDocumentsLoader { * @param state current state * @param features the feature flags * @param executors the executors of authorities - * @param fileTypeMap the map of mime types and file types. - * @param searchString the string for searching + * @param fileTypeMap the map of mime types and file types + * @param queryArgs the bundle of query arguments */ - public GlobalSearchLoader(Context context, ProvidersAccess providers, State state, + GlobalSearchLoader(Context context, ProvidersAccess providers, State state, Lookup<String, Executor> executors, Lookup<String, String> fileTypeMap, - String searchString) { + @NonNull Bundle queryArgs) { super(context, providers, state, executors, fileTypeMap); - mSearchString = searchString; + mQueryArgs = queryArgs; } @Override @@ -91,14 +91,16 @@ public class GlobalSearchLoader extends MultiRootDocumentsLoader { @Override protected void addQueryArgs(@NonNull Bundle queryArgs) { - queryArgs.putString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, mSearchString); queryArgs.putBoolean(DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, true); + queryArgs.putAll(mQueryArgs); } @Override protected Uri getQueryUri(RootInfo rootInfo) { + // For the new querySearchDocuments, we put the query string into queryArgs. + // Use the empty string to build the query uri. return DocumentsContract.buildSearchDocumentsUri(authority, - rootInfo.rootId, mSearchString); + rootInfo.rootId, "" /* query */); } @Override diff --git a/src/com/android/documentsui/base/Shared.java b/src/com/android/documentsui/base/Shared.java index 67bc4b7f8..546a5cb9e 100644 --- a/src/com/android/documentsui/base/Shared.java +++ b/src/com/android/documentsui/base/Shared.java @@ -78,6 +78,11 @@ public final class Shared { public static final String EXTRA_QUERY = "query"; /** + * Extra flag used to store chip's title of type String array in the bundle. + */ + public static final String EXTRA_QUERY_CHIPS = "query_chips"; + + /** * Extra flag used to store state of type State in the bundle. */ public static final String EXTRA_STATE = "state"; diff --git a/src/com/android/documentsui/queries/SearchChipData.java b/src/com/android/documentsui/queries/SearchChipData.java new file mode 100644 index 000000000..1ce9b74ed --- /dev/null +++ b/src/com/android/documentsui/queries/SearchChipData.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 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.documentsui.queries; + +/** + * A data class stored data which search chip row required. + * Used by {@link SearchChipViewManager}. + */ +public class SearchChipData { + + private final int mChipType; + private final int mTitleRes; + private final String[] mMimeTypes; + + public SearchChipData(int chipType, int titleRes, String[] mimeTypes) { + mChipType = chipType; + mTitleRes = titleRes; + mMimeTypes = mimeTypes; + } + + public final int getTitleRes() { + return mTitleRes; + } + + public final String[] getMimeTypes() { + return mMimeTypes; + } + + public final int getChipType() { + return mChipType; + } +}
\ No newline at end of file diff --git a/src/com/android/documentsui/queries/SearchChipViewManager.java b/src/com/android/documentsui/queries/SearchChipViewManager.java new file mode 100644 index 000000000..d94600e91 --- /dev/null +++ b/src/com/android/documentsui/queries/SearchChipViewManager.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2018 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.documentsui.queries; + +import android.animation.ObjectAnimator; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.HorizontalScrollView; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.documentsui.IconUtils; +import com.android.documentsui.R; +import com.android.documentsui.base.MimeTypes; +import com.android.documentsui.base.Shared; + +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.common.primitives.Ints; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Manages search chip behavior. + */ +public class SearchChipViewManager { + + private static final int CHIP_MOVE_ANIMATION_DURATION = 250; + + private static final int TYPE_IMAGES = 0; + private static final int TYPE_DOCUMENTS = 1; + private static final int TYPE_AUDIO = 2; + private static final int TYPE_VIDEOS = 3; + + private static final ChipComparator CHIP_COMPARATOR = new ChipComparator(); + + // we will get the icon drawable with the first mimeType + private static final String[] IMAGES_MIMETYPES = new String[]{"image/*"}; + private static final String[] VIDEOS_MIMETYPES = new String[]{"video/*"}; + private static final String[] AUDIO_MIMETYPES = + new String[]{"audio/*", "application/ogg", "application/x-flac"}; + private static final String[] DOCUMENTS_MIMETYPES = new String[]{"application/*", "text/*"}; + + private static final Map<Integer, SearchChipData> sChipItems = new HashMap<>(); + + private final ChipGroup mChipGroup; + private SearchChipViewManagerListener mListener; + + @VisibleForTesting + Set<SearchChipData> mCheckedChipItems = new HashSet<>(); + + static { + sChipItems.put(TYPE_IMAGES, + new SearchChipData(TYPE_IMAGES, R.string.chip_title_images, IMAGES_MIMETYPES)); + sChipItems.put(TYPE_DOCUMENTS, + new SearchChipData(TYPE_DOCUMENTS, R.string.chip_title_documents, + DOCUMENTS_MIMETYPES)); + sChipItems.put(TYPE_AUDIO, + new SearchChipData(TYPE_AUDIO, R.string.chip_title_audio, AUDIO_MIMETYPES)); + sChipItems.put(TYPE_VIDEOS, + new SearchChipData(TYPE_VIDEOS, R.string.chip_title_videos, VIDEOS_MIMETYPES)); + } + + + + public SearchChipViewManager(@NonNull ChipGroup chipGroup) { + mChipGroup = chipGroup; + } + + /** + * Restore the checked chip items by the saved state. + * + * @param savedState the saved state to restore. + */ + public void restoreCheckedChipItems(Bundle savedState) { + final int[] chipTypes = savedState.getIntArray(Shared.EXTRA_QUERY_CHIPS); + if (chipTypes != null) { + clearCheckedChips(); + for (int chipType : chipTypes) { + final SearchChipData chipData = sChipItems.get(chipType); + mCheckedChipItems.add(chipData); + setCheckedChip(chipData.getChipType()); + } + } + } + + /** + * Set the visibility of the chips row. If the count of chips is less than 2, + * we will hide the chips row. + * + * @param show the value to show/hide the chips row. + */ + public void setChipsRowVisible(boolean show) { + // if there is only one matched chip, hide the chip group. + mChipGroup.setVisibility(show && mChipGroup.getChildCount() > 1 ? View.VISIBLE : View.GONE); + } + + /** + * Check Whether the checked item list has contents. + * + * @return True, if the checked item list is not empty. Otherwise, return false. + */ + public boolean hasCheckedItems() { + return !mCheckedChipItems.isEmpty(); + } + + /** + * Clear the checked state of Chips and the checked list. + */ + public void clearCheckedChips() { + final int count = mChipGroup.getChildCount(); + for (int i = 0; i < count; i++) { + Chip child = (Chip) mChipGroup.getChildAt(i); + setChipChecked(child, false /* isChecked */); + } + mCheckedChipItems.clear(); + } + + /** + * Get the mime types of checked chips + * + * @return the string array of mime types + */ + public String[] getCheckedMimeTypes() { + final ArrayList<String> args = new ArrayList<>(); + for (SearchChipData data : mCheckedChipItems) { + for (String mimeType : data.getMimeTypes()) { + args.add(mimeType); + } + } + return args.toArray(new String[0]); + } + + /** + * Called when owning activity is saving state to be used to restore state during creation. + * + * @param state Bundle to save state + */ + public void onSaveInstanceState(Bundle state) { + List<Integer> checkedChipList = new ArrayList<>(); + + for (SearchChipData item : mCheckedChipItems) { + checkedChipList.add(item.getChipType()); + } + + if (checkedChipList.size() > 0) { + state.putIntArray(Shared.EXTRA_QUERY_CHIPS, Ints.toArray(checkedChipList)); + } + } + + /** + * Update the search chips base on the mime types. + * + * @param acceptMimeTypes use this values to filter chips + */ + public void updateChips(String[] acceptMimeTypes) { + final Context context = mChipGroup.getContext(); + mChipGroup.removeAllViews(); + + final LayoutInflater inflater = LayoutInflater.from(context); + for (SearchChipData chipData : sChipItems.values()) { + final String[] mimeTypes = chipData.getMimeTypes(); + final boolean isMatched = MimeTypes.mimeMatches(acceptMimeTypes, mimeTypes); + if (isMatched) { + Chip chip = (Chip) inflater.inflate(R.layout.search_chip_item, mChipGroup, false); + bindChip(chip, chipData); + mChipGroup.addView(chip); + } + } + reorderCheckedChips(false /* hasAnim */); + } + + + /** + * Set the listener. + * + * @param listener the listener + */ + public void setSearchChipViewManagerListener(SearchChipViewManagerListener listener) { + mListener = listener; + } + + private static void setChipChecked(Chip chip, boolean isChecked) { + chip.setChecked(isChecked); + chip.setCheckedIconVisible(isChecked); + chip.setChipIconVisible(!isChecked); + } + + private void setCheckedChip(int chipType) { + final int count = mChipGroup.getChildCount(); + for (int i = 0; i < count; i++) { + Chip child = (Chip) mChipGroup.getChildAt(i); + SearchChipData item = (SearchChipData) child.getTag(); + if (item.getChipType() == chipType) { + setChipChecked(child, true /* isChecked */); + break; + } + } + } + + private void onChipClick(View v) { + final Chip chip = (Chip) v; + final SearchChipData item = (SearchChipData) chip.getTag(); + if (chip.isChecked()) { + mCheckedChipItems.add(item); + } else { + mCheckedChipItems.remove(item); + } + + setChipChecked(chip, chip.isChecked()); + reorderCheckedChips(true /* hasAnim */); + if (mListener != null) { + mListener.onChipCheckStateChanged(); + } + } + + private void bindChip(Chip chip, SearchChipData chipData) { + chip.setTag(chipData); + chip.setText(mChipGroup.getContext().getString(chipData.getTitleRes())); + // get the icon drawable with the first mimeType + chip.setChipIcon( + IconUtils.loadMimeIcon(mChipGroup.getContext(), chipData.getMimeTypes()[0])); + chip.setOnClickListener(this::onChipClick); + + if (mCheckedChipItems.contains(chipData)) { + setChipChecked(chip, true); + } + } + + /** + * Reorder the chips in chip group. The checked chip has higher order. + * + * @param hasAnim if true, play move animation. Otherwise, not. + */ + private void reorderCheckedChips(boolean hasAnim) { + final ArrayList<Chip> chipList = new ArrayList<>(); + final int count = mChipGroup.getChildCount(); + final boolean playAnimation = hasAnim && mChipGroup.isAttachedToWindow(); + final Map<String, Float> originalXList = new HashMap<>(); + Chip item; + for (int i = 0; i < count; i++) { + item = (Chip) mChipGroup.getChildAt(i); + chipList.add(item); + if (playAnimation) { + originalXList.put(item.getText().toString(), item.getX()); + } + } + + final int chipSpacing = mChipGroup.getChipSpacingHorizontal(); + float lastX = chipList.get(0).getX(); + Collections.sort(chipList, CHIP_COMPARATOR); + + mChipGroup.removeAllViews(); + for (Chip chip : chipList) { + mChipGroup.addView(chip); + if (playAnimation) { + ObjectAnimator animator = ObjectAnimator.ofFloat(chip, "x", + originalXList.get(chip.getText().toString()), lastX); + animator.setDuration(CHIP_MOVE_ANIMATION_DURATION); + animator.start(); + } + lastX += chipSpacing + chip.getMeasuredWidth(); + } + + if (playAnimation) { + // Let the first checked chip can be seen. + View parent = (View) mChipGroup.getParent(); + if (parent != null && parent instanceof HorizontalScrollView) { + ((HorizontalScrollView) mChipGroup.getParent()).smoothScrollTo(0, 0); + } + } + } + + /** + * The listener of SearchChipViewManager. + */ + public interface SearchChipViewManagerListener { + /** + * It will be triggered when the checked state of chips changes. + */ + void onChipCheckStateChanged(); + } + + private static class ChipComparator implements Comparator<Chip> { + + @Override + public int compare(Chip lhs, Chip rhs) { + return (lhs.isChecked() == rhs.isChecked()) ? 0 : (lhs.isChecked() ? -1 : 1); + } + } +} diff --git a/src/com/android/documentsui/queries/SearchViewManager.java b/src/com/android/documentsui/queries/SearchViewManager.java index 70c893a43..9cabf2c0a 100644 --- a/src/com/android/documentsui/queries/SearchViewManager.java +++ b/src/com/android/documentsui/queries/SearchViewManager.java @@ -18,10 +18,10 @@ package com.android.documentsui.queries; import static com.android.documentsui.base.SharedMinimal.DEBUG; -import androidx.annotation.Nullable; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.provider.DocumentsContract; import android.provider.DocumentsContract.Root; import android.text.TextUtils; import android.util.Log; @@ -32,16 +32,20 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.SearchView.OnQueryTextListener; + import com.android.documentsui.R; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.EventHandler; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.Shared; -import androidx.annotation.GuardedBy; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.widget.SearchView; -import androidx.appcompat.widget.SearchView.OnQueryTextListener; + +import com.google.android.material.chip.ChipGroup; import java.util.Timer; import java.util.TimerTask; @@ -60,6 +64,7 @@ public class SearchViewManager implements private final SearchManagerListener mListener; private final EventHandler<String> mCommandProcessor; + private final SearchChipViewManager mChipViewManager; private final Timer mTimer; private final Handler mUiHandler; @@ -80,15 +85,17 @@ public class SearchViewManager implements public SearchViewManager( SearchManagerListener listener, EventHandler<String> commandProcessor, + ChipGroup chipGroup, @Nullable Bundle savedState) { - this(listener, commandProcessor, savedState, new Timer(), - new Handler(Looper.getMainLooper())); + this(listener, commandProcessor, new SearchChipViewManager(chipGroup), savedState, + new Timer(), new Handler(Looper.getMainLooper())); } @VisibleForTesting protected SearchViewManager( SearchManagerListener listener, EventHandler<String> commandProcessor, + SearchChipViewManager chipViewManager, @Nullable Bundle savedState, Timer timer, Handler handler) { @@ -100,7 +107,49 @@ public class SearchViewManager implements mCommandProcessor = commandProcessor; mTimer = timer; mUiHandler = handler; - mCurrentSearch = savedState != null ? savedState.getString(Shared.EXTRA_QUERY) : null; + mChipViewManager = chipViewManager; + mChipViewManager.setSearchChipViewManagerListener(this::onChipCheckedStateChanged); + + if (savedState != null) { + mCurrentSearch = savedState.getString(Shared.EXTRA_QUERY); + mChipViewManager.restoreCheckedChipItems(savedState); + } else { + mCurrentSearch = null; + } + } + + private void onChipCheckedStateChanged() { + performSearch(mCurrentSearch); + } + + /** + * Build the bundle of query arguments. + * Example: search string and mime types + * + * @return the bundle of query arguments + */ + public Bundle buildQueryArgs() { + final Bundle queryArgs = new Bundle(); + if (!TextUtils.isEmpty(mCurrentSearch)) { + queryArgs.putString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, mCurrentSearch); + } + + final String[] checkedMimeTypes = mChipViewManager.getCheckedMimeTypes(); + if (checkedMimeTypes != null && checkedMimeTypes.length > 0) { + queryArgs.putStringArray(DocumentsContract.QUERY_ARG_MIME_TYPES, checkedMimeTypes); + } + return queryArgs; + } + + /** + * Update the search chips base on the acceptMimeTypes. + * If the count of matched chips is less than two, we will + * hide the chip row. + * + * @param acceptMimeTypes use to filter chips + */ + public void updateChips(String[] acceptMimeTypes) { + mChipViewManager.updateChips(acceptMimeTypes); } public void install(Menu menu, boolean isFullBarSearch) { @@ -190,6 +239,15 @@ public class SearchViewManager implements } mMenuItem.setVisible(supportsSearch); + + // Only Storage roots, Downloads root, media roots and recent root + // support mime type query now. + // TODO: b/121234248 add check for whether the root supports new search method. + if (supportsSearch && !root.isDownloads() && !root.isStorage() && !root.isLibrary()) { + supportsSearch = false; + } + + mChipViewManager.setChipsRowVisible(supportsSearch); } /** @@ -204,11 +262,12 @@ public class SearchViewManager implements mSearchView.setQuery("", false); if (mFullBar) { - onClose(); + onClose(); } else { // Causes calling onClose(). onClose() is triggering directory content update. mSearchView.setIconified(true); } + return true; } return false; @@ -253,6 +312,7 @@ public class SearchViewManager implements /** * Clears the search. Triggers refreshing of the directory content. + * * @return True if the default behavior of clearing/dismissing SearchView should be overridden. * False otherwise. */ @@ -265,7 +325,9 @@ public class SearchViewManager implements } // Refresh the directory if a search was done - if (mCurrentSearch != null) { + if (mCurrentSearch != null || mChipViewManager.hasCheckedItems()) { + // Clear checked chips + mChipViewManager.clearCheckedChips(); mCurrentSearch = null; mListener.onSearchChanged(mCurrentSearch); } @@ -282,10 +344,12 @@ public class SearchViewManager implements /** * Called when owning activity is saving state to be used to restore state during creation. + * * @param state Bundle to save state too */ public void onSaveInstanceState(Bundle state) { state.putString(Shared.EXTRA_QUERY, mCurrentSearch); + mChipViewManager.onSaveInstanceState(state); } /** @@ -320,7 +384,7 @@ public class SearchViewManager implements */ @Override public void onFocusChange(View v, boolean hasFocus) { - if (!hasFocus) { + if (!hasFocus && !mChipViewManager.hasCheckedItems()) { if (mCurrentSearch == null) { mSearchView.setIconified(true); } else if (TextUtils.isEmpty(mSearchView.getQuery())) { @@ -351,13 +415,17 @@ public class SearchViewManager implements @Override public boolean onQueryTextChange(String newText) { + performSearch(newText); + return true; + } + + private void performSearch(String newText) { cancelQueuedSearch(); synchronized (mSearchLock) { mQueuedSearchTask = createSearchTask(newText); mTimer.schedule(mQueuedSearchTask, SEARCH_DELAY_MS); } - return true; } @Override @@ -382,7 +450,7 @@ public class SearchViewManager implements } public boolean isSearching() { - return mCurrentSearch != null; + return mCurrentSearch != null || mChipViewManager.hasCheckedItems(); } public boolean isExpanded() { @@ -391,7 +459,9 @@ public class SearchViewManager implements public interface SearchManagerListener { void onSearchChanged(@Nullable String query); + void onSearchFinished(); + void onSearchViewChanged(boolean opened); } } diff --git a/tests/common/com/android/documentsui/testing/TestSearchViewManager.java b/tests/common/com/android/documentsui/testing/TestSearchViewManager.java index 6c37ef7b2..af23b46ec 100644 --- a/tests/common/com/android/documentsui/testing/TestSearchViewManager.java +++ b/tests/common/com/android/documentsui/testing/TestSearchViewManager.java @@ -16,10 +16,14 @@ package com.android.documentsui.testing; +import static org.mockito.Mockito.mock; + import com.android.documentsui.base.DocumentStack; import com.android.documentsui.queries.CommandInterceptor; import com.android.documentsui.queries.SearchViewManager; +import com.google.android.material.chip.ChipGroup; + /** * Test copy of {@link com.android.documentsui.queries.SearchViewManager} * @@ -37,14 +41,20 @@ public class TestSearchViewManager extends SearchViewManager { super( new SearchManagerListener() { @Override - public void onSearchChanged(String query) { } + public void onSearchChanged(String query) { + } + @Override - public void onSearchFinished() { } + public void onSearchFinished() { + } + @Override - public void onSearchViewChanged(boolean opened) { } + public void onSearchViewChanged(boolean opened) { + } }, new CommandInterceptor(new TestFeatures()), - null); + mock(ChipGroup.class), + null /* savedState */); } @Override diff --git a/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java b/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java index 36a36ea7f..d0b8f1731 100644 --- a/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java +++ b/tests/unit/com/android/documentsui/GlobalSearchLoaderTest.java @@ -20,6 +20,7 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import android.database.Cursor; +import android.os.Bundle; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; @@ -62,8 +63,10 @@ public class GlobalSearchLoaderTest { mEnv.state.action = State.ACTION_BROWSE; mEnv.state.acceptMimes = new String[]{"*/*"}; + final Bundle queryArgs = new Bundle(); + queryArgs.putString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, SEARCH_STRING); mLoader = new GlobalSearchLoader(mActivity, mEnv.providers, mEnv.state, - TestImmediateExecutor.createLookup(), new TestFileTypeLookup(), SEARCH_STRING); + TestImmediateExecutor.createLookup(), new TestFileTypeLookup(), queryArgs); final DocumentInfo doc = mEnv.model.createFile(SEARCH_STRING + ".jpg", FILE_FLAG); doc.lastModified = System.currentTimeMillis(); diff --git a/tests/unit/com/android/documentsui/queries/SearchChipViewManagerTest.java b/tests/unit/com/android/documentsui/queries/SearchChipViewManagerTest.java new file mode 100644 index 000000000..bf11cc2a3 --- /dev/null +++ b/tests/unit/com/android/documentsui/queries/SearchChipViewManagerTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 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.documentsui.queries; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.Mockito.mock; + +import android.os.Bundle; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.documentsui.base.MimeTypes; +import com.android.documentsui.base.Shared; + +import com.google.android.material.chip.ChipGroup; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class SearchChipViewManagerTest { + + private static final String[] TEST_MIMETYPES = new String[]{"image/png", "video/mpeg"}; + private static int CHIP_TYPE = 1000; + + private SearchChipViewManager mSearchChipViewManager; + + @Before + public void setUp() { + ChipGroup chipGroup = mock(ChipGroup.class); + mSearchChipViewManager = new SearchChipViewManager(chipGroup); + } + + @Test + public void testGetCheckedChipMimeTypes_HasCorrectValue() throws Exception { + mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList(); + final String[] checkedMimeTypes = mSearchChipViewManager.getCheckedMimeTypes(); + assertTrue(MimeTypes.mimeMatches(TEST_MIMETYPES, checkedMimeTypes[0])); + assertTrue(MimeTypes.mimeMatches(TEST_MIMETYPES, checkedMimeTypes[1])); + } + + @Test + public void testRestoreCheckedChipItems_HasCorrectValue() throws Exception { + Bundle bundle = new Bundle(); + bundle.putIntArray(Shared.EXTRA_QUERY_CHIPS, new int[] {2}); + mSearchChipViewManager.restoreCheckedChipItems(bundle); + + assertEquals(1, mSearchChipViewManager.mCheckedChipItems.size()); + Iterator<SearchChipData> iterator = mSearchChipViewManager.mCheckedChipItems.iterator(); + assertEquals(2, iterator.next().getChipType()); + } + + @Test + public void testSaveInstanceState_HasCorrectValue() throws Exception { + mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList(); + Bundle bundle = new Bundle(); + mSearchChipViewManager.onSaveInstanceState(bundle); + final int[] chipTypes = bundle.getIntArray(Shared.EXTRA_QUERY_CHIPS); + assertEquals(1, chipTypes.length); + assertEquals(CHIP_TYPE, chipTypes[0]); + } + + private static Set<SearchChipData> getFakeSearchChipDataList() { + final Set<SearchChipData> chipDataList = new HashSet<>(); + chipDataList.add(new SearchChipData(CHIP_TYPE, 0, TEST_MIMETYPES)); + return chipDataList; + } +} diff --git a/tests/unit/com/android/documentsui/queries/SearchViewManagerTest.java b/tests/unit/com/android/documentsui/queries/SearchViewManagerTest.java index 00a2166f6..44ec387cb 100644 --- a/tests/unit/com/android/documentsui/queries/SearchViewManagerTest.java +++ b/tests/unit/com/android/documentsui/queries/SearchViewManagerTest.java @@ -16,28 +16,35 @@ package com.android.documentsui.queries; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.mock; + import android.os.Bundle; import android.os.Handler; +import android.provider.DocumentsContract; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.documentsui.base.EventHandler; -import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.queries.SearchViewManager.SearchManagerListener; import com.android.documentsui.testing.TestEventHandler; import com.android.documentsui.testing.TestHandler; import com.android.documentsui.testing.TestMenu; import com.android.documentsui.testing.TestTimer; +import com.google.android.material.chip.ChipGroup; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.HashSet; +import java.util.Set; import java.util.Timer; import java.util.TimerTask; @@ -49,6 +56,7 @@ public final class SearchViewManagerTest { private TestTimer mTestTimer; private TestHandler mTestHandler; private SearchViewManager mSearchViewManager; + private SearchChipViewManager mSearchChipViewManager; private boolean mListenerOnSearchChangedCalled; @@ -63,14 +71,20 @@ public final class SearchViewManagerTest { public void onSearchChanged(@Nullable String query) { mListenerOnSearchChangedCalled = true; } + @Override - public void onSearchFinished() {} + public void onSearchFinished() { + } + @Override - public void onSearchViewChanged(boolean opened) {} + public void onSearchViewChanged(boolean opened) { + } }; - mSearchViewManager = new TestableSearchViewManager( - searchListener, mTestEventHandler, null, mTestTimer, mTestHandler); + ChipGroup chipGroup = mock(ChipGroup.class); + mSearchChipViewManager = new SearchChipViewManager(chipGroup); + mSearchViewManager = new TestableSearchViewManager(searchListener, mTestEventHandler, + mSearchChipViewManager, null /* savedState */, mTestTimer, mTestHandler); final TestMenu testMenu = TestMenu.create(); mSearchViewManager.install(testMenu, true); @@ -80,10 +94,11 @@ public final class SearchViewManagerTest { public TestableSearchViewManager( SearchManagerListener listener, EventHandler<String> commandProcessor, + SearchChipViewManager chipViewManager, @Nullable Bundle savedState, Timer timer, Handler handler) { - super(listener, commandProcessor, savedState, timer, handler); + super(listener, commandProcessor, chipViewManager, savedState, timer, handler); } @Override @@ -113,6 +128,12 @@ public final class SearchViewManagerTest { } @Test + public void testIsSearching_TrueHasCheckedChip() throws Exception { + mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList(); + assertTrue(mSearchViewManager.isSearching()); + } + + @Test public void testIsSearching_FalseOnClick() throws Exception { mSearchViewManager.onClick(null); assertFalse(mSearchViewManager.isSearching()); @@ -214,4 +235,46 @@ public final class SearchViewManagerTest { mSearchViewManager.onQueryTextSubmit("q"); assertFalse(mListenerOnSearchChangedCalled); } + + @Test + public void testCheckedChipItems_IsEmptyIfSearchCanceled() throws Exception { + mSearchViewManager.onClick(null); + mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList(); + mSearchViewManager.cancelSearch(); + fastForwardTo(SearchViewManager.SEARCH_DELAY_MS); + assertTrue(!mSearchChipViewManager.hasCheckedItems()); + } + + @Test + public void testBuildQueryArgs_hasSearchString() throws Exception { + final String query = "q"; + mSearchViewManager.onClick(null); + mSearchViewManager.onQueryTextChange("q"); + fastForwardTo(SearchViewManager.SEARCH_DELAY_MS); + + final Bundle queryArgs = mSearchViewManager.buildQueryArgs(); + assertFalse(queryArgs.isEmpty()); + + final String queryString = queryArgs.getString(DocumentsContract.QUERY_ARG_DISPLAY_NAME); + assertEquals(query, queryString); + } + + @Test + public void testBuildQueryArgs_hasMimeType() throws Exception { + mSearchViewManager.onClick(null); + mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList(); + + final Bundle queryArgs = mSearchViewManager.buildQueryArgs(); + assertFalse(queryArgs.isEmpty()); + + final String[] mimeTypes = queryArgs.getStringArray(DocumentsContract.QUERY_ARG_MIME_TYPES); + assertTrue(mimeTypes.length > 0); + assertEquals("image/*", mimeTypes[0]); + } + + private static Set<SearchChipData> getFakeSearchChipDataList() { + final Set<SearchChipData> chipDataList = new HashSet<>(); + chipDataList.add(new SearchChipData(0 /* chipType */, 0, new String[]{"image/*"})); + return chipDataList; + } } |