DO NOT MERGE Snap to latest SettingsIntelligence source
Bug: 67755462
Test: rebuild
Change-Id: Ib606a68c10f83c3a1479f234af19fe840b3120d0
diff --git a/Android.mk b/Android.mk
index 1efed08..b565774 100644
--- a/Android.mk
+++ b/Android.mk
@@ -24,14 +24,23 @@
LOCAL_PRIVILEGED_MODULE := true
LOCAL_STATIC_ANDROID_LIBRARIES := \
- android-support-v4
+ android-support-v4 \
+ android-support-v13 \
+ android-support-v7-appcompat \
+ android-support-v7-cardview \
+ android-support-v7-preference \
+ android-support-v7-recyclerview \
+ android-support-v14-preference
LOCAL_USE_AAPT2 := true
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_SRC_FILES := \
- $(call all-java-files-under, src)
+ $(call all-java-files-under, src) \
+ $(call all-proto-files-under, proto)
+
+LOCAL_PROTOC_OPTIMIZE_TYPE := nano
include $(BUILD_PACKAGE)
diff --git a/proto/settings_intelligence_log.proto b/proto/settings_intelligence_log.proto
new file mode 100644
index 0000000..9bfd28f
--- /dev/null
+++ b/proto/settings_intelligence_log.proto
@@ -0,0 +1,92 @@
+syntax = "proto2";
+
+option java_outer_classname = "SettingsIntelligenceLogProto";
+
+package com.android.settings.intelligence;
+
+// Wrapper for SettingsIntelligence event.
+// Next index: 3
+message SettingsIntelligenceEvent {
+
+ // Event type for this log.
+ enum EventType {
+ // Do not use
+ UNUSED = 0;
+
+ // Gets suggestion list
+ GET_SUGGESTION = 1;
+
+ // Dismisses a suggestion
+ DISMISS_SUGGESTION = 2;
+
+ // Launches a suggestion
+ LAUNCH_SUGGESTION = 3;
+
+ // Opens search page
+ OPEN_SEARCH_PAGE = 4;
+
+ // Leaves search page
+ LEAVE_SEARCH_PAGE = 5;
+
+ // User sends a query to settings search
+ PERFORM_SEARCH = 6;
+
+ // Clicks a search result
+ CLICK_SEARCH_RESULT = 7;
+
+ // Clicks a saved query
+ CLICK_SAVED_QUERY = 8;
+
+ // Search service indexes database
+ INDEX_SEARCH = 9;
+
+ // Displays the no result image in search
+ SHOW_SEARCH_NO_RESULT = 10;
+
+ // Displays some result in search
+ SHOW_SEARCH_RESULT = 11;
+
+ // Leaves search page without entering any query
+ LEAVE_SEARCH_WITHOUT_QUERY = 12;
+
+ // Queries search data during a search session
+ SEARCH_QUERY_DATABASE = 13;
+
+ // Queries installed app list during a search session
+ SEARCH_QUERY_INSTALLED_APPS = 14;
+
+ // Queries input device list (keyboards, game controller etc) during
+ // a search session
+ SEARCH_QUERY_INPUT_DEVICES = 15;
+
+ // Queries accessiblity service list during a search session
+ SEARCH_QUERY_ACCESSIBILITY_SERVICES = 16;
+ }
+
+ message SearchResultMetadata {
+ // The id of the search result row in this event, this is an internally
+ // generated key and does not associate with any user data.
+ optional string search_result_key = 1;
+
+ // The rank of the search result row in this event.
+ optional int32 search_result_rank = 2;
+
+ // The number of results in this query.
+ optional int32 result_count = 3;
+
+ // The length of query word.
+ optional int32 search_query_length = 4;
+ }
+
+ // The type of suggestion event.
+ optional EventType event_type = 1;
+
+ // The name/id of the suggestion in this event.
+ repeated string suggestion_ids = 2;
+
+ // Data about search results in this event.
+ optional SearchResultMetadata search_result_metadata = 3;
+
+ // Latency for the current event.
+ optional int64 latency_millis = 4;
+}
diff --git a/res/drawable/empty_search_results.xml b/res/drawable/empty_search_results.xml
new file mode 100644
index 0000000..9162107
--- /dev/null
+++ b/res/drawable/empty_search_results.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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="96dp"
+ android:height="96dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal">
+
+ <path
+ android:fillColor="#000000"
+ android:pathData="M15.5,14h-0.79l-0.28-0.27c1.2-1.4,1.82-3.31,1.48-5.34c-0.47-2.78-2.79-5-5.59-5.34c-4.23-0.52-7.79,3.04-7.27,7.27
+c0.34,2.8,2.56,5.12,5.34,5.59c2.03,0.34,3.94-0.28,5.34-1.48L14,14.71v0.79l5.2,5.19c0.41,0.41,1.07,0.41,1.48,0l0.01-0.01
+c0.41-0.41,0.41-1.07,0-1.48L15.5,14z M9.5,14C7.01,14,5,11.99,5,9.5S7.01,5,9.5,5S14,7.01,14,9.5S11.99,14,9.5,14z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_close_24dp.xml b/res/drawable/ic_close_24dp.xml
new file mode 100644
index 0000000..17451d3
--- /dev/null
+++ b/res/drawable/ic_close_24dp.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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:viewportHeight="24.0"
+ android:viewportWidth="24.0"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
diff --git a/res/drawable/ic_help_24dp.xml b/res/drawable/ic_help_24dp.xml
new file mode 100644
index 0000000..3f554e7
--- /dev/null
+++ b/res/drawable/ic_help_24dp.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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="?android:attr/colorControlNormal">
+ <path
+ android:pathData="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 16.81c-.72
+0-1.3-.58-1.3-1.3s.58-1.3 1.3-1.3 1.3 .58 1.3 1.3-.58 1.3-1.3
+1.3zm1.07-4.62c-.09 .52 -.59 .87 -1.13 .79 -.57-.08-.94-.66-.83-1.23 .52 -2.61
+2.66-2.84 2.87-4.5 .12 -.96-.42-1.87-1.34-2.17-1.04-.33-2.21 .16 -2.55 1.37-.12
+.45 -.52 .74 -.97 .74 -.66 0-1.13-.63-.96-1.27 .63 -2.25 2.91-3.38 5.05-2.74
+1.71 .51 2.84 2.16 2.78 3.95-.07 2.44-2.49 2.61-2.92 5.06z"
+ android:fillColor="#FFFFFFFF"/>
+</vector>
diff --git a/res/drawable/ic_restore.xml b/res/drawable/ic_restore.xml
new file mode 100644
index 0000000..201b1df
--- /dev/null
+++ b/res/drawable/ic_restore.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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="24.0dp"
+ android:height="24.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M13.5 7.75v4.5l3.37 2c.34 .2 .46 .65 .25 .99 s-.64 .45 -.98 .24 L12
+13V7.75c0-.41 .34 -.75 .75 -.75s.75 .34 .75 .75 zM13.05 3C8.11 3 4.1 7.02 4.1
+11.95c0 .02 .01 .03 .01 .05H2.05c-.47 0-.71 .57 -.37 .9 l2.95 2.94c.21 .21 .54
+.21 .75 0l2.95-2.94c.33-.33 .1 -.9-.37-.9H5.99c0-.02 .01 -.03 .01 -.05C6 8.06
+9.16 4.9 13.05 4.9S20.1 8.11 20.1 12s-3.16 7.1-7.05 7.1c-1.58
+0-3.08-.51-4.32-1.48a.94 .94 0 0 0-1.32 .16 l-.01 .01 a.94 .94 0 0 0 .16
+1.32l.01 .01 A8.77 8.77 0 0 0 13.05 21c4.94 0 8.95-4.07 8.95-9s-4.02-9-8.95-9z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_search_24dp.xml b/res/drawable/ic_search_24dp.xml
new file mode 100644
index 0000000..8f14e88
--- /dev/null
+++ b/res/drawable/ic_search_24dp.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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"
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal">
+
+ <path
+ android:fillColor="#000000"
+ android:pathData="M15.5,14h-0.79l-0.28-0.27c1.2-1.4,1.82-3.31,1.48-5.34c-0.47-2.78-2.79-5-5.59-5.34c-4.23-0.52-7.79,3.04-7.27,7.27
+c0.34,2.8,2.56,5.12,5.34,5.59c2.03,0.34,3.94-0.28,5.34-1.48L14,14.71v0.79l5.2,5.19c0.41,0.41,1.07,0.41,1.48,0l0.01-0.01
+c0.41-0.41,0.41-1.07,0-1.48L15.5,14z M9.5,14C7.01,14,5,11.99,5,9.5S7.01,5,9.5,5S14,7.01,14,9.5S11.99,14,9.5,14z" />
+</vector>
\ No newline at end of file
diff --git a/res/layout/search_breadcrumb_view.xml b/res/layout/search_breadcrumb_view.xml
new file mode 100644
index 0000000..08a2651
--- /dev/null
+++ b/res/layout/search_breadcrumb_view.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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.
+ -->
+
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/breadcrumb"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="5dp"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:textColor="?android:attr/textColorSecondary"
+ android:ellipsize="marquee"/>
diff --git a/res/layout/search_feedback.xml b/res/layout/search_feedback.xml
new file mode 100644
index 0000000..c9f1fc1
--- /dev/null
+++ b/res/layout/search_feedback.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<View
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/feedback_popup"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:visibility="gone">
+</View>
\ No newline at end of file
diff --git a/res/layout/search_icon_view.xml b/res/layout/search_icon_view.xml
new file mode 100644
index 0000000..ef99bee
--- /dev/null
+++ b/res/layout/search_icon_view.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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.
+ -->
+
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/dashboard_tile_image_size"
+ android:layout_height="match_parent"
+ android:scaleType="centerInside"
+ android:layout_marginStart="@dimen/dashboard_tile_image_margin"
+ android:layout_marginEnd="@dimen/dashboard_tile_image_margin" />
diff --git a/res/layout/search_intent_item.xml b/res/layout/search_intent_item.xml
new file mode 100644
index 0000000..6bc00ad
--- /dev/null
+++ b/res/layout/search_intent_item.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:background="?android:attr/selectableItemBackground"
+ android:clipToPadding="false">
+
+ <include layout="@layout/search_icon_view"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:ellipsize="marquee"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:ellipsize="marquee"/>
+
+ <include layout="@layout/search_breadcrumb_view"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/search_main.xml b/res/layout/search_main.xml
new file mode 100644
index 0000000..6de344d
--- /dev/null
+++ b/res/layout/search_main.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 2016, 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.
+*/
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/main_content"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"/>
diff --git a/res/layout/search_panel.xml b/res/layout/search_panel.xml
new file mode 100644
index 0000000..b5933cc
--- /dev/null
+++ b/res/layout/search_panel.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/search_panel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/search_bar_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/search_panel_background"
+ android:elevation="4dp">
+ <android.support.v7.widget.CardView
+ android:id="@+id/search_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/search_bar_margin"
+ app:cardCornerRadius="2dp"
+ app:cardBackgroundColor="?android:attr/colorBackground"
+ app:cardElevation="2dp">
+ <Toolbar
+ android:id="@+id/search_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/search_bar_height"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentInsetStart="0dp"
+ android:contentInsetStartWithNavigation="0dp"
+ android:theme="?android:attr/actionBarTheme">
+ <SearchView
+ android:id="@+id/search_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:iconifiedByDefault="false"
+ android:imeOptions="actionSearch|flagNoExtractUi"
+ android:searchIcon="@null"/>
+ </Toolbar>
+ </android.support.v7.widget.CardView>
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/layout_results"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <!-- Padding is included in the background -->
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/list_results"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/dashboard_padding_start"
+ android:paddingEnd="@dimen/dashboard_padding_end"
+ android:paddingTop="@dimen/dashboard_padding_top"
+ android:paddingBottom="@dimen/dashboard_padding_bottom"
+ android:scrollbarStyle="outsideOverlay"
+ android:scrollbars="vertical"/>
+
+ <LinearLayout
+ android:id="@+id/no_results_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="35dp"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/actionBarSize"/>
+
+ <ImageView
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:src="@drawable/empty_search_results"/>
+
+ <TextView
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:paddingTop="24dp"
+ android:textSize="18sp"
+ android:text="@string/search_no_results"
+ android:gravity="center"/>
+
+ </LinearLayout>
+
+ </FrameLayout>
+
+ <include layout="@layout/search_feedback"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/search_saved_query_item.xml b/res/layout/search_saved_query_item.xml
new file mode 100644
index 0000000..f8c239a
--- /dev/null
+++ b/res/layout/search_saved_query_item.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:background="?android:attr/selectableItemBackground"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical">
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/dashboard_tile_image_size"
+ android:layout_height="@dimen/dashboard_tile_image_size"
+ android:scaleType="centerInside"
+ android:layout_marginStart="@dimen/dashboard_tile_image_margin"
+ android:layout_marginEnd="@dimen/dashboard_tile_image_margin"
+ android:src="@drawable/ic_restore" />
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:gravity="center_vertical"
+ android:textAppearance="?android:attr/textAppearanceListItem" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
new file mode 100644
index 0000000..947fadf
--- /dev/null
+++ b/res/values/attrs.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2017 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>
+<!-- For Search -->
+ <declare-styleable name="Preference">
+ <attr name="keywords" format="string" />
+ </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 7013b15..e090c23 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -17,4 +17,6 @@
<resources>
<color name="launcher_background_color">#ff009688</color>
+
+ <color name="search_panel_background">#f2f2f2</color>
</resources>
\ No newline at end of file
diff --git a/res/values/configs.xml b/res/values/configs.xml
new file mode 100644
index 0000000..eaafe2f
--- /dev/null
+++ b/res/values/configs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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>
+ <!-- Fully-qualified class name for the implementation of the FeatureFactory to be instantiated. -->
+ <string name="config_featureFactory" translatable="false">com.android.settings.intelligence.overlay.FeatureFactoryImpl</string>
+</resources>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100644
index 0000000..8a40eb6
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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>
+
+ <!-- Dashboard padding in its container -->
+ <dimen name="dashboard_padding_start">0dp</dimen>
+ <dimen name="dashboard_padding_end">0dp</dimen>
+ <dimen name="dashboard_padding_top">0dp</dimen>
+ <dimen name="dashboard_padding_bottom">0dp</dimen>
+
+ <!-- The following two margins need to match, with the caveat that
+ the second should be negative. The second one ensures that the icons and text
+ align despite the additional padding caused by the search bar's card background. -->
+ <dimen name="search_bar_margin">8dp</dimen>
+ <dimen name="search_bar_negative_margin">-8dp</dimen>
+
+ <dimen name="search_bar_height">48dp</dimen>
+ <dimen name="search_bar_text_size">16dp</dimen>
+
+ <!-- Dashboard image tile size -->
+ <dimen name="dashboard_tile_image_size">24dp</dimen>
+
+ <!-- Dashboard tile image margin start / end -->
+ <dimen name="dashboard_tile_image_margin">24dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7fa89ab..3c05a31 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -15,7 +15,34 @@
limitations under the License.
-->
-<resources>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Applications settings title. If clicked, the user is taken to a screen full of application settings [CHAR_LIMIT=NONE BACKUP_MESSAGE_ID=5281808652705396152] -->
+ <string name="applications_settings">App info</string>
+
+ <!-- Settings title in main settings screen for accessibility settings [CHAR_LIMIT=NONE BACKUP_MESSAGE_ID=3975902491934816215] -->
+ <string name="accessibility_settings">Accessibility</string>
+
+ <!-- Title for the 'physical keyboard' settings screen. [CHAR LIMIT=35 BACKUP_MESSAGE_ID=8285149877925752042] -->
+ <string name="physical_keyboard_title">Physical keyboard</string>
+
+ <!-- Title for the button to trigger the 'Manage keyboards' preference sub-screen, where the user can turn on/off installed virtual keyboards.[CHAR LIMIT=35 BACKUP_MESSAGE_ID=3302152381456516928] -->
+ <string name="add_virtual_keyboard">Manage keyboards</string>
+
<!-- App name of SettingsIntelligence [CHAR_LIMIT=30] -->
<string name="app_name_settings_intelligence">Settings Intelligence</string>
+
+ <!-- SEARCH STRINGS -->
+
+ <!-- Button to clear all search history in Settings [CHAR LIMIT=40]-->
+ <string name="search_clear_history">Clear history</string>
+
+ <!-- DO NOT TRANSLATE Summary placeholder -->
+ <string name="summary_placeholder" translatable="false"> </string>
+
+ <!-- Search breadcrumb connector symbol -->
+ <string name="search_breadcrumb_connector" translatable="false">
+ <xliff:g name="first_item">%1$s</xliff:g> > <xliff:g name="second_item">%2$s</xliff:g>
+ </string>
+
+ <string name="search_no_results">No results</string>
</resources>
\ No newline at end of file
diff --git a/res/values/themes.xml b/res/values/themes.xml
new file mode 100644
index 0000000..4e8ae4e
--- /dev/null
+++ b/res/values/themes.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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>
+ <style name="Theme.Settings" parent="@android:style/Theme.DeviceDefault.Settings"/>
+
+ <style name="Theme.Settings.NoActionBar">
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/SettingsIntelligenceDumpService.java b/src/com/android/settings/intelligence/SettingsIntelligenceDumpService.java
new file mode 100644
index 0000000..3ece045
--- /dev/null
+++ b/src/com/android/settings/intelligence/SettingsIntelligenceDumpService.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.IBinder;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.settings.intelligence.suggestions.model.SuggestionCategory;
+import com.android.settings.intelligence.suggestions.model.SuggestionCategoryRegistry;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * A service that reacts to adb shell dumpsys.
+ * <p/>
+ * adb shell am startservice com.android.settings.intelligence/\
+ * .SettingsIntelligenceDumpService
+ * adb shell dumpsys activity service com.android.settings.intelligence/\
+ * .SettingsIntelligenceDumpService
+ */
+public class SettingsIntelligenceDumpService extends Service {
+
+ private static final String KEY_SUGGESTION_CATEGORY = "suggestion_category: ";
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ final StringBuilder dump = new StringBuilder();
+ dump.append(getString(R.string.app_name_settings_intelligence))
+ .append('\n')
+ .append(dumpSuggestions());
+ writer.println(dump.toString());
+ }
+
+ @VisibleForTesting
+ String dumpSuggestions() {
+ final StringBuilder dump = new StringBuilder();
+ final PackageManager pm = getPackageManager();
+ dump.append(" suggestion dump\n");
+ for (SuggestionCategory category : SuggestionCategoryRegistry.CATEGORIES) {
+ dump.append(KEY_SUGGESTION_CATEGORY)
+ .append(category.getCategory())
+ .append('\n');
+
+ final Intent probe = new Intent(Intent.ACTION_MAIN);
+ probe.addCategory(category.getCategory());
+ final List<ResolveInfo> results = pm
+ .queryIntentActivities(probe, PackageManager.GET_META_DATA);
+ if (results == null || results.isEmpty()) {
+ continue;
+ }
+ for (ResolveInfo info : results) {
+ dump.append("\t\t")
+ .append(info.activityInfo.packageName)
+ .append('/')
+ .append(info.activityInfo.name)
+ .append('\n');
+ }
+ }
+ return dump.toString();
+ }
+}
diff --git a/tests/robotests/src/android/service/settings/suggestions/SuggestionService.java b/src/com/android/settings/intelligence/experiment/ExperimentFeatureProvider.java
similarity index 61%
rename from tests/robotests/src/android/service/settings/suggestions/SuggestionService.java
rename to src/com/android/settings/intelligence/experiment/ExperimentFeatureProvider.java
index e74f1fa..f3b9958 100644
--- a/tests/robotests/src/android/service/settings/suggestions/SuggestionService.java
+++ b/src/com/android/settings/intelligence/experiment/ExperimentFeatureProvider.java
@@ -14,18 +14,18 @@
* limitations under the License.
*/
-package android.service.settings.suggestions;
+package com.android.settings.intelligence.experiment;
-import android.app.Service;
+import android.content.Context;
-import java.util.List;
+public class ExperimentFeatureProvider {
-/**
- * Dupe to android.service.settings.suggestions.SuggestionService to get around robolectric problem.
- */
-public abstract class SuggestionService extends Service {
+ public long getMaxSuggestionDisplayCount(Context context) {
+ return 3;
+ }
- public abstract List<Suggestion> onGetSuggestions();
+ public boolean isPredictiveRingerEnabled(Context context) { return false; }
- public abstract void onSuggestionDismissed(Suggestion suggestion);
+ public boolean isPredictiveBluetoothDrivingEnabled(Context context) { return false; }
+
}
diff --git a/tests/robotests/src/com/android/settings/intelligence/TestConfig.java b/src/com/android/settings/intelligence/instrumentation/EventLogger.java
similarity index 70%
copy from tests/robotests/src/com/android/settings/intelligence/TestConfig.java
copy to src/com/android/settings/intelligence/instrumentation/EventLogger.java
index 44d238b..3dadee1 100644
--- a/tests/robotests/src/com/android/settings/intelligence/TestConfig.java
+++ b/src/com/android/settings/intelligence/instrumentation/EventLogger.java
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-package com.android.settings.intelligence;
+package com.android.settings.intelligence.instrumentation;
-/**
- * Constants for Robolectric config.
- */
-public class TestConfig {
- public static final int SDK_VERSION = 26;
- public static final String MANIFEST_PATH = "--default";
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto
+ .SettingsIntelligenceEvent;
+
+public interface EventLogger {
+
+ void log(SettingsIntelligenceEvent event);
}
diff --git a/src/com/android/settings/intelligence/instrumentation/LocalEventLogger.java b/src/com/android/settings/intelligence/instrumentation/LocalEventLogger.java
new file mode 100644
index 0000000..22e9eb7
--- /dev/null
+++ b/src/com/android/settings/intelligence/instrumentation/LocalEventLogger.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.instrumentation;
+
+import android.util.Log;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+
+public class LocalEventLogger implements EventLogger {
+
+ private static final String TAG = "SettingsIntLogLocal";
+
+ @Override
+ public void log(SettingsIntelligenceLogProto.SettingsIntelligenceEvent event) {
+ Log.i(TAG, event.toString());
+ }
+}
diff --git a/src/com/android/settings/intelligence/instrumentation/MetricsFeatureProvider.java b/src/com/android/settings/intelligence/instrumentation/MetricsFeatureProvider.java
new file mode 100644
index 0000000..6231620
--- /dev/null
+++ b/src/com/android/settings/intelligence/instrumentation/MetricsFeatureProvider.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.instrumentation;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto
+ .SettingsIntelligenceEvent;
+import com.android.settings.intelligence.search.SearchResult;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MetricsFeatureProvider {
+
+ protected Context mContext;
+ protected List<EventLogger> mLoggers;
+
+ public MetricsFeatureProvider(Context context) {
+ mContext = context;
+ mLoggers = new ArrayList<>();
+ mLoggers.add(new LocalEventLogger());
+ }
+
+ public void logGetSuggestion(List<String> ids, long latency) {
+ final SettingsIntelligenceEvent event = new SettingsIntelligenceEvent();
+ event.eventType = SettingsIntelligenceEvent.GET_SUGGESTION;
+ event.latencyMillis = latency;
+ if (ids != null) {
+ event.suggestionIds = ids.toArray(new String[0]);
+ }
+ logEvent(event);
+ }
+
+ public void logDismissSuggestion(String id, long latency) {
+ final SettingsIntelligenceEvent event = new SettingsIntelligenceEvent();
+ event.eventType = SettingsIntelligenceEvent.DISMISS_SUGGESTION;
+ event.latencyMillis = latency;
+ if (!TextUtils.isEmpty(id)) {
+ event.suggestionIds = new String[]{id};
+ }
+ logEvent(event);
+ }
+
+ public void logLaunchSuggestion(String id, long latency) {
+ final SettingsIntelligenceEvent event = new SettingsIntelligenceEvent();
+ event.eventType = SettingsIntelligenceEvent.LAUNCH_SUGGESTION;
+ event.latencyMillis = latency;
+ if (!TextUtils.isEmpty(id)) {
+ event.suggestionIds = new String[]{id};
+ }
+ logEvent(event);
+ }
+
+ public void logSearchResultClick(SearchResult result, String query, int type, int count,
+ int rank) {
+ final SettingsIntelligenceEvent event = new SettingsIntelligenceEvent();
+ event.eventType = type;
+ event.searchResultMetadata = new SettingsIntelligenceEvent.SearchResultMetadata();
+ event.searchResultMetadata.resultCount = count;
+ event.searchResultMetadata.searchResultRank = rank;
+ event.searchResultMetadata.searchResultKey = result.dataKey != null ? result.dataKey : "";
+ event.searchResultMetadata.searchQueryLength = query != null ? query.length() : 0;
+ logEvent(event);
+ }
+
+ public void logEvent(int eventType) {
+ logEvent(eventType, 0 /* latency */);
+ }
+
+ public void logEvent(int eventType, long latency) {
+ final SettingsIntelligenceEvent event = new SettingsIntelligenceEvent();
+ event.eventType = eventType;
+ event.latencyMillis = latency;
+ logEvent(event);
+ }
+
+ private void logEvent(SettingsIntelligenceEvent event) {
+ for (EventLogger logger : mLoggers) {
+ logger.log(event);
+ }
+ }
+}
diff --git a/src/com/android/settings/intelligence/overlay/FeatureFactory.java b/src/com/android/settings/intelligence/overlay/FeatureFactory.java
new file mode 100644
index 0000000..e144f39
--- /dev/null
+++ b/src/com/android/settings/intelligence/overlay/FeatureFactory.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.overlay;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.experiment.ExperimentFeatureProvider;
+import com.android.settings.intelligence.instrumentation.MetricsFeatureProvider;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.suggestions.SuggestionFeatureProvider;
+
+public abstract class FeatureFactory {
+
+ private static final String LOG_TAG = "FeatureFactory";
+ private static final boolean DEBUG = false;
+
+ protected static FeatureFactory sFactory;
+
+ /**
+ * Returns a factory for creating feature controllers. Creates the factory if it does not
+ * already exist. Uses the value of {@link R.string#config_featureFactory} to instantiate
+ * a factory implementation.
+ */
+ public static FeatureFactory get(Context context) {
+ if (sFactory != null) {
+ return sFactory;
+ }
+
+ if (DEBUG) {
+ Log.d(LOG_TAG, "getFactory");
+ }
+ final String clsName = context.getString(R.string.config_featureFactory);
+ if (TextUtils.isEmpty(clsName)) {
+ throw new UnsupportedOperationException("No feature factory configured");
+ }
+ try {
+ sFactory = (FeatureFactory) context.getClassLoader().loadClass(clsName).newInstance();
+ } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
+ throw new FactoryNotFoundException(e);
+ }
+
+ if (DEBUG) {
+ Log.d(LOG_TAG, "started " + sFactory.getClass().getSimpleName());
+ }
+ return sFactory;
+ }
+
+ public abstract MetricsFeatureProvider metricsFeatureProvider(Context context);
+
+ public abstract SuggestionFeatureProvider suggestionFeatureProvider();
+
+ public abstract ExperimentFeatureProvider experimentFeatureProvider();
+
+ public abstract SearchFeatureProvider searchFeatureProvider();
+
+ public static final class FactoryNotFoundException extends RuntimeException {
+ public FactoryNotFoundException(Throwable throwable) {
+ super("Unable to create factory. Did you misconfigure Proguard?", throwable);
+ }
+ }
+
+}
diff --git a/src/com/android/settings/intelligence/overlay/FeatureFactoryImpl.java b/src/com/android/settings/intelligence/overlay/FeatureFactoryImpl.java
new file mode 100644
index 0000000..99798e7
--- /dev/null
+++ b/src/com/android/settings/intelligence/overlay/FeatureFactoryImpl.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.overlay;
+
+import android.content.Context;
+import android.support.annotation.Keep;
+
+import com.android.settings.intelligence.experiment.ExperimentFeatureProvider;
+import com.android.settings.intelligence.instrumentation.MetricsFeatureProvider;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchFeatureProviderImpl;
+import com.android.settings.intelligence.suggestions.SuggestionFeatureProvider;
+
+@Keep
+public class FeatureFactoryImpl extends FeatureFactory {
+
+ protected MetricsFeatureProvider mMetricsFeatureProvider;
+ protected SuggestionFeatureProvider mSuggestionFeatureProvider;
+ protected ExperimentFeatureProvider mExperimentFeatureProvider;
+ protected SearchFeatureProvider mSearchFeatureProvider;
+
+ @Override
+ public MetricsFeatureProvider metricsFeatureProvider(Context context) {
+ if (mMetricsFeatureProvider == null) {
+ mMetricsFeatureProvider = new MetricsFeatureProvider(context.getApplicationContext());
+ }
+ return mMetricsFeatureProvider;
+ }
+
+ @Override
+ public SuggestionFeatureProvider suggestionFeatureProvider() {
+ if (mSuggestionFeatureProvider == null) {
+ mSuggestionFeatureProvider = new SuggestionFeatureProvider();
+ }
+ return mSuggestionFeatureProvider;
+ }
+
+ @Override
+ public ExperimentFeatureProvider experimentFeatureProvider() {
+ if (mExperimentFeatureProvider == null) {
+ mExperimentFeatureProvider = new ExperimentFeatureProvider();
+ }
+ return mExperimentFeatureProvider;
+ }
+
+ @Override
+ public SearchFeatureProvider searchFeatureProvider() {
+ if (mSearchFeatureProvider == null) {
+ mSearchFeatureProvider = new SearchFeatureProviderImpl();
+ }
+ return mSearchFeatureProvider;
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/AppSearchResult.java b/src/com/android/settings/intelligence/search/AppSearchResult.java
new file mode 100644
index 0000000..567fd70
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/AppSearchResult.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.content.pm.ApplicationInfo;
+
+public class AppSearchResult extends SearchResult {
+ /**
+ * Installed app's ApplicationInfo for delayed loading of icons
+ */
+ public final ApplicationInfo info;
+
+ public AppSearchResult(Builder builder) {
+ super(builder);
+ info = builder.mInfo;
+ }
+
+ public static class Builder extends SearchResult.Builder {
+ protected ApplicationInfo mInfo;
+
+ public SearchResult.Builder setAppInfo(ApplicationInfo info) {
+ mInfo = info;
+ return this;
+ }
+
+ public AppSearchResult build() {
+ return new AppSearchResult(this);
+ }
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/IntentSearchViewHolder.java b/src/com/android/settings/intelligence/search/IntentSearchViewHolder.java
new file mode 100644
index 0000000..ec02164
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/IntentSearchViewHolder.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import android.view.View;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+
+import java.util.List;
+
+/**
+ * ViewHolder for intent based search results.
+ * The DatabaseResultTask is the primary use case for this ViewHolder.
+ */
+public class IntentSearchViewHolder extends SearchViewHolder {
+
+ private static final String TAG = "IntentSearchViewHolder";
+ @VisibleForTesting
+ static final int REQUEST_CODE_NO_OP = 0;
+
+ public IntentSearchViewHolder(View view) {
+ super(view);
+ }
+
+ @Override
+ public int getClickActionMetricName() {
+ return SettingsIntelligenceLogProto.SettingsIntelligenceEvent.CLICK_SEARCH_RESULT;
+ }
+
+ @Override
+ public void onBind(final SearchFragment fragment, final SearchResult result) {
+ super.onBind(fragment, result);
+ final SearchViewHolder viewHolder = this;
+
+ // TODO (b/64935342) add dynamic api
+ itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ fragment.onSearchResultClicked(viewHolder, result);
+ final Intent intent = result.payload.getIntent();
+ // Use app user id to support work profile use case.
+ if (result instanceof AppSearchResult) {
+ AppSearchResult appResult = (AppSearchResult) result;
+ fragment.getActivity().startActivity(intent);
+ } else {
+ final PackageManager pm = fragment.getActivity().getPackageManager();
+ final List<ResolveInfo> info = pm.queryIntentActivities(intent, 0 /* flags */);
+ if (info != null && !info.isEmpty()) {
+ fragment.startActivityForResult(intent, REQUEST_CODE_NO_OP);
+ } else {
+ Log.e(TAG, "Cannot launch search result, title: "
+ + result.title + ", " + intent);
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/ResultPayload.java b/src/com/android/settings/intelligence/search/ResultPayload.java
new file mode 100644
index 0000000..da5ff60
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/ResultPayload.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.support.annotation.IntDef;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A interface for search results types. Examples include Inline results, third party apps
+ * or any future possibilities.
+ */
+public class ResultPayload implements Parcelable {
+ protected final Intent mIntent;
+
+ @IntDef({PayloadType.INTENT, PayloadType.INLINE_SLIDER, PayloadType.INLINE_SWITCH,
+ PayloadType.INLINE_LIST, PayloadType.SAVED_QUERY})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PayloadType {
+ /**
+ * Resulting page will be started using an mIntent
+ */
+ int INTENT = 0;
+
+ /**
+ * Result is a inline widget, using a slider widget as UI.
+ */
+ int INLINE_SLIDER = 1;
+
+ /**
+ * Result is a inline widget, using a toggle widget as UI.
+ */
+ int INLINE_SWITCH = 2;
+
+ /**
+ * Result is an inline list-select, with an undefined UI.
+ */
+ int INLINE_LIST = 3;
+
+ /**
+ * Result is a recently saved query.
+ */
+ int SAVED_QUERY = 4;
+ }
+
+ /**
+ * Enumerates the possible values for the Availability of a setting.
+ */
+ @IntDef({Availability.AVAILABLE,
+ Availability.DISABLED_DEPENDENT_SETTING,
+ Availability.DISABLED_DEPENDENT_APP,
+ Availability.DISABLED_UNSUPPORTED,
+ Availability.RESOURCE_CONTENTION,
+ Availability.INTENT_ONLY,
+ Availability.DISABLED_FOR_USER,})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Availability {
+ /**
+ * The setting is available.
+ */
+ int AVAILABLE = 0;
+
+ /**
+ * The setting has a dependency in settings app which is currently disabled, blocking
+ * access.
+ */
+ int DISABLED_DEPENDENT_SETTING = 1;
+
+ /**
+ * The setting is not supported by the device.
+ */
+ int DISABLED_UNSUPPORTED = 2;
+
+ /**
+ * The setting you are trying to change is being used by another application and cannot
+ * be changed until it is released by said application.
+ */
+ int RESOURCE_CONTENTION = 3;
+
+ /**
+ * The setting is disabled because corresponding app is disabled.
+ */
+ int DISABLED_DEPENDENT_APP = 4;
+
+ /**
+ * This setting is supported on the device but cannot be changed inline.
+ */
+ int INTENT_ONLY = 5;
+
+ /**
+ * The setting cannot be changed by the current user.
+ * ex: MobileNetworkTakeMeThereSetting should not be available to a secondary user.
+ */
+ int DISABLED_FOR_USER = 6;
+ }
+
+ @IntDef({SettingsSource.UNKNOWN, SettingsSource.SYSTEM, SettingsSource.SECURE,
+ SettingsSource.GLOBAL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SettingsSource {
+ int UNKNOWN = 0;
+ int SYSTEM = 1;
+ int SECURE = 2;
+ int GLOBAL = 3;
+ }
+
+
+ private ResultPayload(Parcel in) {
+ mIntent = in.readParcelable(ResultPayload.class.getClassLoader());
+ }
+
+ public ResultPayload(Intent intent) {
+ mIntent = intent;
+ }
+
+ @ResultPayload.PayloadType
+ public int getType() {
+ return PayloadType.INTENT;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mIntent, flags);
+ }
+
+ public static final Creator<ResultPayload> CREATOR = new Creator<ResultPayload>() {
+ @Override
+ public ResultPayload createFromParcel(Parcel in) {
+ return new ResultPayload(in);
+ }
+
+ @Override
+ public ResultPayload[] newArray(int size) {
+ return new ResultPayload[size];
+ }
+ };
+
+ public Intent getIntent() {
+ return mIntent;
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/ResultPayloadUtils.java b/src/com/android/settings/intelligence/search/ResultPayloadUtils.java
new file mode 100644
index 0000000..096edd5
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/ResultPayloadUtils.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Utility class to Marshall and Unmarshall the payloads stored in the SQLite Database
+ */
+public class ResultPayloadUtils {
+
+ private static final String TAG = "PayloadUtil";
+
+ public static byte[] marshall(ResultPayload payload) {
+ Parcel parcel = Parcel.obtain();
+ payload.writeToParcel(parcel, 0);
+ byte[] bytes = parcel.marshall();
+ parcel.recycle();
+ return bytes;
+ }
+
+ public static <T> T unmarshall(byte[] bytes, Parcelable.Creator<T> creator) {
+ T result;
+ Parcel parcel = unmarshall(bytes);
+ result = creator.createFromParcel(parcel);
+ parcel.recycle();
+ return result;
+ }
+
+ private static Parcel unmarshall(byte[] bytes) {
+ Parcel parcel = Parcel.obtain();
+ parcel.unmarshall(bytes, 0, bytes.length);
+ parcel.setDataPosition(0);
+ return parcel;
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchActivity.java b/src/com/android/settings/intelligence/search/SearchActivity.java
new file mode 100644
index 0000000..3521ef3
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchActivity.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import com.android.settings.intelligence.R;
+
+public class SearchActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.search_main);
+ // Keeps layouts in-place when keyboard opens.
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
+
+ FragmentManager fragmentManager = getFragmentManager();
+ Fragment fragment = fragmentManager.findFragmentById(R.id.main_content);
+ if (fragment == null) {
+ fragmentManager.beginTransaction()
+ .add(R.id.main_content, new SearchFragment())
+ .commit();
+ }
+ }
+
+ @Override
+ public boolean onNavigateUp() {
+ finish();
+ return true;
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchFeatureProvider.java b/src/com/android/settings/intelligence/search/SearchFeatureProvider.java
new file mode 100644
index 0000000..2ccac53
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchFeatureProvider.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.content.Context;
+import android.util.Pair;
+import android.view.View;
+
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingManager;
+import com.android.settings.intelligence.search.indexing.IndexingCallback;
+import com.android.settings.intelligence.search.query.SearchQueryTask;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryLoader;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.FutureTask;
+
+/**
+ * FeatureProvider for Settings Search
+ */
+public interface SearchFeatureProvider {
+
+ boolean DEBUG = false;
+
+ /**
+ * Returns a new loader to get settings search results.
+ */
+ SearchResultLoader getSearchResultLoader(Context context, String query);
+
+ /**
+ * Returns a list of {@link SearchQueryTask}, each responsible for searching a subsystem for
+ * user query.
+ */
+ List<SearchQueryTask> getSearchQueryTasks(Context context, String query);
+
+ /**
+ * Returns a new loader to get all recently saved queries search terms.
+ */
+ SavedQueryLoader getSavedQueryLoader(Context context);
+
+ /**
+ * Returns the manager for indexing Settings data.
+ */
+ DatabaseIndexingManager getIndexingManager(Context context);
+
+ /**
+ * Returns the manager for looking up breadcrumbs.
+ */
+ SiteMapManager getSiteMapManager();
+
+ /**
+ * Updates the Settings indexes and calls {@link IndexingCallback#onIndexingFinished()} on
+ * {@param callback} when indexing is complete.
+ */
+ void updateIndexAsync(Context context, IndexingCallback callback);
+
+ /**
+ * @returns true when indexing is complete.
+ */
+ boolean isIndexingComplete(Context context);
+
+ /**
+ * @return a {@link ExecutorService} to be shared between search tasks.
+ */
+ ExecutorService getExecutorService();
+
+ /**
+ * Initializes the feedback button in case it was dismissed.
+ */
+ void initFeedbackButton();
+
+ /**
+ * Show a button users can click to submit feedback on the quality of the search results.
+ */
+ void showFeedbackButton(SearchFragment fragment, View root);
+
+ /**
+ * Hide the feedback button shown by
+ * {@link #showFeedbackButton(SearchFragment fragment, View view) showFeedbackButton}
+ */
+ void hideFeedbackButton(View root);
+
+ /**
+ * Notify that a search result is clicked.
+ *
+ * @param context application context
+ * @param query input user query
+ * @param searchResult clicked result
+ */
+ void searchResultClicked(Context context, String query, SearchResult searchResult);
+
+ /**
+ * @return true to enable search ranking.
+ */
+ boolean isSmartSearchRankingEnabled(Context context);
+
+ /**
+ * @return smart ranking timeout in milliseconds.
+ */
+ long smartSearchRankingTimeoutMs(Context context);
+
+ /**
+ * Prepare for search ranking predictions to avoid latency on the first prediction call.
+ */
+ void searchRankingWarmup(Context context);
+
+ /**
+ * Return a FutureTask to get a list of scores for search results.
+ */
+ FutureTask<List<Pair<String, Float>>> getRankerTask(Context context, String query);
+}
diff --git a/src/com/android/settings/intelligence/search/SearchFeatureProviderImpl.java b/src/com/android/settings/intelligence/search/SearchFeatureProviderImpl.java
new file mode 100644
index 0000000..0489117
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchFeatureProviderImpl.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.view.View;
+
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingManager;
+import com.android.settings.intelligence.search.indexing.IndexData;
+import com.android.settings.intelligence.search.indexing.IndexingCallback;
+import com.android.settings.intelligence.search.query.AccessibilityServiceResultTask;
+import com.android.settings.intelligence.search.query.DatabaseResultTask;
+import com.android.settings.intelligence.search.query.InputDeviceResultTask;
+import com.android.settings.intelligence.search.query.InstalledAppResultTask;
+import com.android.settings.intelligence.search.query.SearchQueryTask;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryLoader;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+
+/**
+ * FeatureProvider for the refactored search code.
+ */
+public class SearchFeatureProviderImpl implements SearchFeatureProvider {
+
+ private static final String TAG = "SearchFeatureProvider";
+
+ private DatabaseIndexingManager mDatabaseIndexingManager;
+ private ExecutorService mExecutorService;
+ private SiteMapManager mSiteMapManager;
+
+ @Override
+ public SearchResultLoader getSearchResultLoader(Context context, String query) {
+ return new SearchResultLoader(context, cleanQuery(query));
+ }
+
+ @Override
+ public List<SearchQueryTask> getSearchQueryTasks(Context context, String query) {
+ final List<SearchQueryTask> tasks = new ArrayList<>();
+ final String cleanQuery = cleanQuery(query);
+ tasks.add(DatabaseResultTask.newTask(context, getSiteMapManager(), cleanQuery));
+ tasks.add(InstalledAppResultTask.newTask(context, getSiteMapManager(), cleanQuery));
+ tasks.add(AccessibilityServiceResultTask.newTask(context, getSiteMapManager(), cleanQuery));
+ tasks.add(InputDeviceResultTask.newTask(context, getSiteMapManager(), cleanQuery));
+ return tasks;
+ }
+
+ @Override
+ public SavedQueryLoader getSavedQueryLoader(Context context) {
+ return new SavedQueryLoader(context);
+ }
+
+ @Override
+ public DatabaseIndexingManager getIndexingManager(Context context) {
+ if (mDatabaseIndexingManager == null) {
+ mDatabaseIndexingManager = new DatabaseIndexingManager(context.getApplicationContext());
+ }
+ return mDatabaseIndexingManager;
+ }
+
+ @Override
+ public SiteMapManager getSiteMapManager() {
+ if (mSiteMapManager == null) {
+ mSiteMapManager = new SiteMapManager();
+ }
+ return mSiteMapManager;
+ }
+
+ @Override
+ public boolean isIndexingComplete(Context context) {
+ return getIndexingManager(context).isIndexingComplete();
+ }
+
+ @Override
+ public void initFeedbackButton() {
+ }
+
+ @Override
+ public void showFeedbackButton(SearchFragment fragment, View root) {
+ }
+
+ @Override
+ public void hideFeedbackButton(View root) {
+ }
+
+ @Override
+ public void searchResultClicked(Context context, String query, SearchResult searchResult) {
+ }
+
+ @Override
+ public boolean isSmartSearchRankingEnabled(Context context) {
+ return false;
+ }
+
+ @Override
+ public long smartSearchRankingTimeoutMs(Context context) {
+ return 300L;
+ }
+
+ @Override
+ public void searchRankingWarmup(Context context) {
+ }
+
+ @Override
+ public FutureTask<List<Pair<String, Float>>> getRankerTask(Context context, String query) {
+ return null;
+ }
+
+ @Override
+ public void updateIndexAsync(Context context, IndexingCallback callback) {
+ if (DEBUG) {
+ Log.d(TAG, "updating index async");
+ }
+ getIndexingManager(context).indexDatabase(callback);
+ }
+
+ @Override
+ public ExecutorService getExecutorService() {
+ if (mExecutorService == null) {
+ mExecutorService = Executors.newCachedThreadPool();
+ }
+ return mExecutorService;
+ }
+
+ /**
+ * A generic method to make the query suitable for searching the database.
+ *
+ * @return the cleaned query string
+ */
+ @VisibleForTesting
+ String cleanQuery(String query) {
+ if (TextUtils.isEmpty(query)) {
+ return null;
+ }
+ if (Locale.getDefault().equals(Locale.JAPAN)) {
+ query = IndexData.normalizeJapaneseString(query);
+ }
+ return query.trim();
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchFragment.java b/src/com/android/settings/intelligence/search/SearchFragment.java
new file mode 100644
index 0000000..e92e301
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchFragment.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import static com.android.settings.intelligence.nano.SettingsIntelligenceLogProto
+ .SettingsIntelligenceEvent;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.Loader;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.LinearLayout;
+import android.widget.SearchView;
+import android.widget.TextView;
+import android.widget.Toolbar;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.instrumentation.MetricsFeatureProvider;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.indexing.IndexingCallback;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryController;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryViewHolder;
+
+import java.util.List;
+
+/**
+ * This fragment manages the lifecycle of indexing and searching.
+ *
+ * In onCreate, the indexing process is initiated in DatabaseIndexingManager.
+ * While the indexing is happening, loaders are blocked from accessing the database, but the user
+ * is free to start typing their query.
+ *
+ * When the indexing is complete, the fragment gets a callback to initialize the loaders and search
+ * the query if the user has entered text.
+ */
+public class SearchFragment extends Fragment implements SearchView.OnQueryTextListener,
+ LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
+ private static final String TAG = "SearchFragment";
+
+ // State values
+ private static final String STATE_QUERY = "state_query";
+ private static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
+ private static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
+
+ public static final class SearchLoaderId {
+ // Search Query IDs
+ public static final int SEARCH_RESULT = 1;
+
+ // Saved Query IDs
+ public static final int SAVE_QUERY_TASK = 2;
+ public static final int REMOVE_QUERY_TASK = 3;
+ public static final int SAVED_QUERIES = 4;
+ }
+
+ @VisibleForTesting
+ String mQuery;
+
+ private boolean mNeverEnteredQuery = true;
+ private long mEnterQueryTimestampMs;
+
+ @VisibleForTesting
+ boolean mShowingSavedQuery;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+ @VisibleForTesting
+ SavedQueryController mSavedQueryController;
+
+ @VisibleForTesting
+ SearchFeatureProvider mSearchFeatureProvider;
+
+ @VisibleForTesting
+ SearchResultsAdapter mSearchAdapter;
+
+ @VisibleForTesting
+ RecyclerView mResultsRecyclerView;
+ @VisibleForTesting
+ SearchView mSearchView;
+ @VisibleForTesting
+ LinearLayout mNoResultsView;
+
+ @VisibleForTesting
+ final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (dy != 0) {
+ hideKeyboard();
+ }
+ }
+ };
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mSearchFeatureProvider = FeatureFactory.get(context).searchFeatureProvider();
+ mMetricsFeatureProvider = FeatureFactory.get(context).metricsFeatureProvider(context);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ long startTime = System.currentTimeMillis();
+ setHasOptionsMenu(true);
+
+ final LoaderManager loaderManager = getLoaderManager();
+ mSearchAdapter = new SearchResultsAdapter(this /* fragment */);
+ mSavedQueryController = new SavedQueryController(
+ getContext(), loaderManager, mSearchAdapter);
+ mSearchFeatureProvider.initFeedbackButton();
+
+ if (savedInstanceState != null) {
+ mQuery = savedInstanceState.getString(STATE_QUERY);
+ mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
+ mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY);
+ } else {
+ mShowingSavedQuery = true;
+ }
+ mSearchFeatureProvider.updateIndexAsync(getContext(), this /* indexingCallback */);
+ if (SearchFeatureProvider.DEBUG) {
+ Log.d(TAG, "onCreate spent " + (System.currentTimeMillis() - startTime) + " ms");
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ mSavedQueryController.buildMenuItem(menu);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+ final View view = inflater.inflate(R.layout.search_panel, container, false);
+ mResultsRecyclerView = view.findViewById(R.id.list_results);
+ mResultsRecyclerView.setAdapter(mSearchAdapter);
+ mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
+ mResultsRecyclerView.addOnScrollListener(mScrollListener);
+
+ mNoResultsView = view.findViewById(R.id.no_results_layout);
+
+ final Toolbar toolbar = view.findViewById(R.id.search_toolbar);
+ activity.setActionBar(toolbar);
+ activity.getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ mSearchView = toolbar.findViewById(R.id.search_view);
+ mSearchView.setQuery(mQuery, false /* submitQuery */);
+ mSearchView.setOnQueryTextListener(this);
+ mSearchView.requestFocus();
+
+ // TODO Refactor
+// ActionBarShadowController.attachToRecyclerView(
+// view.findViewById(R.id.search_bar_container), getLifecycle(),
+// mResultsRecyclerView);
+ return view;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.OPEN_SEARCH_PAGE);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ Context appContext = getContext().getApplicationContext();
+ if (mSearchFeatureProvider.isSmartSearchRankingEnabled(appContext)) {
+ mSearchFeatureProvider.searchRankingWarmup(appContext);
+ }
+ requery();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.LEAVE_SEARCH_PAGE);
+ final Activity activity = getActivity();
+ if (activity != null && activity.isFinishing()) {
+ if (mNeverEnteredQuery) {
+ mMetricsFeatureProvider.logEvent(
+ SettingsIntelligenceEvent.LEAVE_SEARCH_WITHOUT_QUERY);
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString(STATE_QUERY, mQuery);
+ outState.putBoolean(STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
+ outState.putBoolean(STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
+ }
+
+ @Override
+ public boolean onQueryTextChange(String query) {
+ if (TextUtils.equals(query, mQuery)) {
+ return true;
+ }
+ mEnterQueryTimestampMs = System.currentTimeMillis();
+ final boolean isEmptyQuery = TextUtils.isEmpty(query);
+
+ // Hide no-results-view when the new query is not a super-string of the previous
+ if (mQuery != null
+ && mNoResultsView.getVisibility() == View.VISIBLE
+ && query.length() < mQuery.length()) {
+ mNoResultsView.setVisibility(View.GONE);
+ }
+
+ mNeverEnteredQuery = false;
+ mQuery = query;
+
+ // If indexing is not finished, register the query text, but don't search.
+ if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
+ return true;
+ }
+
+ if (isEmptyQuery) {
+ final LoaderManager loaderManager = getLoaderManager();
+ loaderManager.destroyLoader(SearchLoaderId.SEARCH_RESULT);
+ mShowingSavedQuery = true;
+ mSavedQueryController.loadSavedQueries();
+ mSearchFeatureProvider.hideFeedbackButton(getView());
+ } else {
+ mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.PERFORM_SEARCH);
+ restartLoaders();
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ // Save submitted query.
+ mSavedQueryController.saveQuery(mQuery);
+ hideKeyboard();
+ return true;
+ }
+
+ @Override
+ public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
+ final Activity activity = getActivity();
+
+ switch (id) {
+ case SearchLoaderId.SEARCH_RESULT:
+ return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery);
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
+ List<? extends SearchResult> data) {
+ mSearchAdapter.postSearchResults(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
+ }
+
+ /**
+ * Gets called when Indexing is completed.
+ */
+ @Override
+ public void onIndexingFinished() {
+ if (getActivity() == null) {
+ return;
+ }
+ if (mShowingSavedQuery) {
+ mSavedQueryController.loadSavedQueries();
+ } else {
+ final LoaderManager loaderManager = getLoaderManager();
+ loaderManager.initLoader(SearchLoaderId.SEARCH_RESULT, null /* args */,
+ this /* callback */);
+ }
+
+ requery();
+ }
+
+ public List<SearchResult> getSearchResults() {
+ return mSearchAdapter.getSearchResults();
+ }
+
+ public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result) {
+ logSearchResultClicked(resultViewHolder, result);
+ mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result);
+ mSavedQueryController.saveQuery(mQuery);
+ }
+
+ public void onSearchResultsDisplayed(int resultCount) {
+ final long queryToResultLatencyMs = mEnterQueryTimestampMs > 0
+ ? System.currentTimeMillis() - mEnterQueryTimestampMs
+ : 0;
+ if (resultCount == 0) {
+ mNoResultsView.setVisibility(View.VISIBLE);
+ mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.SHOW_SEARCH_NO_RESULT,
+ queryToResultLatencyMs);
+ } else {
+ mNoResultsView.setVisibility(View.GONE);
+ mResultsRecyclerView.scrollToPosition(0);
+ mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.SHOW_SEARCH_RESULT,
+ queryToResultLatencyMs);
+ }
+ mSearchFeatureProvider.showFeedbackButton(this, getView());
+ }
+
+ public void onSavedQueryClicked(SavedQueryViewHolder vh, CharSequence query) {
+ final String queryString = query.toString();
+ mMetricsFeatureProvider.logEvent(vh.getClickActionMetricName());
+ mSearchView.setQuery(queryString, false /* submit */);
+ onQueryTextChange(queryString);
+ }
+
+ private void restartLoaders() {
+ mShowingSavedQuery = false;
+ final LoaderManager loaderManager = getLoaderManager();
+ loaderManager.restartLoader(
+ SearchLoaderId.SEARCH_RESULT, null /* args */, this /* callback */);
+ }
+
+ public String getQuery() {
+ return mQuery;
+ }
+
+ private void requery() {
+ if (TextUtils.isEmpty(mQuery)) {
+ return;
+ }
+ final String query = mQuery;
+ mQuery = "";
+ onQueryTextChange(query);
+ }
+
+ private void hideKeyboard() {
+ final Activity activity = getActivity();
+ if (activity != null) {
+ View view = activity.getCurrentFocus();
+ InputMethodManager imm = (InputMethodManager)
+ activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+
+ if (mResultsRecyclerView != null) {
+ mResultsRecyclerView.requestFocus();
+ }
+ }
+
+ private void logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result) {
+ final int resultType = resultViewHolder.getClickActionMetricName();
+ final int resultCount = mSearchAdapter.getItemCount();
+ final int resultRank = resultViewHolder.getAdapterPosition();
+ mMetricsFeatureProvider.logSearchResultClick(result, mQuery, resultType, resultCount,
+ resultRank);
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchIndexableRaw.java b/src/com/android/settings/intelligence/search/SearchIndexableRaw.java
new file mode 100644
index 0000000..46ce0c6
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchIndexableRaw.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014 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.settings.intelligence.search;
+
+import android.content.Context;
+import android.provider.SearchIndexableData;
+
+/**
+ * Indexable raw data for Search.
+ *
+ * This is the raw data used by the Indexer and should match its data model.
+ *
+ * See {@link Indexable} and {@link android.provider.SearchIndexableResource}.
+ */
+public class SearchIndexableRaw extends SearchIndexableData {
+
+ /**
+ * Title's raw data.
+ */
+ public String title;
+
+ /**
+ * Summary's raw data when the data is "ON".
+ */
+ public String summaryOn;
+
+ /**
+ * Summary's raw data when the data is "OFF".
+ */
+ public String summaryOff;
+
+ /**
+ * Entries associated with the raw data (when the data can have several values).
+ */
+ public String entries;
+
+ /**
+ * Keywords' raw data.
+ */
+ public String keywords;
+
+ /**
+ * Fragment's or Activity's title associated with the raw data.
+ */
+ public String screenTitle;
+
+ public SearchIndexableRaw(Context context) {
+ super(context);
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchResult.java b/src/com/android/settings/intelligence/search/SearchResult.java
new file mode 100644
index 0000000..14065e8
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchResult.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Data class as an interface for all Search Results.
+ */
+public class SearchResult implements Comparable<SearchResult> {
+
+ private static final String TAG = "SearchResult";
+
+ /**
+ * Defines the lowest rank for a search result to be considered as ranked. Results with ranks
+ * higher than this have no guarantee for sorting order.
+ */
+ public static final int BOTTOM_RANK = 10;
+
+ /**
+ * Defines the highest rank for a search result. Used for special search results only.
+ */
+ public static final int TOP_RANK = 0;
+
+ /**
+ * The title of the result and main text displayed.
+ * Intent Results: Displays as the primary
+ */
+ public final CharSequence title;
+
+ /**
+ * Summary / subtitle text
+ * Intent Results: Displays the text underneath the title
+ */
+ final public CharSequence summary;
+
+ /**
+ * An ordered list of the information hierarchy.
+ * Intent Results: Displayed a hierarchy of selections to reach the setting from the home screen
+ */
+ public final List<String> breadcrumbs;
+
+ /**
+ * A suggestion for the ranking of the result.
+ * Based on Settings Rank:
+ * 1 is a near perfect match
+ * 9 is the weakest match
+ * TODO subject to change
+ */
+ public final int rank;
+
+ /**
+ * Identifier for the recycler view adapter.
+ */
+ @ResultPayload.PayloadType
+ public final int viewType;
+
+ /**
+ * Metadata for the specific result types.
+ */
+ public final ResultPayload payload;
+
+ /**
+ * Result's icon.
+ */
+ public final Drawable icon;
+
+ /**
+ * A unique key for this object.
+ */
+ public final String dataKey;
+
+ protected SearchResult(Builder builder) {
+ dataKey = builder.mDataKey;
+ title = builder.mTitle;
+ summary = builder.mSummary;
+ breadcrumbs = builder.mBreadcrumbs;
+ rank = builder.mRank;
+ icon = builder.mIcon;
+ payload = builder.mResultPayload;
+ viewType = payload.getType();
+ }
+
+ @Override
+ public int compareTo(SearchResult searchResult) {
+ if (searchResult == null) {
+ return -1;
+ }
+ return this.rank - searchResult.rank;
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+ if (!(that instanceof SearchResult)) {
+ return false;
+ }
+ return TextUtils.equals(dataKey, ((SearchResult) that).dataKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return dataKey.hashCode();
+ }
+
+ public static class Builder {
+ private CharSequence mTitle;
+ private CharSequence mSummary;
+ private List<String> mBreadcrumbs;
+ private int mRank = 42;
+ private ResultPayload mResultPayload;
+ private Drawable mIcon;
+ private String mDataKey;
+
+ public Builder setTitle(CharSequence title) {
+ mTitle = title;
+ return this;
+ }
+
+ public Builder setSummary(CharSequence summary) {
+ mSummary = summary;
+ return this;
+ }
+
+ public Builder addBreadcrumbs(List<String> breadcrumbs) {
+ mBreadcrumbs = breadcrumbs;
+ return this;
+ }
+
+ public Builder setRank(int rank) {
+ if (rank >= 0 && rank <= 9) {
+ mRank = rank;
+ }
+ return this;
+ }
+
+ public Builder setIcon(Drawable icon) {
+ mIcon = icon;
+ return this;
+ }
+
+ public Builder setPayload(ResultPayload payload) {
+ mResultPayload = payload;
+ return this;
+ }
+
+ public Builder setDataKey(String key) {
+ mDataKey = key;
+ return this;
+ }
+
+ public SearchResult build() {
+ // Check that all of the mandatory fields are set.
+ if (TextUtils.isEmpty(mTitle)) {
+ throw new IllegalStateException("SearchResult missing title argument");
+ } else if (TextUtils.isEmpty(mDataKey)) {
+ Log.v(TAG, "No data key on SearchResult with title: " + mTitle);
+ throw new IllegalStateException("SearchResult missing stableId argument");
+ } else if (mResultPayload == null) {
+ throw new IllegalStateException("SearchResult missing Payload argument");
+ }
+ return new SearchResult(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/SearchResultAggregator.java b/src/com/android/settings/intelligence/search/SearchResultAggregator.java
new file mode 100644
index 0000000..20b156c
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchResultAggregator.java
@@ -0,0 +1,107 @@
+package com.android.settings.intelligence.search;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.query.DatabaseResultTask;
+import com.android.settings.intelligence.search.query.SearchQueryTask;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Collects the sorted list of all setting search results.
+ */
+public class SearchResultAggregator {
+
+ private static final String TAG = "SearchResultAggregator";
+
+ /**
+ * Timeout for subsequent tasks to allow for fast returning tasks.
+ * TODO(70164062): Tweak the timeout values.
+ */
+ private static final long SHORT_CHECK_TASK_TIMEOUT_MS = 300;
+
+ private static SearchResultAggregator sResultAggregator;
+
+ private SearchResultAggregator() {
+ }
+
+ public static SearchResultAggregator getInstance() {
+ if (sResultAggregator == null) {
+ sResultAggregator = new SearchResultAggregator();
+ }
+
+ return sResultAggregator;
+ }
+
+ @NonNull
+ public synchronized List<? extends SearchResult> fetchResults(Context context, String query) {
+ final SearchFeatureProvider mFeatureProvider = FeatureFactory.get(context)
+ .searchFeatureProvider();
+ final ExecutorService executorService = mFeatureProvider.getExecutorService();
+
+ final List<SearchQueryTask> tasks =
+ mFeatureProvider.getSearchQueryTasks(context, query);
+ // Start tasks
+ for (SearchQueryTask task : tasks) {
+ executorService.execute(task);
+ }
+
+ // Collect results
+ final Map<Integer, List<? extends SearchResult>> taskResults = new ArrayMap<>();
+ final long allTasksStart = System.currentTimeMillis();
+ for (SearchQueryTask task : tasks) {
+ final int taskId = task.getTaskId();
+ try {
+ taskResults.put(taskId,
+ task.get(SHORT_CHECK_TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } catch (TimeoutException | InterruptedException | ExecutionException e) {
+ Log.d(TAG, "Could not retrieve result in time: " + taskId, e);
+ taskResults.put(taskId, Collections.EMPTY_LIST);
+ }
+ }
+
+ // Merge results
+ final long mergeStartTime = System.currentTimeMillis();
+ if (SearchFeatureProvider.DEBUG) {
+ Log.d(TAG, "Total result loader time: " + (mergeStartTime - allTasksStart));
+ }
+ final List<? extends SearchResult> mergedResults = mergeSearchResults(taskResults);
+ if (SearchFeatureProvider.DEBUG) {
+ Log.d(TAG, "Total merge time: " + (System.currentTimeMillis() - mergeStartTime));
+ Log.d(TAG, "Total aggregator time: " + (System.currentTimeMillis() - allTasksStart));
+ }
+
+ return mergedResults;
+ }
+
+ // TODO (b/68255021) scale the dynamic search results ranks
+ private List<? extends SearchResult> mergeSearchResults(
+ Map<Integer, List<? extends SearchResult>> taskResults) {
+
+ final List<SearchResult> searchResults = new ArrayList<>();
+ // First add db results as a special case
+ searchResults.addAll(taskResults.remove(DatabaseResultTask.QUERY_WORKER_ID));
+
+ // Merge the rest into result list: add everything to heap then pop them out one by one.
+ final PriorityQueue<SearchResult> heap = new PriorityQueue<>();
+ for (List<? extends SearchResult> taskResult : taskResults.values()) {
+ heap.addAll(taskResult);
+ }
+ while (!heap.isEmpty()) {
+ searchResults.add(heap.poll());
+ }
+ return searchResults;
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchResultDiffCallback.java b/src/com/android/settings/intelligence/search/SearchResultDiffCallback.java
new file mode 100644
index 0000000..1d2b946
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchResultDiffCallback.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.support.v7.util.DiffUtil;
+
+import java.util.List;
+
+/**
+ * Callback for DiffUtil to elegantly update search data when the query changes.
+ */
+public class SearchResultDiffCallback extends DiffUtil.Callback {
+
+ private List<? extends SearchResult> mOldList;
+ private List<? extends SearchResult> mNewList;
+
+ public SearchResultDiffCallback(List<? extends SearchResult> oldList,
+ List<? extends SearchResult> newList) {
+ mOldList = oldList;
+ mNewList = newList;
+ }
+
+ @Override
+ public int getOldListSize() {
+ return mOldList.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return mNewList.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+ return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+ return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchResultLoader.java b/src/com/android/settings/intelligence/search/SearchResultLoader.java
new file mode 100644
index 0000000..92104cd
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchResultLoader.java
@@ -0,0 +1,31 @@
+package com.android.settings.intelligence.search;
+
+
+import android.content.Context;
+
+import com.android.settings.intelligence.utils.AsyncLoader;
+
+import java.util.List;
+
+/**
+ * Loads a sorted list of Search results for a given query.
+ */
+public class SearchResultLoader extends AsyncLoader<List<? extends SearchResult>> {
+
+ private final String mQuery;
+
+ public SearchResultLoader(Context context, String query) {
+ super(context);
+ mQuery = query;
+ }
+
+ @Override
+ public List<? extends SearchResult> loadInBackground() {
+ SearchResultAggregator aggregator = SearchResultAggregator.getInstance();
+ return aggregator.fetchResults(getContext(), mQuery);
+ }
+
+ @Override
+ protected void onDiscardResult(List<? extends SearchResult> result) {
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchResultsAdapter.java b/src/com/android/settings/intelligence/search/SearchResultsAdapter.java
new file mode 100644
index 0000000..decf730
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchResultsAdapter.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.content.Context;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryViewHolder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder> {
+
+ private final SearchFragment mFragment;
+ private final List<SearchResult> mSearchResults;
+
+ public SearchResultsAdapter(SearchFragment fragment) {
+ mFragment = fragment;
+ mSearchResults = new ArrayList<>();
+
+ setHasStableIds(true);
+ }
+
+ @Override
+ public SearchViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ final Context context = parent.getContext();
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ final View view;
+ switch (viewType) {
+ case ResultPayload.PayloadType.INTENT:
+ view = inflater.inflate(R.layout.search_intent_item, parent, false);
+ return new IntentSearchViewHolder(view);
+ case ResultPayload.PayloadType.INLINE_SWITCH:
+ // TODO (b/62807132) replace layout InlineSwitchViewHolder and return an
+ // InlineSwitchViewHolder.
+ view = inflater.inflate(R.layout.search_intent_item, parent, false);
+ return new IntentSearchViewHolder(view);
+ case ResultPayload.PayloadType.INLINE_LIST:
+ // TODO (b/62807132) build a inline-list view holder & layout.
+ view = inflater.inflate(R.layout.search_intent_item, parent, false);
+ return new IntentSearchViewHolder(view);
+ case ResultPayload.PayloadType.SAVED_QUERY:
+ view = inflater.inflate(R.layout.search_saved_query_item, parent, false);
+ return new SavedQueryViewHolder(view);
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(SearchViewHolder holder, int position) {
+ holder.onBind(mFragment, mSearchResults.get(position));
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mSearchResults.get(position).hashCode();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mSearchResults.get(position).viewType;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mSearchResults.size();
+ }
+
+ /**
+ * Displays recent searched queries.
+ */
+ public void displaySavedQuery(List<? extends SearchResult> data) {
+ clearResults();
+ mSearchResults.addAll(data);
+ notifyDataSetChanged();
+ }
+
+ public void clearResults() {
+ mSearchResults.clear();
+ notifyDataSetChanged();
+ }
+
+ public void postSearchResults(List<? extends SearchResult> newSearchResults) {
+ final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
+ new SearchResultDiffCallback(mSearchResults, newSearchResults));
+ mSearchResults.clear();
+ mSearchResults.addAll(newSearchResults);
+ diffResult.dispatchUpdatesTo(this);
+ mFragment.onSearchResultsDisplayed(mSearchResults.size());
+ }
+
+ public List<SearchResult> getSearchResults() {
+ return mSearchResults;
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchViewHolder.java b/src/com/android/settings/intelligence/search/SearchViewHolder.java
new file mode 100644
index 0000000..c74391e
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchViewHolder.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+
+/**
+ * The ViewHolder for the Search RecyclerView.
+ * There are multiple search result types in the same Recycler view with different UI requirements.
+ * Some examples include Intent results, Inline results, and Help articles.
+ */
+public abstract class SearchViewHolder extends RecyclerView.ViewHolder {
+
+ private final String DYNAMIC_PLACEHOLDER = "%s";
+
+ private final String mPlaceholderSummary;
+
+ public final TextView titleView;
+ public final TextView summaryView;
+ public final TextView breadcrumbView;
+ public final ImageView iconView;
+
+ protected final SearchFeatureProvider mSearchFeatureProvider;
+
+ public SearchViewHolder(View view) {
+ super(view);
+ final FeatureFactory featureFactory = FeatureFactory
+ .get(view.getContext().getApplicationContext());
+ mSearchFeatureProvider = featureFactory.searchFeatureProvider();
+ titleView = view.findViewById(android.R.id.title);
+ summaryView = view.findViewById(android.R.id.summary);
+ iconView = view.findViewById(android.R.id.icon);
+ breadcrumbView = view.findViewById(R.id.breadcrumb);
+
+ mPlaceholderSummary = view.getContext().getString(R.string.summary_placeholder);
+ }
+
+ public abstract int getClickActionMetricName();
+
+ public void onBind(SearchFragment fragment, SearchResult result) {
+ titleView.setText(result.title);
+ // TODO (b/36101902) remove check for DYNAMIC_PLACEHOLDER
+ if (TextUtils.isEmpty(result.summary)
+ || TextUtils.equals(result.summary, mPlaceholderSummary)
+ || TextUtils.equals(result.summary, DYNAMIC_PLACEHOLDER)) {
+ summaryView.setVisibility(View.GONE);
+ } else {
+ summaryView.setText(result.summary);
+ summaryView.setVisibility(View.VISIBLE);
+ }
+
+ if (result instanceof AppSearchResult) {
+ AppSearchResult appResult = (AppSearchResult) result;
+ PackageManager pm = fragment.getActivity().getPackageManager();
+ iconView.setImageDrawable(appResult.info.loadIcon(pm));
+ } else {
+ // Valid even when result.icon is null.
+ iconView.setImageDrawable(result.icon);
+ }
+
+ bindBreadcrumbView(result);
+ }
+
+ private void bindBreadcrumbView(SearchResult result) {
+ if (result.breadcrumbs == null || result.breadcrumbs.isEmpty()) {
+ breadcrumbView.setVisibility(View.GONE);
+ return;
+ }
+ final Context context = breadcrumbView.getContext();
+ String breadcrumb = result.breadcrumbs.get(0);
+ final int count = result.breadcrumbs.size();
+ for (int i = 1; i < count; i++) {
+ breadcrumb = context.getString(R.string.search_breadcrumb_connector,
+ breadcrumb, result.breadcrumbs.get(i));
+ }
+ breadcrumbView.setText(breadcrumb);
+ breadcrumbView.setVisibility(View.VISIBLE);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingManager.java b/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingManager.java
new file mode 100644
index 0000000..0928611
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingManager.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.indexing;
+
+import static com.android.settings.intelligence.search.query.DatabaseResultTask.SELECT_COLUMNS;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_PACKAGE;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.CLASS_NAME;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_KEY_REF;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_TITLE;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.ENABLED;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.ICON;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_ACTION;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.PAYLOAD;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.SCREEN_TITLE;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX;
+import static com.android.settings.intelligence.search.SearchFeatureProvider.DEBUG;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.os.AsyncTask;
+import android.provider.SearchIndexablesContract;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.sitemap.SiteMapPair;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Consumes the SearchIndexableProvider content providers.
+ * Updates the Resource, Raw Data and non-indexable data for Search.
+ *
+ * TODO(b/33577327) this class needs to be refactored by moving most of its methods into controllers
+ */
+public class DatabaseIndexingManager {
+
+ private static final String TAG = "DatabaseIndexingManager";
+
+ @VisibleForTesting
+ final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false);
+
+ private PreIndexDataCollector mCollector;
+ private IndexDataConverter mConverter;
+
+ private Context mContext;
+
+ public DatabaseIndexingManager(Context context) {
+ mContext = context;
+ }
+
+ public boolean isIndexingComplete() {
+ return mIsIndexingComplete.get();
+ }
+
+ public void indexDatabase(IndexingCallback callback) {
+ IndexingTask task = new IndexingTask(callback);
+ task.execute();
+ }
+
+ /**
+ * Accumulate all data and non-indexable keys from each of the content-providers.
+ * Only the first indexing for the default language gets static search results - subsequent
+ * calls will only gather non-indexable keys.
+ */
+ public void performIndexing() {
+ final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
+ final List<ResolveInfo> providers =
+ mContext.getPackageManager().queryIntentContentProviders(intent, 0);
+
+ final boolean isFullIndex = IndexDatabaseHelper.isFullIndex(mContext, providers);
+
+ if (isFullIndex) {
+ rebuildDatabase();
+ }
+
+ PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex);
+
+ final long updateDatabaseStartTime = System.currentTimeMillis();
+ updateDatabase(indexData, isFullIndex);
+ IndexDatabaseHelper.setIndexed(mContext, providers);
+ if (DEBUG) {
+ final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime;
+ Log.d(TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime);
+ }
+ }
+
+ @VisibleForTesting
+ PreIndexData getIndexDataFromProviders(List<ResolveInfo> providers, boolean isFullIndex) {
+ if (mCollector == null) {
+ mCollector = new PreIndexDataCollector(mContext);
+ }
+ return mCollector.collectIndexableData(providers, isFullIndex);
+ }
+
+ /**
+ * Drop the currently stored database, and clear the flags which mark the database as indexed.
+ */
+ private void rebuildDatabase() {
+ // Drop the database when the locale or build has changed. This eliminates rows which are
+ // dynamically inserted in the old language, or deprecated settings.
+ final SQLiteDatabase db = getWritableDatabase();
+ IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
+ }
+
+ /**
+ * Adds new data to the database and verifies the correctness of the ENABLED column.
+ * First, the data to be updated and all non-indexable keys are copied locally.
+ * Then all new data to be added is inserted.
+ * Then search results are verified to have the correct value of enabled.
+ * Finally, we record that the locale has been indexed.
+ *
+ * @param isFullIndex true the database needs to be rebuilt.
+ */
+ @VisibleForTesting
+ void updateDatabase(PreIndexData preIndexData, boolean isFullIndex) {
+ final Map<String, Set<String>> nonIndexableKeys = preIndexData.getNonIndexableKeys();
+
+ final SQLiteDatabase database = getWritableDatabase();
+ if (database == null) {
+ Log.w(TAG, "Cannot indexDatabase Index as I cannot get a writable database");
+ return;
+ }
+
+ try {
+ database.beginTransaction();
+
+ // Convert all Pre-index data to Index data and and insert to db.
+ final List<IndexData> indexData = getIndexData(preIndexData);
+ insertIndexData(database, indexData);
+ insertSiteMapData(database, getSiteMapPairs(indexData, preIndexData.getSiteMapPairs()));
+
+ // Only check for non-indexable key updates after initial index.
+ // Enabled state with non-indexable keys is checked when items are first inserted.
+ if (!isFullIndex) {
+ updateDataInDatabase(database, nonIndexableKeys);
+ }
+
+ database.setTransactionSuccessful();
+ } finally {
+ database.endTransaction();
+ }
+ }
+
+ private List<IndexData> getIndexData(PreIndexData data) {
+ if (mConverter == null) {
+ mConverter = new IndexDataConverter(mContext);
+ }
+ return mConverter.convertPreIndexDataToIndexData(data);
+ }
+
+ private List<SiteMapPair> getSiteMapPairs(List<IndexData> indexData,
+ List<Pair<String, String>> siteMapClassNames) {
+ if (mConverter == null) {
+ mConverter = new IndexDataConverter(mContext);
+ }
+ return mConverter.convertSiteMapPairs(indexData, siteMapClassNames);
+ }
+
+ private void insertSiteMapData(SQLiteDatabase database, List<SiteMapPair> siteMapPairs) {
+ if (siteMapPairs == null) {
+ return;
+ }
+ for (SiteMapPair pair : siteMapPairs) {
+ database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SITE_MAP,
+ null /* nullColumnHack */, pair.toContentValue());
+ }
+ }
+
+ /**
+ * Inserts all of the entries in {@param indexData} into the {@param database}
+ * as Search Data and as part of the Information Hierarchy.
+ */
+ private void insertIndexData(SQLiteDatabase database, List<IndexData> indexData) {
+ ContentValues values;
+
+ for (IndexData dataRow : indexData) {
+ if (TextUtils.isEmpty(dataRow.normalizedTitle)) {
+ continue;
+ }
+
+ values = new ContentValues();
+ values.put(DATA_TITLE, dataRow.updatedTitle);
+ values.put(DATA_TITLE_NORMALIZED, dataRow.normalizedTitle);
+ values.put(DATA_SUMMARY_ON, dataRow.updatedSummaryOn);
+ values.put(DATA_SUMMARY_ON_NORMALIZED, dataRow.normalizedSummaryOn);
+ values.put(DATA_ENTRIES, dataRow.entries);
+ values.put(DATA_KEYWORDS, dataRow.spaceDelimitedKeywords);
+ values.put(DATA_PACKAGE, dataRow.packageName);
+ values.put(CLASS_NAME, dataRow.className);
+ values.put(SCREEN_TITLE, dataRow.screenTitle);
+ values.put(INTENT_ACTION, dataRow.intentAction);
+ values.put(INTENT_TARGET_PACKAGE, dataRow.intentTargetPackage);
+ values.put(INTENT_TARGET_CLASS, dataRow.intentTargetClass);
+ values.put(ICON, dataRow.iconResId);
+ values.put(ENABLED, dataRow.enabled);
+ values.put(DATA_KEY_REF, dataRow.key);
+ values.put(PAYLOAD_TYPE, dataRow.payloadType);
+ values.put(PAYLOAD, dataRow.payload);
+
+ database.replaceOrThrow(TABLE_PREFS_INDEX, null, values);
+ }
+ }
+
+ /**
+ * Upholds the validity of enabled data for the user.
+ * All rows which are enabled but are now flagged with non-indexable keys will become disabled.
+ * All rows which are disabled but no longer a non-indexable key will become enabled.
+ *
+ * @param database The database to validate.
+ * @param nonIndexableKeys A map between package name and the set of non-indexable keys for it.
+ */
+ @VisibleForTesting
+ void updateDataInDatabase(SQLiteDatabase database,
+ Map<String, Set<String>> nonIndexableKeys) {
+ final String whereEnabled = ENABLED + " = 1";
+ final String whereDisabled = ENABLED + " = 0";
+
+ final Cursor enabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
+ whereEnabled, null, null, null, null);
+
+ final ContentValues enabledToDisabledValue = new ContentValues();
+ enabledToDisabledValue.put(ENABLED, 0);
+
+ String packageName;
+ // TODO Refactor: Move these two loops into one method.
+ while (enabledResults.moveToNext()) {
+ packageName = enabledResults.getString(enabledResults.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.DATA_PACKAGE));
+ final String key = enabledResults.getString(enabledResults.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.DATA_KEY_REF));
+ final Set<String> packageKeys = nonIndexableKeys.get(packageName);
+
+ // The indexed item is set to Enabled but is now non-indexable
+ if (packageKeys != null && packageKeys.contains(key)) {
+ final String whereClause = getKeyWhereClause(key);
+ database.update(TABLE_PREFS_INDEX, enabledToDisabledValue, whereClause, null);
+ }
+ }
+ enabledResults.close();
+
+ final Cursor disabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
+ whereDisabled, null, null, null, null);
+
+ final ContentValues disabledToEnabledValue = new ContentValues();
+ disabledToEnabledValue.put(ENABLED, 1);
+
+ while (disabledResults.moveToNext()) {
+ packageName = disabledResults.getString(disabledResults.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.DATA_PACKAGE));
+
+ final String key = disabledResults.getString(disabledResults.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.DATA_KEY_REF));
+ final Set<String> packageKeys = nonIndexableKeys.get(packageName);
+
+ // The indexed item is set to Disabled but is no longer non-indexable.
+ // We do not enable keys when packageKeys is null because it means the keys came
+ // from an unrecognized package and therefore should not be surfaced as results.
+ if (packageKeys != null && !packageKeys.contains(key)) {
+ final String whereClause = getKeyWhereClause(key);
+ database.update(TABLE_PREFS_INDEX, disabledToEnabledValue, whereClause, null);
+ }
+ }
+ disabledResults.close();
+ }
+
+ private String getKeyWhereClause(String key) {
+ return IndexDatabaseHelper.IndexColumns.DATA_KEY_REF + " = \"" + key + "\"";
+ }
+
+ private SQLiteDatabase getWritableDatabase() {
+ try {
+ return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Cannot open writable database", e);
+ return null;
+ }
+ }
+
+ public class IndexingTask extends AsyncTask<Void, Void, Void> {
+
+ @VisibleForTesting
+ IndexingCallback mCallback;
+ private long mIndexStartTime;
+
+ public IndexingTask(IndexingCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ mIndexStartTime = System.currentTimeMillis();
+ mIsIndexingComplete.set(false);
+ }
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ performIndexing();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ int indexingTime = (int) (System.currentTimeMillis() - mIndexStartTime);
+ FeatureFactory.get(mContext).metricsFeatureProvider(mContext).logEvent(
+ SettingsIntelligenceLogProto.SettingsIntelligenceEvent.INDEX_SEARCH,
+ indexingTime);
+
+ mIsIndexingComplete.set(true);
+ if (mCallback != null) {
+ mCallback.onIndexingFinished();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingUtils.java b/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingUtils.java
new file mode 100644
index 0000000..534a20c
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingUtils.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.indexing;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+/**
+ * Utility class for {@like DatabaseIndexingManager} to handle the mapping between Payloads
+ * and Preference controllers, and managing indexable classes.
+ */
+public class DatabaseIndexingUtils {
+
+ private static final String TAG = "DatabaseIndexingUtils";
+
+ // frameworks/base/proto/src/metrics_constants.proto#DASHBOARD_SEARCH_RESULTS
+ // Have to hardcode value here because we can't depend on internal framework constants.
+ public static final int DASHBOARD_SEARCH_RESULTS = 34;
+
+ /**
+ * Below are internal contract between Settings/SettingsIntelligence to launch a search result
+ * page.
+ */
+ public static final String EXTRA_SHOW_FRAGMENT = ":settings:show_fragment";
+ public static final String EXTRA_SOURCE_METRICS_CATEGORY = ":settings:source_metrics";
+ public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
+ public static final String EXTRA_SHOW_FRAGMENT_TITLE = ":settings:show_fragment_title";
+ public static final String SEARCH_RESULT_TRAMPOLINE_ACTION =
+ "com.android.settings.SEARCH_RESULT_TRAMPOLINE";
+
+ /**
+ * Builds intent that launches the search destination as a sub-setting.
+ */
+ public static Intent buildSearchTrampolineIntent(Context context, String className, String key,
+ String screenTitle) {
+ final Intent intent = new Intent(SEARCH_RESULT_TRAMPOLINE_ACTION);
+ intent.putExtra(EXTRA_SHOW_FRAGMENT, className)
+ .putExtra(EXTRA_SHOW_FRAGMENT_TITLE, screenTitle)
+ .putExtra(EXTRA_SOURCE_METRICS_CATEGORY, DASHBOARD_SEARCH_RESULTS)
+ .putExtra(EXTRA_FRAGMENT_ARG_KEY, key);
+ return intent;
+ }
+
+ public static Intent buildDirectSearchResultIntent(String action, String targetPackage,
+ String targetClass, String key) {
+ final Intent intent = new Intent(action).putExtra(EXTRA_FRAGMENT_ARG_KEY, key);
+ if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) {
+ final ComponentName component = new ComponentName(targetPackage, targetClass);
+ intent.setComponent(component);
+ }
+ return intent;
+ }
+
+ // TODO REFACTOR (b/62807132) Add inline support with proper intents.
+// /**
+// * @param className which wil provide the map between from {@link Uri}s to
+// * {@link PreferenceControllerMixin}
+// * @return A map between {@link Uri}s and {@link PreferenceControllerMixin}s to get the
+// payload
+// * types for Settings.
+// */
+// public static Map<String, PreferenceControllerMixin> getPreferenceControllerUriMap(
+// String className, Context context) {
+// if (context == null) {
+// return null;
+// }
+//
+// final Class<?> clazz = getIndexableClass(className);
+//
+// if (clazz == null) {
+// Log.d(TAG, "SearchIndexableResource '" + className +
+// "' should implement the " + Indexable.class.getName() + " interface!");
+// return null;
+// }
+//
+// // Will be non null only for a Local provider implementing a
+// // SEARCH_INDEX_DATA_PROVIDER field
+// final Indexable.SearchIndexProvider provider = getSearchIndexProvider(clazz);
+//
+// List<AbstractPreferenceController> controllers =
+// provider.getPreferenceControllers(context);
+//
+// if (controllers == null) {
+// return null;
+// }
+//
+// ArrayMap<String, PreferenceControllerMixin> map = new ArrayMap<>();
+//
+// for (AbstractPreferenceController controller : controllers) {
+// if (controller instanceof PreferenceControllerMixin) {
+// map.put(controller.getPreferenceKey(), (PreferenceControllerMixin) controller);
+// } else {
+// throw new IllegalStateException(controller.getClass().getName()
+// + " must implement " + PreferenceControllerMixin.class.getName());
+// }
+// }
+//
+// return map;
+// }
+//
+// /**
+// * @param uriMap Map between the {@link PreferenceControllerMixin} keys
+// * and the controllers themselves.
+// * @param key The look-up key
+// * @return The Payload from the {@link PreferenceControllerMixin} specified by the key,
+// * if it exists. Otherwise null.
+// */
+// public static ResultPayload getPayloadFromUriMap(Map<String, PreferenceControllerMixin>
+// uriMap,
+// String key) {
+// if (uriMap == null) {
+// return null;
+// }
+//
+// PreferenceControllerMixin controller = uriMap.get(key);
+// if (controller == null) {
+// return null;
+// }
+//
+// return controller.getResultPayload();
+// }
+
+}
diff --git a/src/com/android/settings/intelligence/search/indexing/IndexData.java b/src/com/android/settings/intelligence/search/indexing/IndexData.java
new file mode 100644
index 0000000..d178344
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/IndexData.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.indexing;
+
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.ResultPayloadUtils;
+
+import java.text.Normalizer;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * Data class representing a single row in the Setting Search results database.
+ */
+public class IndexData {
+ /**
+ * This is different from intentTargetPackage.
+ *
+ * @see SearchIndexableData#iconResId
+ */
+ public final String packageName;
+ public final String locale;
+ public final String updatedTitle;
+ public final String normalizedTitle;
+ public final String updatedSummaryOn;
+ public final String normalizedSummaryOn;
+ public final String entries;
+ public final String className;
+ public final String childClassName;
+ public final String screenTitle;
+ public final int iconResId;
+ public final String spaceDelimitedKeywords;
+ public final String intentAction;
+ public final String intentTargetPackage;
+ public final String intentTargetClass;
+ public final boolean enabled;
+ public final String key;
+ public final int payloadType;
+ public final byte[] payload;
+
+ private static final String NON_BREAKING_HYPHEN = "\u2011";
+ private static final String EMPTY = "";
+ private static final String HYPHEN = "-";
+ private static final String SPACE = " ";
+ // Regex matching a comma, and any number of subsequent white spaces.
+ private static final String LIST_DELIMITERS = "[,]\\s*";
+
+ private static final Pattern REMOVE_DIACRITICALS_PATTERN
+ = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
+
+ private IndexData(Builder builder) {
+ locale = Locale.getDefault().toString();
+ updatedTitle = normalizeHyphen(builder.mTitle);
+ updatedSummaryOn = normalizeHyphen(builder.mSummaryOn);
+ if (Locale.JAPAN.toString().equalsIgnoreCase(locale)) {
+ // Special case for JP. Convert charset to the same type for indexing purpose.
+ normalizedTitle = normalizeJapaneseString(builder.mTitle);
+ normalizedSummaryOn = normalizeJapaneseString(builder.mSummaryOn);
+ } else {
+ normalizedTitle = normalizeString(builder.mTitle);
+ normalizedSummaryOn = normalizeString(builder.mSummaryOn);
+ }
+ entries = builder.mEntries;
+ className = builder.mClassName;
+ childClassName = builder.mChildClassName;
+ screenTitle = builder.mScreenTitle;
+ iconResId = builder.mIconResId;
+ spaceDelimitedKeywords = normalizeKeywords(builder.mKeywords);
+ intentAction = builder.mIntentAction;
+ packageName = builder.mPackageName;
+ intentTargetPackage = builder.mIntentTargetPackage;
+ intentTargetClass = builder.mIntentTargetClass;
+ enabled = builder.mEnabled;
+ key = builder.mKey;
+ payloadType = builder.mPayloadType;
+ payload = builder.mPayload != null ? ResultPayloadUtils.marshall(builder.mPayload)
+ : null;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(updatedTitle)
+ .append(": ")
+ .append(updatedSummaryOn)
+ .toString();
+ }
+
+ /**
+ * In the list of keywords, replace the comma and all subsequent whitespace with a single space.
+ */
+ public static String normalizeKeywords(String input) {
+ return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY;
+ }
+
+ /**
+ * @return {@param input} where all non-standard hyphens are replaced by normal hyphens.
+ */
+ public static String normalizeHyphen(String input) {
+ return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY;
+ }
+
+ /**
+ * @return {@param input} with all hyphens removed, and all letters lower case.
+ */
+ public static String normalizeString(String input) {
+ final String normalizedHypen = normalizeHyphen(input);
+ final String nohyphen = (input != null) ? normalizedHypen.replaceAll(HYPHEN, EMPTY) : EMPTY;
+ final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD);
+
+ return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase();
+ }
+
+ public static String normalizeJapaneseString(String input) {
+ final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY;
+ final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFKD);
+ final StringBuffer sb = new StringBuffer();
+ final int length = normalized.length();
+ for (int i = 0; i < length; i++) {
+ char c = normalized.charAt(i);
+ // Convert Hiragana to full-width Katakana
+ if (c >= '\u3041' && c <= '\u3096') {
+ sb.append((char) (c - '\u3041' + '\u30A1'));
+ } else {
+ sb.append(c);
+ }
+ }
+
+ return REMOVE_DIACRITICALS_PATTERN.matcher(sb.toString()).replaceAll("").toLowerCase();
+ }
+
+ public static class Builder {
+ private String mTitle;
+ private String mSummaryOn;
+ private String mEntries;
+ private String mClassName;
+ private String mChildClassName;
+ private String mScreenTitle;
+ private String mPackageName;
+ private int mIconResId;
+ private String mKeywords;
+ private String mIntentAction;
+ private String mIntentTargetPackage;
+ private String mIntentTargetClass;
+ private boolean mEnabled;
+ private String mKey;
+ @ResultPayload.PayloadType
+ private int mPayloadType;
+ private ResultPayload mPayload;
+
+ @Override
+ public String toString() {
+ return "IndexData.Builder {"
+ + "title: " + mTitle + ","
+ + "package: " + mPackageName
+ + "}";
+ }
+
+ public Builder setTitle(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ public Builder setSummaryOn(String summaryOn) {
+ mSummaryOn = summaryOn;
+ return this;
+ }
+
+ public Builder setEntries(String entries) {
+ mEntries = entries;
+ return this;
+ }
+
+ public Builder setClassName(String className) {
+ mClassName = className;
+ return this;
+ }
+
+ public Builder setChildClassName(String childClassName) {
+ mChildClassName = childClassName;
+ return this;
+ }
+
+ public Builder setScreenTitle(String screenTitle) {
+ mScreenTitle = screenTitle;
+ return this;
+ }
+
+ public Builder setPackageName(String packageName) {
+ mPackageName = packageName;
+ return this;
+ }
+
+ public Builder setIconResId(int iconResId) {
+ mIconResId = iconResId;
+ return this;
+ }
+
+ public Builder setKeywords(String keywords) {
+ mKeywords = keywords;
+ return this;
+ }
+
+ public Builder setIntentAction(String intentAction) {
+ mIntentAction = intentAction;
+ return this;
+ }
+
+ public Builder setIntentTargetPackage(String intentTargetPackage) {
+ mIntentTargetPackage = intentTargetPackage;
+ return this;
+ }
+
+ public Builder setIntentTargetClass(String intentTargetClass) {
+ mIntentTargetClass = intentTargetClass;
+ return this;
+ }
+
+ public Builder setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ return this;
+ }
+
+ public Builder setKey(String key) {
+ mKey = key;
+ return this;
+ }
+
+ public Builder setPayload(ResultPayload payload) {
+ mPayload = payload;
+
+ if (mPayload != null) {
+ setPayloadType(mPayload.getType());
+ }
+ return this;
+ }
+
+ /**
+ * Payload type is added when a Payload is added to the Builder in {setPayload}
+ *
+ * @param payloadType PayloadType
+ * @return The Builder
+ */
+ private Builder setPayloadType(@ResultPayload.PayloadType int payloadType) {
+ mPayloadType = payloadType;
+ return this;
+ }
+
+ /**
+ * Adds intent to inline payloads, or creates an Intent Payload as a fallback if the
+ * payload is null.
+ */
+ private void setIntent(Context context) {
+ if (mPayload != null) {
+ return;
+ }
+ final Intent intent = buildIntent(context);
+ mPayload = new ResultPayload(intent);
+ mPayloadType = ResultPayload.PayloadType.INTENT;
+ }
+
+ /**
+ * Adds Intent payload to builder.
+ */
+ private Intent buildIntent(Context context) {
+ final Intent intent;
+
+ // TODO REFACTOR (b/62807132) With inline results re-add proper intent support
+ boolean isEmptyIntentAction = TextUtils.isEmpty(mIntentAction);
+ if (isEmptyIntentAction) {
+ // No intent action is set, or the intent action is for a sub-setting.
+ intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(context, mClassName,
+ mKey, mScreenTitle);
+ } else {
+ intent = DatabaseIndexingUtils.buildDirectSearchResultIntent(mIntentAction,
+ mIntentTargetPackage, mIntentTargetClass, mKey);
+ }
+ return intent;
+ }
+
+ public IndexData build(Context context) {
+ setIntent(context);
+ return new IndexData(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/indexing/IndexDataConverter.java b/src/com/android/settings/intelligence/search/indexing/IndexDataConverter.java
new file mode 100644
index 0000000..167019b
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/IndexDataConverter.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.indexing;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.os.AsyncTask;
+import android.provider.SearchIndexableData;
+import android.provider.SearchIndexableResource;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Xml;
+
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchIndexableRaw;
+import com.android.settings.intelligence.search.sitemap.SiteMapPair;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * Helper class to convert {@link PreIndexData} to {@link IndexData}.
+ */
+public class IndexDataConverter {
+
+ private static final String TAG = "IndexDataConverter";
+
+ private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen";
+ private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference";
+ private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference";
+ private static final List<String> SKIP_NODES = Arrays.asList("intent", "extra");
+
+ private final Context mContext;
+
+ public IndexDataConverter(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Return the collection of {@param preIndexData} converted into {@link IndexData}.
+ *
+ * @param preIndexData a collection of {@link SearchIndexableResource},
+ * {@link SearchIndexableRaw} and non-indexable keys.
+ */
+ public List<IndexData> convertPreIndexDataToIndexData(PreIndexData preIndexData) {
+ final long startConversion = System.currentTimeMillis();
+ final List<SearchIndexableData> indexableData = preIndexData.getDataToUpdate();
+ final Map<String, Set<String>> nonIndexableKeys = preIndexData.getNonIndexableKeys();
+ final List<IndexData> indexData = new ArrayList<>();
+
+ for (SearchIndexableData data : indexableData) {
+ if (data instanceof SearchIndexableRaw) {
+ final SearchIndexableRaw rawData = (SearchIndexableRaw) data;
+ final Set<String> rawNonIndexableKeys = nonIndexableKeys.get(
+ rawData.intentTargetPackage);
+ final IndexData convertedRaw = convertRaw(mContext, rawData, rawNonIndexableKeys);
+ if (convertedRaw != null) {
+ indexData.add(convertedRaw);
+ }
+ } else if (data instanceof SearchIndexableResource) {
+ final SearchIndexableResource sir = (SearchIndexableResource) data;
+ final Set<String> resourceNonIndexableKeys =
+ getNonIndexableKeysForResource(nonIndexableKeys, sir.packageName);
+ final List<IndexData> resourceData = convertResource(sir, resourceNonIndexableKeys);
+ indexData.addAll(resourceData);
+ }
+ }
+
+ final long endConversion = System.currentTimeMillis();
+ Log.d(TAG, "Converting pre-index data to index data took: "
+ + (endConversion - startConversion));
+
+ return indexData;
+ }
+
+ /**
+ * Returns a full list of site map pairs based on metadata from all data sources.
+ *
+ * The content schema follows {@link IndexDatabaseHelper.Tables#TABLE_SITE_MAP}
+ */
+ public List<SiteMapPair> convertSiteMapPairs(List<IndexData> indexData,
+ List<Pair<String, String>> siteMapClassNames) {
+ final List<SiteMapPair> pairs = new ArrayList<>();
+ if (indexData == null) {
+ return pairs;
+ }
+ // Step 1: loop indexData and build all static site map pairs.
+ final Map<String, String> classToTitleMap = new TreeMap<>();
+ for (IndexData row : indexData) {
+ if (TextUtils.isEmpty(row.className)) {
+ continue;
+ }
+ // Build a map of [class, title] for the next step.
+ classToTitleMap.put(row.className, row.screenTitle);
+ if (!TextUtils.isEmpty(row.childClassName)) {
+ pairs.add(new SiteMapPair(row.className, row.screenTitle,
+ row.childClassName, row.updatedTitle));
+ }
+ }
+ // Step 2: Extend the sitemap pairs by adding dynamic pairs provided by
+ // SearchIndexableProvider. The provider only tells us class name so we need to finish
+ // the mapping by looking up display title for each class.
+ for (Pair<String, String> pair : siteMapClassNames) {
+ final String parentName = classToTitleMap.get(pair.first);
+ final String childName = classToTitleMap.get(pair.second);
+ if (TextUtils.isEmpty(parentName) || TextUtils.isEmpty(childName)) {
+ Log.w(TAG, "Cannot build sitemap pair for incomplete names "
+ + pair + parentName + childName);
+ } else {
+ pairs.add(new SiteMapPair(pair.first, parentName, pair.second, childName));
+ }
+ }
+ // Done
+ return pairs;
+ }
+
+ /**
+ * Return the conversion of {@link SearchIndexableRaw} to {@link IndexData}.
+ * The fields of {@link SearchIndexableRaw} are a subset of {@link IndexData},
+ * and there is some data sanitization in the conversion.
+ */
+ @Nullable
+ private IndexData convertRaw(Context context, SearchIndexableRaw raw,
+ Set<String> nonIndexableKeys) {
+ if (TextUtils.isEmpty(raw.key)) {
+ Log.w(TAG, "Skipping null key for raw indexable " + raw.packageName + "/" + raw.title);
+ return null;
+ }
+ // A row is enabled if it does not show up as an nonIndexableKey
+ boolean enabled = !(nonIndexableKeys != null && nonIndexableKeys.contains(raw.key));
+
+ final IndexData.Builder builder = new IndexData.Builder();
+ builder.setTitle(raw.title)
+ .setSummaryOn(raw.summaryOn)
+ .setEntries(raw.entries)
+ .setKeywords(raw.keywords)
+ .setClassName(raw.className)
+ .setScreenTitle(raw.screenTitle)
+ .setIconResId(raw.iconResId)
+ .setIntentAction(raw.intentAction)
+ .setIntentTargetPackage(raw.intentTargetPackage)
+ .setIntentTargetClass(raw.intentTargetClass)
+ .setEnabled(enabled)
+ .setPackageName(raw.packageName)
+ .setKey(raw.key);
+
+ return builder.build(context);
+ }
+
+ /**
+ * Return the conversion of the {@link SearchIndexableResource} to {@link IndexData}.
+ * Each of the elements in the xml layout attribute of {@param sir} is a candidate to be
+ * converted (including the header element).
+ *
+ * TODO (b/33577327) simplify this method.
+ */
+ private List<IndexData> convertResource(SearchIndexableResource sir,
+ Set<String> nonIndexableKeys) {
+ final Context context = sir.context;
+ XmlResourceParser parser = null;
+
+ List<IndexData> resourceIndexData = new ArrayList<>();
+ try {
+ parser = context.getResources().getXml(sir.xmlResId);
+
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ // Parse next until start tag is found
+ }
+
+ String nodeName = parser.getName();
+ if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) {
+ throw new RuntimeException(
+ "XML document must start with <PreferenceScreen> tag; found"
+ + nodeName + " at " + parser.getPositionDescription());
+ }
+
+ final int outerDepth = parser.getDepth();
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ final String screenTitle = XmlParserUtils.getDataTitle(context, attrs);
+ final String headerKey = XmlParserUtils.getDataKey(context, attrs);
+
+ String title;
+ String key;
+ String headerTitle;
+ String summary;
+ String headerSummary;
+ String keywords;
+ String headerKeywords;
+ String childFragment;
+ @DrawableRes int iconResId;
+ ResultPayload payload;
+ boolean enabled;
+
+ // TODO REFACTOR (b/62807132) Add proper inline support
+// Map<String, PreferenceControllerMixin> controllerUriMap = null;
+//
+// if (fragmentName != null) {
+// controllerUriMap = DatabaseIndexingUtils
+// .getPreferenceControllerUriMap(fragmentName, context);
+// }
+
+ headerTitle = XmlParserUtils.getDataTitle(context, attrs);
+ headerSummary = XmlParserUtils.getDataSummary(context, attrs);
+ headerKeywords = XmlParserUtils.getDataKeywords(context, attrs);
+ enabled = !nonIndexableKeys.contains(headerKey);
+ // TODO: Set payload type for header results
+ IndexData.Builder headerBuilder = new IndexData.Builder();
+ headerBuilder.setTitle(headerTitle)
+ .setSummaryOn(headerSummary)
+ .setScreenTitle(screenTitle)
+ .setKeywords(headerKeywords)
+ .setClassName(sir.className)
+ .setPackageName(sir.packageName)
+ .setIntentAction(sir.intentAction)
+ .setIntentTargetPackage(sir.intentTargetPackage)
+ .setIntentTargetClass(sir.intentTargetClass)
+ .setEnabled(enabled)
+ .setKey(headerKey);
+
+ // Flag for XML headers which a child element's title.
+ boolean isHeaderUnique = true;
+ IndexData.Builder builder;
+
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ nodeName = parser.getName();
+ if (SKIP_NODES.contains(nodeName)) {
+ if (SearchFeatureProvider.DEBUG) {
+ Log.d(TAG, nodeName + " is not a valid entity to index, skip.");
+ }
+ continue;
+ }
+
+ title = XmlParserUtils.getDataTitle(context, attrs);
+ key = XmlParserUtils.getDataKey(context, attrs);
+ enabled = !nonIndexableKeys.contains(key);
+ keywords = XmlParserUtils.getDataKeywords(context, attrs);
+ iconResId = XmlParserUtils.getDataIcon(context, attrs);
+
+ if (isHeaderUnique && TextUtils.equals(headerTitle, title)) {
+ isHeaderUnique = false;
+ }
+
+ builder = new IndexData.Builder();
+ builder.setTitle(title)
+ .setKeywords(keywords)
+ .setClassName(sir.className)
+ .setScreenTitle(screenTitle)
+ .setIconResId(iconResId)
+ .setPackageName(sir.packageName)
+ .setIntentAction(sir.intentAction)
+ .setIntentTargetPackage(sir.intentTargetPackage)
+ .setIntentTargetClass(sir.intentTargetClass)
+ .setEnabled(enabled)
+ .setKey(key);
+
+ if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) {
+ summary = XmlParserUtils.getDataSummary(context, attrs);
+
+ String entries = null;
+
+ if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) {
+ entries = XmlParserUtils.getDataEntries(context, attrs);
+ }
+
+ // TODO (b/62254931) index primitives instead of payload
+ // TODO (b/62807132) Add proper inline support
+ //payload = DatabaseIndexingUtils.getPayloadFromUriMap(controllerUriMap, key);
+ childFragment = XmlParserUtils.getDataChildFragment(context, attrs);
+
+ builder.setSummaryOn(summary)
+ .setEntries(entries)
+ .setChildClassName(childFragment);
+ tryAddIndexDataToList(resourceIndexData, builder);
+ } else {
+ // TODO (b/33577327) We removed summary off here. We should check if we can
+ // merge this 'else' section with the one above. Put a break point to
+ // investigate.
+ String summaryOn = XmlParserUtils.getDataSummaryOn(context, attrs);
+
+ if (TextUtils.isEmpty(summaryOn)) {
+ summaryOn = XmlParserUtils.getDataSummary(context, attrs);
+ }
+
+ builder.setSummaryOn(summaryOn);
+
+ tryAddIndexDataToList(resourceIndexData, builder);
+ }
+ }
+
+ // The xml header's title does not match the title of one of the child settings.
+ if (isHeaderUnique) {
+ tryAddIndexDataToList(resourceIndexData, headerBuilder);
+ }
+ } catch (XmlPullParserException e) {
+ Log.w(TAG, "XML Error parsing PreferenceScreen: " + sir.className, e);
+ } catch (IOException e) {
+ Log.w(TAG, "IO Error parsing PreferenceScreen: " + sir.className, e);
+ } catch (Resources.NotFoundException e) {
+ Log.w(TAG, "Resoucre not found error parsing PreferenceScreen: " + sir.className, e);
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+ return resourceIndexData;
+ }
+
+ private void tryAddIndexDataToList(List<IndexData> list, IndexData.Builder data) {
+ if (!TextUtils.isEmpty(data.getKey())) {
+ list.add(data.build(mContext));
+ } else {
+ Log.w(TAG, "Skipping index for null-key item " + data);
+ }
+ }
+
+ private Set<String> getNonIndexableKeysForResource(Map<String, Set<String>> nonIndexableKeys,
+ String packageName) {
+ return nonIndexableKeys.containsKey(packageName)
+ ? nonIndexableKeys.get(packageName)
+ : new HashSet<String>();
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/indexing/IndexDatabaseHelper.java b/src/com/android/settings/intelligence/search/indexing/IndexDatabaseHelper.java
new file mode 100644
index 0000000..4bec50e
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/IndexDatabaseHelper.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.indexing;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Build;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
+import java.util.Locale;
+
+public class IndexDatabaseHelper extends SQLiteOpenHelper {
+
+ private static final String TAG = "IndexDatabaseHelper";
+
+ private static final String DATABASE_NAME = "search_index.db";
+ private static final int DATABASE_VERSION = 119;
+
+ @VisibleForTesting
+ static final String SHARED_PREFS_TAG = "indexing_manager";
+
+ private static final String PREF_KEY_INDEXED_PROVIDERS = "indexed_providers";
+
+ public interface Tables {
+ String TABLE_PREFS_INDEX = "prefs_index";
+ String TABLE_SITE_MAP = "site_map";
+ String TABLE_META_INDEX = "meta_index";
+ String TABLE_SAVED_QUERIES = "saved_queries";
+ }
+
+ public interface IndexColumns {
+ String DATA_TITLE = "data_title";
+ String DATA_TITLE_NORMALIZED = "data_title_normalized";
+ String DATA_SUMMARY_ON = "data_summary_on";
+ String DATA_SUMMARY_ON_NORMALIZED = "data_summary_on_normalized";
+ String DATA_SUMMARY_OFF = "data_summary_off";
+ String DATA_SUMMARY_OFF_NORMALIZED = "data_summary_off_normalized";
+ String DATA_ENTRIES = "data_entries";
+ String DATA_KEYWORDS = "data_keywords";
+ String DATA_PACKAGE = "package";
+ String CLASS_NAME = "class_name";
+ String SCREEN_TITLE = "screen_title";
+ String INTENT_ACTION = "intent_action";
+ String INTENT_TARGET_PACKAGE = "intent_target_package";
+ String INTENT_TARGET_CLASS = "intent_target_class";
+ String ICON = "icon";
+ String ENABLED = "enabled";
+ String DATA_KEY_REF = "data_key_reference";
+ String PAYLOAD_TYPE = "payload_type";
+ String PAYLOAD = "payload";
+ }
+
+ public interface MetaColumns {
+ String BUILD = "build";
+ }
+
+ public interface SavedQueriesColumns {
+ String QUERY = "query";
+ String TIME_STAMP = "timestamp";
+ }
+
+ public interface SiteMapColumns {
+ String DOCID = "docid";
+ String PARENT_CLASS = "parent_class";
+ String CHILD_CLASS = "child_class";
+ String PARENT_TITLE = "parent_title";
+ String CHILD_TITLE = "child_title";
+ }
+
+ private static final String CREATE_INDEX_TABLE =
+ "CREATE VIRTUAL TABLE " + Tables.TABLE_PREFS_INDEX + " USING fts4" +
+ "(" +
+ IndexColumns.DATA_TITLE +
+ ", " +
+ IndexColumns.DATA_TITLE_NORMALIZED +
+ ", " +
+ IndexColumns.DATA_SUMMARY_ON +
+ ", " +
+ IndexColumns.DATA_SUMMARY_ON_NORMALIZED +
+ ", " +
+ IndexColumns.DATA_SUMMARY_OFF +
+ ", " +
+ IndexColumns.DATA_SUMMARY_OFF_NORMALIZED +
+ ", " +
+ IndexColumns.DATA_ENTRIES +
+ ", " +
+ IndexColumns.DATA_KEYWORDS +
+ ", " +
+ IndexColumns.DATA_PACKAGE +
+ ", " +
+ IndexColumns.SCREEN_TITLE +
+ ", " +
+ IndexColumns.CLASS_NAME +
+ ", " +
+ IndexColumns.ICON +
+ ", " +
+ IndexColumns.INTENT_ACTION +
+ ", " +
+ IndexColumns.INTENT_TARGET_PACKAGE +
+ ", " +
+ IndexColumns.INTENT_TARGET_CLASS +
+ ", " +
+ IndexColumns.ENABLED +
+ ", " +
+ IndexColumns.DATA_KEY_REF +
+ ", " +
+ IndexColumns.PAYLOAD_TYPE +
+ ", " +
+ IndexColumns.PAYLOAD +
+ ");";
+
+ private static final String CREATE_META_TABLE =
+ "CREATE TABLE " + Tables.TABLE_META_INDEX +
+ "(" +
+ MetaColumns.BUILD + " VARCHAR(32) NOT NULL" +
+ ")";
+
+ private static final String CREATE_SAVED_QUERIES_TABLE =
+ "CREATE TABLE " + Tables.TABLE_SAVED_QUERIES +
+ "(" +
+ SavedQueriesColumns.QUERY + " VARCHAR(64) NOT NULL" +
+ ", " +
+ SavedQueriesColumns.TIME_STAMP + " INTEGER" +
+ ")";
+
+ private static final String CREATE_SITE_MAP_TABLE =
+ "CREATE VIRTUAL TABLE " + Tables.TABLE_SITE_MAP + " USING fts4" +
+ "(" +
+ SiteMapColumns.PARENT_CLASS +
+ ", " +
+ SiteMapColumns.CHILD_CLASS +
+ ", " +
+ SiteMapColumns.PARENT_TITLE +
+ ", " +
+ SiteMapColumns.CHILD_TITLE +
+ ")";
+ private static final String INSERT_BUILD_VERSION =
+ "INSERT INTO " + Tables.TABLE_META_INDEX +
+ " VALUES ('" + Build.VERSION.INCREMENTAL + "');";
+
+ private static final String SELECT_BUILD_VERSION =
+ "SELECT " + MetaColumns.BUILD + " FROM " + Tables.TABLE_META_INDEX + " LIMIT 1;";
+
+ private static IndexDatabaseHelper sSingleton;
+
+ private final Context mContext;
+
+ public static synchronized IndexDatabaseHelper getInstance(Context context) {
+ if (sSingleton == null) {
+ sSingleton = new IndexDatabaseHelper(context);
+ }
+ return sSingleton;
+ }
+
+ public IndexDatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ mContext = context.getApplicationContext();
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ bootstrapDB(db);
+ }
+
+ private void bootstrapDB(SQLiteDatabase db) {
+ db.execSQL(CREATE_INDEX_TABLE);
+ db.execSQL(CREATE_META_TABLE);
+ db.execSQL(CREATE_SAVED_QUERIES_TABLE);
+ db.execSQL(CREATE_SITE_MAP_TABLE);
+ db.execSQL(INSERT_BUILD_VERSION);
+ Log.i(TAG, "Bootstrapped database");
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ super.onOpen(db);
+
+ Log.i(TAG, "Using schema version: " + db.getVersion());
+
+ if (!Build.VERSION.INCREMENTAL.equals(getBuildVersion(db))) {
+ Log.w(TAG, "Index needs to be rebuilt as build-version is not the same");
+ // We need to drop the tables and recreate them
+ reconstruct(db);
+ } else {
+ Log.i(TAG, "Index is fine");
+ }
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < DATABASE_VERSION) {
+ Log.w(TAG, "Detected schema version '" + oldVersion + "'. " +
+ "Index needs to be rebuilt for schema version '" + newVersion + "'.");
+ // We need to drop the tables and recreate them
+ reconstruct(db);
+ }
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.w(TAG, "Detected schema version '" + oldVersion + "'. " +
+ "Index needs to be rebuilt for schema version '" + newVersion + "'.");
+ // We need to drop the tables and recreate them
+ reconstruct(db);
+ }
+
+ public void reconstruct(SQLiteDatabase db) {
+ mContext.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE)
+ .edit()
+ .clear()
+ .commit();
+ dropTables(db);
+ bootstrapDB(db);
+ }
+
+ private String getBuildVersion(SQLiteDatabase db) {
+ String version = null;
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery(SELECT_BUILD_VERSION, null);
+ if (cursor.moveToFirst()) {
+ version = cursor.getString(0);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Cannot get build version from Index metadata");
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return version;
+ }
+
+ @VisibleForTesting
+ static String buildProviderVersionedNames(Context context, List<ResolveInfo> providers) {
+ // TODO Refactor update test to reflect version code change.
+ try {
+ StringBuilder sb = new StringBuilder();
+ for (ResolveInfo info : providers) {
+ String packageName = info.providerInfo.packageName;
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName,
+ 0 /* flags */);
+ sb.append(packageName)
+ .append(':')
+ .append(packageInfo.versionCode)
+ .append(',');
+ }
+ // add SettingsIntelligence version as well.
+ sb.append(context.getPackageName())
+ .append(':')
+ .append(context.getPackageManager()
+ .getPackageInfo(context.getPackageName(), 0 /* flags */).versionCode);
+ return sb.toString();
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.d(TAG, "Could not find package name in provider", e);
+ }
+ return "";
+ }
+
+ /**
+ * Set a flag that indicates the search database is fully indexed.
+ */
+ static void setIndexed(Context context, List<ResolveInfo> providers) {
+ final String localeStr = Locale.getDefault().toString();
+ final String fingerprint = Build.FINGERPRINT;
+ final String providerVersionedNames =
+ IndexDatabaseHelper.buildProviderVersionedNames(context, providers);
+ context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE)
+ .edit()
+ .putBoolean(localeStr, true)
+ .putBoolean(fingerprint, true)
+ .putString(PREF_KEY_INDEXED_PROVIDERS, providerVersionedNames)
+ .apply();
+ }
+
+ /**
+ * Checks if the indexed data requires full index. The index data is out of date when:
+ * - Device language has changed
+ * - Device has taken an OTA.
+ * In both cases, the device requires a full index.
+ *
+ * @return true if a full index should be preformed.
+ */
+ static boolean isFullIndex(Context context, List<ResolveInfo> providers) {
+ final String localeStr = Locale.getDefault().toString();
+ final String fingerprint = Build.FINGERPRINT;
+ final String providerVersionedNames =
+ IndexDatabaseHelper.buildProviderVersionedNames(context, providers);
+ final SharedPreferences prefs = context
+ .getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE);
+
+ final boolean isIndexed = prefs.getBoolean(fingerprint, false)
+ && prefs.getBoolean(localeStr, false)
+ && TextUtils.equals(
+ prefs.getString(PREF_KEY_INDEXED_PROVIDERS, null), providerVersionedNames);
+ return !isIndexed;
+ }
+
+ private void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SITE_MAP);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/intelligence/TestConfig.java b/src/com/android/settings/intelligence/search/indexing/IndexingCallback.java
similarity index 73%
rename from tests/robotests/src/com/android/settings/intelligence/TestConfig.java
rename to src/com/android/settings/intelligence/search/indexing/IndexingCallback.java
index 44d238b..908db8f 100644
--- a/tests/robotests/src/com/android/settings/intelligence/TestConfig.java
+++ b/src/com/android/settings/intelligence/search/indexing/IndexingCallback.java
@@ -14,12 +14,15 @@
* limitations under the License.
*/
-package com.android.settings.intelligence;
+package com.android.settings.intelligence.search.indexing;
/**
- * Constants for Robolectric config.
+ * Callback for Settings search indexing.
*/
-public class TestConfig {
- public static final int SDK_VERSION = 26;
- public static final String MANIFEST_PATH = "--default";
+public interface IndexingCallback {
+
+ /**
+ * Called when Indexing is finished.
+ */
+ void onIndexingFinished();
}
diff --git a/src/com/android/settings/intelligence/search/indexing/PreIndexData.java b/src/com/android/settings/intelligence/search/indexing/PreIndexData.java
new file mode 100644
index 0000000..8f8ce07
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/PreIndexData.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.indexing;
+
+import android.provider.SearchIndexableData;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * Holds Data sources for indexable data.
+ */
+public class PreIndexData {
+
+ private final List<SearchIndexableData> mDataToUpdate;
+ private final Map<String, Set<String>> mNonIndexableKeys;
+ private final List<Pair<String, String>> mSiteMapPairs;
+
+ public PreIndexData() {
+ mDataToUpdate = new ArrayList<>();
+ mNonIndexableKeys = new HashMap<>();
+ mSiteMapPairs = new ArrayList<>();
+ }
+
+ public Map<String, Set<String>> getNonIndexableKeys() {
+ return mNonIndexableKeys;
+ }
+
+ public List<SearchIndexableData> getDataToUpdate() {
+ return mDataToUpdate;
+ }
+
+ public List<Pair<String, String>> getSiteMapPairs() {
+ return mSiteMapPairs;
+ }
+
+ public void addNonIndexableKeysForAuthority(String authority, Set<String> keys) {
+ mNonIndexableKeys.put(authority, keys);
+ }
+
+ public void addDataToUpdate(List<? extends SearchIndexableData> data) {
+ mDataToUpdate.addAll(data);
+ }
+
+ public void addSiteMapPairs(List<Pair<String, String>> siteMapPairs) {
+ if (siteMapPairs == null) {
+ return;
+ }
+ mSiteMapPairs.addAll(siteMapPairs);
+ }
+
+}
diff --git a/src/com/android/settings/intelligence/search/indexing/PreIndexDataCollector.java b/src/com/android/settings/intelligence/search/indexing/PreIndexDataCollector.java
new file mode 100644
index 0000000..fbd3209
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/PreIndexDataCollector.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.indexing;
+
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID;
+
+import android.Manifest;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.SearchIndexableResource;
+import android.provider.SearchIndexablesContract;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchIndexableRaw;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Collects all data from {@link android.provider.SearchIndexablesProvider} to be indexed.
+ */
+public class PreIndexDataCollector {
+
+ private static final String TAG = "IndexableDataCollector";
+
+ private static final List<String> EMPTY_LIST = Collections.emptyList();
+
+ private Context mContext;
+
+ private PreIndexData mIndexData;
+
+ public PreIndexDataCollector(Context context) {
+ mContext = context;
+ }
+
+ public PreIndexData collectIndexableData(List<ResolveInfo> providers, boolean isFullIndex) {
+ mIndexData = new PreIndexData();
+
+ for (final ResolveInfo info : providers) {
+ if (!isWellKnownProvider(info)) {
+ continue;
+ }
+ final String authority = info.providerInfo.authority;
+ final String packageName = info.providerInfo.packageName;
+
+ if (isFullIndex) {
+ addIndexablesFromRemoteProvider(packageName, authority);
+ }
+
+ final long nonIndexableStartTime = System.currentTimeMillis();
+ addNonIndexablesKeysFromRemoteProvider(packageName, authority);
+ if (SearchFeatureProvider.DEBUG) {
+ final long nonIndexableTime = System.currentTimeMillis() - nonIndexableStartTime;
+ Log.d(TAG, "performIndexing update non-indexable for package " + packageName
+ + " took time: " + nonIndexableTime);
+ }
+ }
+
+ return mIndexData;
+ }
+
+ private void addIndexablesFromRemoteProvider(String packageName, String authority) {
+ try {
+ final Context context = mContext.createPackageContext(packageName, 0);
+
+ final Uri uriForResources = buildUriForXmlResources(authority);
+ mIndexData.addDataToUpdate(getIndexablesForXmlResourceUri(context, packageName,
+ uriForResources, SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS));
+
+ final Uri uriForRawData = buildUriForRawData(authority);
+ mIndexData.addDataToUpdate(getIndexablesForRawDataUri(context, packageName,
+ uriForRawData, SearchIndexablesContract.INDEXABLES_RAW_COLUMNS));
+
+ final Uri uriForSiteMap = buildUriForSiteMap(authority);
+ mIndexData.addSiteMapPairs(getSiteMapFromProvider(context, uriForSiteMap));
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Could not create context for " + packageName + ": "
+ + Log.getStackTraceString(e));
+ }
+ }
+
+ @VisibleForTesting
+ List<SearchIndexableResource> getIndexablesForXmlResourceUri(Context packageContext,
+ String packageName, Uri uri, String[] projection) {
+
+ final ContentResolver resolver = packageContext.getContentResolver();
+ final Cursor cursor = resolver.query(uri, projection, null, null, null);
+ List<SearchIndexableResource> resources = new ArrayList<>();
+
+ if (cursor == null) {
+ Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
+ return resources;
+ }
+
+ try {
+ final int count = cursor.getCount();
+ if (count > 0) {
+ while (cursor.moveToNext()) {
+ SearchIndexableResource sir = new SearchIndexableResource(packageContext);
+ sir.packageName = packageName;
+ sir.xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
+ sir.className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
+ sir.iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
+ sir.intentAction = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
+ sir.intentTargetPackage = cursor.getString(
+ COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
+ sir.intentTargetClass = cursor.getString(
+ COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);
+ resources.add(sir);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return resources;
+ }
+
+ private void addNonIndexablesKeysFromRemoteProvider(String packageName, String authority) {
+ final List<String> keys =
+ getNonIndexablesKeysFromRemoteProvider(packageName, authority);
+
+ if (keys != null && !keys.isEmpty()) {
+ Set<String> keySet = new ArraySet<>();
+ keySet.addAll(keys);
+ mIndexData.addNonIndexableKeysForAuthority(authority, keySet);
+ }
+ }
+
+ @VisibleForTesting
+ List<String> getNonIndexablesKeysFromRemoteProvider(String packageName,
+ String authority) {
+ try {
+ final Context packageContext = mContext.createPackageContext(packageName, 0);
+
+ final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority);
+ return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys,
+ SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Could not create context for " + packageName + ": "
+ + Log.getStackTraceString(e));
+ return EMPTY_LIST;
+ }
+ }
+
+ private Uri buildUriForXmlResources(String authority) {
+ return Uri.parse("content://" + authority + "/" +
+ SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
+ }
+
+ private Uri buildUriForRawData(String authority) {
+ return Uri.parse("content://" + authority + "/" +
+ SearchIndexablesContract.INDEXABLES_RAW_PATH);
+ }
+
+ private Uri buildUriForNonIndexableKeys(String authority) {
+ return Uri.parse("content://" + authority + "/" +
+ SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
+ }
+
+ @VisibleForTesting
+ Uri buildUriForSiteMap(String authority) {
+ return Uri.parse("content://" + authority + "/settings/site_map_pairs");
+ }
+
+ @VisibleForTesting
+ List<SearchIndexableRaw> getIndexablesForRawDataUri(Context packageContext, String packageName,
+ Uri uri, String[] projection) {
+ final ContentResolver resolver = packageContext.getContentResolver();
+ final Cursor cursor = resolver.query(uri, projection, null, null, null);
+ List<SearchIndexableRaw> rawData = new ArrayList<>();
+
+ if (cursor == null) {
+ Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
+ return rawData;
+ }
+
+ try {
+ final int count = cursor.getCount();
+ if (count > 0) {
+ while (cursor.moveToNext()) {
+ final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE);
+ final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON);
+ final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF);
+ final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES);
+ final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS);
+
+ final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE);
+
+ final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME);
+ final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID);
+
+ final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION);
+ final String targetPackage = cursor.getString(
+ COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE);
+ final String targetClass = cursor.getString(
+ COLUMN_INDEX_RAW_INTENT_TARGET_CLASS);
+
+ final String key = cursor.getString(COLUMN_INDEX_RAW_KEY);
+ final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID);
+
+ SearchIndexableRaw data = new SearchIndexableRaw(packageContext);
+ data.title = title;
+ data.summaryOn = summaryOn;
+ data.summaryOff = summaryOff;
+ data.entries = entries;
+ data.keywords = keywords;
+ data.screenTitle = screenTitle;
+ data.className = className;
+ data.packageName = packageName;
+ data.iconResId = iconResId;
+ data.intentAction = action;
+ data.intentTargetPackage = targetPackage;
+ data.intentTargetClass = targetClass;
+ data.key = key;
+ data.userId = userId;
+
+ rawData.add(data);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return rawData;
+ }
+
+ @VisibleForTesting
+ List<Pair<String, String>> getSiteMapFromProvider(Context packageContext, Uri uri) {
+ final ContentResolver resolver = packageContext.getContentResolver();
+ final Cursor cursor = resolver.query(uri, null, null, null, null);
+ if (cursor == null) {
+ Log.d(TAG, "No site map information from " + packageContext.getPackageName());
+ return null;
+ }
+ final List<Pair<String, String>> siteMapPairs = new ArrayList<>();
+ try {
+ final int count = cursor.getCount();
+ if (count > 0) {
+ while (cursor.moveToNext()) {
+ final String parentClass = cursor.getString(cursor.getColumnIndex(
+ IndexDatabaseHelper.SiteMapColumns.PARENT_CLASS));
+ final String childClass = cursor.getString(cursor.getColumnIndex(
+ IndexDatabaseHelper.SiteMapColumns.CHILD_CLASS));
+ if (TextUtils.isEmpty(parentClass) || TextUtils.isEmpty(childClass)) {
+ Log.w(TAG, "Incomplete site map pair: " + parentClass + "/" + childClass);
+ continue;
+ }
+ siteMapPairs.add(Pair.create(parentClass, childClass));
+ }
+ }
+ return siteMapPairs;
+ } finally {
+ cursor.close();
+ }
+
+ }
+
+ private List<String> getNonIndexablesKeys(Context packageContext, Uri uri,
+ String[] projection) {
+
+ final ContentResolver resolver = packageContext.getContentResolver();
+ final Cursor cursor = resolver.query(uri, projection, null, null, null);
+ final List<String> result = new ArrayList<>();
+
+ if (cursor == null) {
+ Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
+ return result;
+ }
+
+ try {
+ final int count = cursor.getCount();
+ if (count > 0) {
+ while (cursor.moveToNext()) {
+ final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE);
+
+ if (TextUtils.isEmpty(key) && Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Empty non-indexable key from: "
+ + packageContext.getPackageName());
+ continue;
+ }
+
+ result.add(key);
+ }
+ }
+ return result;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Only allow a "well known" SearchIndexablesProvider. The provider should:
+ *
+ * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES}
+ * - be from a privileged package
+ */
+ @VisibleForTesting
+ boolean isWellKnownProvider(ResolveInfo info) {
+ final String authority = info.providerInfo.authority;
+ final String packageName = info.providerInfo.applicationInfo.packageName;
+
+ if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) {
+ return false;
+ }
+
+ final String readPermission = info.providerInfo.readPermission;
+ final String writePermission = info.providerInfo.writePermission;
+
+ if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) {
+ return false;
+ }
+
+ if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) ||
+ !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) {
+ return false;
+ }
+
+ return isPrivilegedPackage(packageName, mContext);
+ }
+
+ /**
+ * @return true if the {@param packageName} is privileged.
+ */
+ private boolean isPrivilegedPackage(String packageName, Context context) {
+ final PackageManager pm = context.getPackageManager();
+ try {
+ PackageInfo packInfo = pm.getPackageInfo(packageName, 0);
+ // TODO REFACTOR Changed privileged check
+ return ((packInfo.applicationInfo.flags
+ & ApplicationInfo.FLAG_SYSTEM) != 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/indexing/XmlParserUtils.java b/src/com/android/settings/intelligence/search/indexing/XmlParserUtils.java
new file mode 100644
index 0000000..6e7db64
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/XmlParserUtils.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.indexing;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+
+import com.android.settings.intelligence.R;
+
+/**
+ * Utility class to parse elements of XML preferences
+ */
+public class XmlParserUtils {
+
+ private static final String TAG = "XmlParserUtils";
+ private static final String NS_APP_RES_AUTO = "http://schemas.android.com/apk/res-auto";
+ private static final String ENTRIES_SEPARATOR = "|";
+
+ public static String getDataKey(Context context, AttributeSet attrs) {
+ return getData(context, attrs,
+ R.styleable.Preference,
+ R.styleable.Preference_android_key);
+ }
+
+ public static String getDataTitle(Context context, AttributeSet attrs) {
+ return getData(context, attrs,
+ R.styleable.Preference,
+ R.styleable.Preference_android_title);
+ }
+
+ public static String getDataSummary(Context context, AttributeSet attrs) {
+ return getData(context, attrs,
+ R.styleable.Preference,
+ R.styleable.Preference_android_summary);
+ }
+
+ public static String getDataSummaryOn(Context context, AttributeSet attrs) {
+ return getData(context, attrs,
+ R.styleable.CheckBoxPreference,
+ R.styleable.CheckBoxPreference_android_summaryOn);
+ }
+
+ public static String getDataSummaryOff(Context context, AttributeSet attrs) {
+ return getData(context, attrs,
+ R.styleable.CheckBoxPreference,
+ R.styleable.CheckBoxPreference_android_summaryOff);
+ }
+
+ public static String getDataEntries(Context context, AttributeSet attrs) {
+ return getDataEntries(context, attrs,
+ R.styleable.ListPreference,
+ R.styleable.ListPreference_android_entries);
+ }
+
+ public static String getDataKeywords(Context context, AttributeSet attrs) {
+ final String keywordRes = attrs.getAttributeValue(NS_APP_RES_AUTO, "keywords");
+ if (TextUtils.isEmpty(keywordRes)) {
+ return null;
+ }
+ // The format of keyword is either a string, or @ followed by int (@123456).
+ // When it's int, we need to look up the actual string from context.
+ if (!keywordRes.startsWith("@")) {
+ // It's a string.
+ return keywordRes;
+ } else {
+ // It's a resource
+ try {
+ final int resValue = Integer.parseInt(keywordRes.substring(1));
+ return context.getString(resValue);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Failed to parse keyword attribute, skipping " + keywordRes);
+ return null;
+ }
+ }
+ }
+
+ public static int getDataIcon(Context context, AttributeSet attrs) {
+ final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Preference);
+ final int dataIcon = ta.getResourceId(R.styleable.Preference_android_icon, 0);
+ ta.recycle();
+ return dataIcon;
+ }
+
+ /**
+ * Returns the fragment name if this preference launches a child fragment.
+ */
+ public static String getDataChildFragment(Context context, AttributeSet attrs) {
+ return getData(context, attrs, R.styleable.Preference,
+ R.styleable.Preference_android_fragment);
+ }
+
+ @Nullable
+ private static String getData(Context context, AttributeSet set, int[] attrs, int resId) {
+ final TypedArray ta = context.obtainStyledAttributes(set, attrs);
+ String data = ta.getString(resId);
+ ta.recycle();
+ return data;
+ }
+
+
+ private static String getDataEntries(Context context, AttributeSet set, int[] attrs,
+ int resId) {
+ final TypedArray sa = context.obtainStyledAttributes(set, attrs);
+ final TypedValue tv = sa.peekValue(resId);
+ sa.recycle();
+ String[] data = null;
+ if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) {
+ if (tv.resourceId != 0) {
+ data = context.getResources().getStringArray(tv.resourceId);
+ }
+ }
+ final int count = (data == null) ? 0 : data.length;
+ if (count == 0) {
+ return null;
+ }
+ final StringBuilder result = new StringBuilder();
+ for (int n = 0; n < count; n++) {
+ result.append(data[n]);
+ result.append(ENTRIES_SEPARATOR);
+ }
+ return result.toString();
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/query/AccessibilityServiceResultTask.java b/src/com/android/settings/intelligence/search/query/AccessibilityServiceResultTask.java
new file mode 100644
index 0000000..cd7b4ad
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/AccessibilityServiceResultTask.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.query;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.graphics.drawable.Drawable;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingUtils;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class AccessibilityServiceResultTask extends SearchQueryTask.QueryWorker {
+
+ public static final int QUERY_WORKER_ID = SettingsIntelligenceLogProto.SettingsIntelligenceEvent
+ .SEARCH_QUERY_ACCESSIBILITY_SERVICES;
+
+ private static final String ACCESSIBILITY_SETTINGS_CLASSNAME =
+ "com.android.settings.accessibility.AccessibilitySettings";
+ private static final int NAME_NO_MATCH = -1;
+
+ private final AccessibilityManager mAccessibilityManager;
+ private final PackageManager mPackageManager;
+
+ private List<String> mBreadcrumb;
+
+ public static SearchQueryTask newTask(Context context, SiteMapManager manager,
+ String query) {
+ return new SearchQueryTask(new AccessibilityServiceResultTask(context, manager, query));
+ }
+
+ public AccessibilityServiceResultTask(Context context, SiteMapManager mapManager,
+ String query) {
+ super(context, mapManager, query);
+ mPackageManager = context.getPackageManager();
+ mAccessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ }
+
+ @Override
+ protected List<? extends SearchResult> query() {
+ final List<SearchResult> results = new ArrayList<>();
+ final List<AccessibilityServiceInfo> services = mAccessibilityManager
+ .getInstalledAccessibilityServiceList();
+ final String screenTitle = mContext.getString(R.string.accessibility_settings);
+ for (AccessibilityServiceInfo service : services) {
+ if (service == null) {
+ continue;
+ }
+ final ResolveInfo resolveInfo = service.getResolveInfo();
+ if (service.getResolveInfo() == null) {
+ continue;
+ }
+ final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ final CharSequence title = resolveInfo.loadLabel(mPackageManager);
+ final int wordDiff = SearchQueryUtils.getWordDifference(title.toString(), mQuery);
+ if (wordDiff == NAME_NO_MATCH) {
+ continue;
+ }
+ final Drawable icon = serviceInfo.loadIcon(mPackageManager);
+ final String componentName = new ComponentName(serviceInfo.packageName,
+ serviceInfo.name).flattenToString();
+ final Intent intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mContext,
+ ACCESSIBILITY_SETTINGS_CLASSNAME, componentName, screenTitle);
+
+ results.add(new SearchResult.Builder()
+ .setTitle(title)
+ .addBreadcrumbs(getBreadCrumb())
+ .setPayload(new ResultPayload(intent))
+ .setRank(wordDiff)
+ .setIcon(icon)
+ .setDataKey(componentName)
+ .build());
+ }
+ Collections.sort(results);
+ return results;
+ }
+
+ @Override
+ protected int getQueryWorkerId() {
+ return QUERY_WORKER_ID;
+ }
+
+ private List<String> getBreadCrumb() {
+ if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
+ mBreadcrumb = mSiteMapManager.buildBreadCrumb(
+ mContext, ACCESSIBILITY_SETTINGS_CLASSNAME,
+ mContext.getString(R.string.accessibility_settings));
+ }
+ return mBreadcrumb;
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/query/CursorToSearchResultConverter.java b/src/com/android/settings/intelligence/search/query/CursorToSearchResultConverter.java
new file mode 100644
index 0000000..dad9f14
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/CursorToSearchResultConverter.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.query;
+
+import static com.android.settings.intelligence.search.query.DatabaseResultTask.BASE_RANKS;
+import static com.android.settings.intelligence.search.SearchResult.TOP_RANK;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.os.BadParcelableException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.ResultPayloadUtils;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Controller to Build search results from {@link Cursor} Objects.
+ *
+ * Each converted {@link Cursor} has the following fields:
+ * - String Title
+ * - String Summary
+ * - int rank
+ * - {@link Drawable} icon
+ * - {@link ResultPayload} payload
+ */
+public class CursorToSearchResultConverter {
+
+ private static final String TAG = "CursorConverter";
+
+ private final Context mContext;
+
+ private final int LONG_TITLE_LENGTH = 20;
+
+ private static final String[] whiteList = {
+ "main_toggle_wifi",
+ "main_toggle_bluetooth",
+ "main_toggle_bluetooth_obsolete",
+ "toggle_airplane",
+ "tether_settings",
+ "battery_saver",
+ "toggle_nfc",
+ "restrict_background",
+ "data_usage_enable",
+ "button_roaming_key",
+ };
+ private static final Set<String> prioritySettings = new HashSet(Arrays.asList(whiteList));
+
+
+ public CursorToSearchResultConverter(Context context) {
+ mContext = context;
+ }
+
+ public Set<SearchResult> convertCursor(Cursor cursorResults, int baseRank,
+ SiteMapManager siteMapManager) {
+ if (cursorResults == null) {
+ return null;
+ }
+ final Map<String, Context> contextMap = new HashMap<>();
+ final Set<SearchResult> results = new HashSet<>();
+
+ while (cursorResults.moveToNext()) {
+ SearchResult result = buildSingleSearchResultFromCursor(siteMapManager,
+ contextMap, cursorResults, baseRank);
+ if (result != null) {
+ results.add(result);
+ }
+ }
+ return results;
+ }
+
+ public static ResultPayload getUnmarshalledPayload(byte[] marshalledPayload,
+ int payloadType) {
+ try {
+ switch (payloadType) {
+ case ResultPayload.PayloadType.INTENT:
+ return ResultPayloadUtils.unmarshall(marshalledPayload,
+ ResultPayload.CREATOR);
+ // TODO REFACTOR (b/62807132) Re-add inline
+// case ResultPayload.PayloadType.INLINE_SWITCH:
+// return ResultPayloadUtils.unmarshall(marshalledPayload,
+// InlineSwitchPayload.CREATOR);
+// case ResultPayload.PayloadType.INLINE_LIST:
+// return ResultPayloadUtils.unmarshall(marshalledPayload,
+// InlineListPayload.CREATOR);
+ }
+ } catch (BadParcelableException e) {
+ Log.w(TAG, "Error creating parcelable: " + e);
+ }
+ return null;
+ }
+
+ private SearchResult buildSingleSearchResultFromCursor(SiteMapManager siteMapManager,
+ Map<String, Context> contextMap, Cursor cursor, int baseRank) {
+ final String pkgName = cursor.getString(cursor.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.DATA_PACKAGE));
+ final String title = cursor.getString(cursor.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.DATA_TITLE));
+ final String summaryOn = cursor.getString(cursor.getColumnIndex(
+ IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON));
+ final String key = cursor.getString(cursor.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.DATA_KEY_REF));
+ final String iconResStr = cursor.getString(cursor.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.ICON));
+ final int payloadType = cursor.getInt(cursor.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE));
+ final byte[] marshalledPayload = cursor.getBlob(cursor.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.PAYLOAD));
+ final ResultPayload payload = getUnmarshalledPayload(marshalledPayload, payloadType);
+
+ final List<String> breadcrumbs = getBreadcrumbs(siteMapManager, cursor);
+ final int rank = getRank(title, baseRank, key);
+ final Drawable icon = getIconForPackage(contextMap, pkgName, iconResStr);
+
+ final SearchResult.Builder builder = new SearchResult.Builder()
+ .setDataKey(key)
+ .setTitle(title)
+ .setSummary(summaryOn)
+ .addBreadcrumbs(breadcrumbs)
+ .setRank(rank)
+ .setIcon(icon)
+ .setPayload(payload);
+ return builder.build();
+ }
+
+ private Drawable getIconForPackage(Map<String, Context> contextMap, String pkgName,
+ String iconResStr) {
+ if (TextUtils.isEmpty(pkgName)) {
+ return null;
+ }
+ final int iconId = TextUtils.isEmpty(iconResStr) ? 0 : Integer.parseInt(iconResStr);
+ if (iconId == 0) {
+ return null;
+ }
+ Context packageContext = contextMap.get(pkgName);
+ if (packageContext == null) {
+ try {
+ packageContext = mContext.createPackageContext(pkgName, 0);
+ contextMap.put(pkgName, packageContext);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Cannot create Context for package: " + pkgName);
+ return null;
+ }
+ }
+ try {
+ final Drawable drawable = packageContext.getDrawable(iconId);
+ Log.d(TAG, "Returning icon, id :" + iconId);
+ return drawable;
+ } catch (Resources.NotFoundException nfe) {
+ Log.w(TAG, "Cannot get icon, pkg/id :" + pkgName + "/" + iconId);
+ return null;
+ }
+ }
+
+ private List<String> getBreadcrumbs(SiteMapManager siteMapManager, Cursor cursor) {
+ final String screenTitle = cursor.getString(cursor.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.SCREEN_TITLE));
+ final String screenClass = cursor.getString(cursor.getColumnIndexOrThrow(
+ IndexDatabaseHelper.IndexColumns.CLASS_NAME));
+ return siteMapManager == null ? null : siteMapManager.buildBreadCrumb(mContext,
+ screenClass, screenTitle);
+ }
+
+ /**
+ * Uses the breadcrumbs to determine the offset to the base rank.
+ * There are three checks
+ * A) If the result is prioritized and the highest base level
+ * B) If the query matches the highest level menu title
+ * C) If the query is longer than 20
+ *
+ * If the query matches A, set it to TOP_RANK
+ * If the query matches B, the offset is 0.
+ * If the query matches C, the offset is 1
+ *
+ * @param title of the result.
+ * @param baseRank of the result. Lower if it's a better result.
+ */
+ private int getRank(String title, int baseRank, String key) {
+ // The result can only be prioritized if it is a top ranked result.
+ if (prioritySettings.contains(key) && baseRank < BASE_RANKS[1]) {
+ return TOP_RANK;
+ }
+ if (title.length() > LONG_TITLE_LENGTH) {
+ return baseRank + 1;
+ }
+ return baseRank;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/query/DatabaseResultTask.java b/src/com/android/settings/intelligence/search/query/DatabaseResultTask.java
new file mode 100644
index 0000000..65d190a
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/DatabaseResultTask.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.query;
+
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns;
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.Tables
+ .TABLE_PREFS_INDEX;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * AsyncTask to retrieve Settings, first party app and any intent based results.
+ */
+public class DatabaseResultTask extends SearchQueryTask.QueryWorker {
+
+ private static final String TAG = "DatabaseResultTask";
+
+ public static final String[] SELECT_COLUMNS = {
+ IndexColumns.DATA_TITLE,
+ IndexColumns.DATA_SUMMARY_ON,
+ IndexColumns.DATA_SUMMARY_OFF,
+ IndexColumns.CLASS_NAME,
+ IndexColumns.SCREEN_TITLE,
+ IndexColumns.ICON,
+ IndexColumns.INTENT_ACTION,
+ IndexColumns.DATA_PACKAGE,
+ IndexColumns.INTENT_TARGET_PACKAGE,
+ IndexColumns.INTENT_TARGET_CLASS,
+ IndexColumns.DATA_KEY_REF,
+ IndexColumns.PAYLOAD_TYPE,
+ IndexColumns.PAYLOAD
+ };
+
+ public static final String[] MATCH_COLUMNS_PRIMARY = {
+ IndexColumns.DATA_TITLE,
+ IndexColumns.DATA_TITLE_NORMALIZED,
+ };
+
+ public static final String[] MATCH_COLUMNS_SECONDARY = {
+ IndexColumns.DATA_SUMMARY_ON,
+ IndexColumns.DATA_SUMMARY_ON_NORMALIZED,
+ IndexColumns.DATA_SUMMARY_OFF,
+ IndexColumns.DATA_SUMMARY_OFF_NORMALIZED,
+ };
+
+ public static final int QUERY_WORKER_ID =
+ SettingsIntelligenceLogProto.SettingsIntelligenceEvent.SEARCH_QUERY_DATABASE;
+
+ /**
+ * Base ranks defines the best possible rank based on what the query matches.
+ * If the query matches the prefix of the first word in the title, the best rank it can be
+ * is 1
+ * If the query matches the prefix of the other words in the title, the best rank it can be
+ * is 3
+ * If the query only matches the summary, the best rank it can be is 7
+ * If the query only matches keywords or entries, the best rank it can be is 9
+ */
+ static final int[] BASE_RANKS = {1, 3, 7, 9};
+
+ public static SearchQueryTask newTask(Context context, SiteMapManager siteMapManager,
+ String query) {
+ return new SearchQueryTask(new DatabaseResultTask(context, siteMapManager, query));
+ }
+
+ public final String[] MATCH_COLUMNS_TERTIARY = {
+ IndexColumns.DATA_KEYWORDS,
+ IndexColumns.DATA_ENTRIES
+ };
+
+ private final CursorToSearchResultConverter mConverter;
+ private final SearchFeatureProvider mFeatureProvider;
+
+ public DatabaseResultTask(Context context, SiteMapManager siteMapManager, String queryText) {
+ super(context, siteMapManager, queryText);
+ mConverter = new CursorToSearchResultConverter(context);
+ mFeatureProvider = FeatureFactory.get(context).searchFeatureProvider();
+ }
+
+ @Override
+ protected int getQueryWorkerId() {
+ return QUERY_WORKER_ID;
+ }
+
+ @Override
+ protected List<? extends SearchResult> query() {
+ if (mQuery == null || mQuery.isEmpty()) {
+ return new ArrayList<>();
+ }
+ // Start a Future to get search result scores.
+ FutureTask<List<Pair<String, Float>>> rankerTask = mFeatureProvider.getRankerTask(
+ mContext, mQuery);
+
+ if (rankerTask != null) {
+ ExecutorService executorService = mFeatureProvider.getExecutorService();
+ executorService.execute(rankerTask);
+ }
+
+ final Set<SearchResult> resultSet = new HashSet<>();
+
+ resultSet.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]));
+ resultSet.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]));
+ resultSet.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]));
+ resultSet.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]));
+
+ // Try to retrieve the scores in time. Otherwise use static ranking.
+ if (rankerTask != null) {
+ try {
+ final long timeoutMs = mFeatureProvider.smartSearchRankingTimeoutMs(mContext);
+ List<Pair<String, Float>> searchRankScores = rankerTask.get(timeoutMs,
+ TimeUnit.MILLISECONDS);
+ return getDynamicRankedResults(resultSet, searchRankScores);
+ } catch (TimeoutException | InterruptedException | ExecutionException e) {
+ Log.d(TAG, "Error waiting for result scores: " + e);
+ }
+ }
+
+ List<SearchResult> resultList = new ArrayList<>(resultSet);
+ Collections.sort(resultList);
+ return resultList;
+ }
+
+ // TODO (b/33577327) Retrieve all search results with a single query.
+
+ /**
+ * Creates and executes the query which matches prefixes of the first word of the given
+ * columns.
+ *
+ * @param matchColumns The columns to match on
+ * @param baseRank The highest rank achievable by these results
+ * @return A set of the matching results.
+ */
+ private Set<SearchResult> firstWordQuery(String[] matchColumns, int baseRank) {
+ final String whereClause = buildSingleWordWhereClause(matchColumns);
+ final String query = mQuery + "%";
+ final String[] selection = buildSingleWordSelection(query, matchColumns.length);
+
+ return query(whereClause, selection, baseRank);
+ }
+
+ /**
+ * Creates and executes the query which matches prefixes of the non-first words of the
+ * given columns.
+ *
+ * @param matchColumns The columns to match on
+ * @param baseRank The highest rank achievable by these results
+ * @return A set of the matching results.
+ */
+ private Set<SearchResult> secondaryWordQuery(String[] matchColumns, int baseRank) {
+ final String whereClause = buildSingleWordWhereClause(matchColumns);
+ final String query = "% " + mQuery + "%";
+ final String[] selection = buildSingleWordSelection(query, matchColumns.length);
+
+ return query(whereClause, selection, baseRank);
+ }
+
+ /**
+ * Creates and executes the query which matches prefixes of the any word of the given
+ * columns.
+ *
+ * @param matchColumns The columns to match on
+ * @param baseRank The highest rank achievable by these results
+ * @return A set of the matching results.
+ */
+ private Set<SearchResult> anyWordQuery(String[] matchColumns, int baseRank) {
+ final String whereClause = buildTwoWordWhereClause(matchColumns);
+ final String[] selection = buildAnyWordSelection(matchColumns.length * 2);
+
+ return query(whereClause, selection, baseRank);
+ }
+
+ /**
+ * Generic method used by all of the query methods above to execute a query.
+ *
+ * @param whereClause Where clause for the SQL query which uses bindings.
+ * @param selection List of the transformed query to match each bind in the whereClause
+ * @param baseRank The highest rank achievable by these results.
+ * @return A set of the matching results.
+ */
+ private Set<SearchResult> query(String whereClause, String[] selection, int baseRank) {
+ final SQLiteDatabase database =
+ IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();
+ try (Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
+ whereClause,
+ selection, null, null, null)) {
+ return mConverter.convertCursor(resultCursor, baseRank, mSiteMapManager);
+ }
+ }
+
+ /**
+ * Builds the SQLite WHERE clause that matches all matchColumns for a single query.
+ *
+ * @param matchColumns List of columns that will be used for matching.
+ * @return The constructed WHERE clause.
+ */
+ private static String buildSingleWordWhereClause(String[] matchColumns) {
+ StringBuilder sb = new StringBuilder(" (");
+ final int count = matchColumns.length;
+ for (int n = 0; n < count; n++) {
+ sb.append(matchColumns[n]);
+ sb.append(" like ? ");
+ if (n < count - 1) {
+ sb.append(" OR ");
+ }
+ }
+ sb.append(") AND enabled = 1");
+ return sb.toString();
+ }
+
+ /**
+ * Builds the SQLite WHERE clause that matches all matchColumns to two different queries.
+ *
+ * @param matchColumns List of columns that will be used for matching.
+ * @return The constructed WHERE clause.
+ */
+ private static String buildTwoWordWhereClause(String[] matchColumns) {
+ StringBuilder sb = new StringBuilder(" (");
+ final int count = matchColumns.length;
+ for (int n = 0; n < count; n++) {
+ sb.append(matchColumns[n]);
+ sb.append(" like ? OR ");
+ sb.append(matchColumns[n]);
+ sb.append(" like ?");
+ if (n < count - 1) {
+ sb.append(" OR ");
+ }
+ }
+ sb.append(") AND enabled = 1");
+ return sb.toString();
+ }
+
+ /**
+ * Fills out the selection array to match the query as the prefix of a single word.
+ *
+ * @param size is the number of columns to be matched.
+ */
+ private String[] buildSingleWordSelection(String query, int size) {
+ String[] selection = new String[size];
+
+ for (int i = 0; i < size; i++) {
+ selection[i] = query;
+ }
+ return selection;
+ }
+
+ /**
+ * Fills out the selection array to match the query as the prefix of a word.
+ *
+ * @param size is twice the number of columns to be matched. The first match is for the
+ * prefix
+ * of the first word in the column. The second match is for any subsequent word
+ * prefix match.
+ */
+ private String[] buildAnyWordSelection(int size) {
+ String[] selection = new String[size];
+ final String query = mQuery + "%";
+ final String subStringQuery = "% " + mQuery + "%";
+
+ for (int i = 0; i < (size - 1); i += 2) {
+ selection[i] = query;
+ selection[i + 1] = subStringQuery;
+ }
+ return selection;
+ }
+
+ private List<SearchResult> getDynamicRankedResults(Set<SearchResult> unsortedSet,
+ final List<Pair<String, Float>> searchRankScores) {
+ final TreeSet<SearchResult> dbResultsSortedByScores = new TreeSet<>(
+ new Comparator<SearchResult>() {
+ @Override
+ public int compare(SearchResult o1, SearchResult o2) {
+ final float score1 = getRankingScoreByKey(searchRankScores, o1.dataKey);
+ final float score2 = getRankingScoreByKey(searchRankScores, o2.dataKey);
+ if (score1 > score2) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+ });
+ dbResultsSortedByScores.addAll(unsortedSet);
+
+ return new ArrayList<>(dbResultsSortedByScores);
+ }
+
+ /**
+ * Looks up ranking score by key.
+ *
+ * @param key key for a single search result.
+ * @return the ranking score corresponding to the given stableId. If there is no score
+ * available for this stableId, -Float.MAX_VALUE is returned.
+ */
+ @VisibleForTesting
+ Float getRankingScoreByKey(List<Pair<String, Float>> searchRankScores, String key) {
+ for (Pair<String, Float> rankingScore : searchRankScores) {
+ if (key.compareTo(rankingScore.first) == 0) {
+ return rankingScore.second;
+ }
+ }
+ // If key not found in the list, we assign the minimum score so it will appear at
+ // the end of the list.
+ Log.w(TAG, key + " was not in the ranking scores.");
+ return -Float.MAX_VALUE;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/query/InputDeviceResultTask.java b/src/com/android/settings/intelligence/search/query/InputDeviceResultTask.java
new file mode 100644
index 0000000..e98ea0d
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/InputDeviceResultTask.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.query;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.icu.text.ListFormatter;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingUtils;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+public class InputDeviceResultTask extends SearchQueryTask.QueryWorker {
+
+ private static final String TAG = "InputResultFutureTask";
+
+ public static final int QUERY_WORKER_ID =
+ SettingsIntelligenceLogProto.SettingsIntelligenceEvent.SEARCH_QUERY_INPUT_DEVICES;
+
+ @VisibleForTesting
+ static final String PHYSICAL_KEYBOARD_FRAGMENT =
+ "com.android.settings.inputmethod.PhysicalKeyboardFragment";
+ @VisibleForTesting
+ static final String VIRTUAL_KEYBOARD_FRAGMENT =
+ "com.android.settings.inputmethod.AvailableVirtualKeyboardFragment";
+
+ public static SearchQueryTask newTask(Context context, SiteMapManager manager,
+ String query) {
+ return new SearchQueryTask(new InputDeviceResultTask(context, manager, query));
+ }
+
+
+ private static final int NAME_NO_MATCH = -1;
+
+ private final InputMethodManager mImm;
+ private final PackageManager mPackageManager;
+
+ private List<String> mPhysicalKeyboardBreadcrumb;
+ private List<String> mVirtualKeyboardBreadcrumb;
+
+ public InputDeviceResultTask(Context context, SiteMapManager manager, String query) {
+ super(context, manager, query);
+
+ mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ mPackageManager = context.getPackageManager();
+ }
+
+ @Override
+ protected int getQueryWorkerId() {
+ return QUERY_WORKER_ID;
+ }
+
+ @Override
+ protected List<? extends SearchResult> query() {
+ long startTime = System.currentTimeMillis();
+ final List<SearchResult> results = new ArrayList<>();
+ results.addAll(buildPhysicalKeyboardSearchResults());
+ results.addAll(buildVirtualKeyboardSearchResults());
+ Collections.sort(results);
+ if (SearchFeatureProvider.DEBUG) {
+ Log.d(TAG, "Input search loading took:" + (System.currentTimeMillis() - startTime));
+ }
+ return results;
+ }
+
+ private Set<SearchResult> buildPhysicalKeyboardSearchResults() {
+ final Set<SearchResult> results = new HashSet<>();
+ final String screenTitle = mContext.getString(R.string.physical_keyboard_title);
+
+ for (final InputDevice device : getPhysicalFullKeyboards()) {
+ final String deviceName = device.getName();
+ final int wordDiff = SearchQueryUtils.getWordDifference(deviceName, mQuery);
+ if (wordDiff == NAME_NO_MATCH) {
+ continue;
+ }
+ final Intent intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mContext,
+ PHYSICAL_KEYBOARD_FRAGMENT, deviceName, screenTitle);
+ results.add(new SearchResult.Builder()
+ .setTitle(deviceName)
+ .setPayload(new ResultPayload(intent))
+ .setDataKey(deviceName)
+ .setRank(wordDiff)
+ .addBreadcrumbs(getPhysicalKeyboardBreadCrumb())
+ .build());
+ }
+ return results;
+ }
+
+ private Set<SearchResult> buildVirtualKeyboardSearchResults() {
+ final Set<SearchResult> results = new HashSet<>();
+ final String screenTitle = mContext.getString(R.string.add_virtual_keyboard);
+ final List<InputMethodInfo> inputMethods = mImm.getInputMethodList();
+ for (InputMethodInfo info : inputMethods) {
+ final String title = info.loadLabel(mPackageManager).toString();
+ final String summary = getSubtypeLocaleNameListAsSentence(
+ getAllSubtypesOf(info), mContext, info);
+ int wordDiff = SearchQueryUtils.getWordDifference(title, mQuery);
+ if (wordDiff == NAME_NO_MATCH) {
+ wordDiff = SearchQueryUtils.getWordDifference(summary, mQuery);
+ }
+ if (wordDiff == NAME_NO_MATCH) {
+ continue;
+ }
+ final ServiceInfo serviceInfo = info.getServiceInfo();
+ final String key = new ComponentName(serviceInfo.packageName, serviceInfo.name)
+ .flattenToString();
+ final Intent intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mContext,
+ VIRTUAL_KEYBOARD_FRAGMENT, key, screenTitle);
+ results.add(new SearchResult.Builder()
+ .setTitle(title)
+ .setSummary(summary)
+ .setRank(wordDiff)
+ .setDataKey(key)
+ .addBreadcrumbs(getVirtualKeyboardBreadCrumb())
+ .setPayload(new ResultPayload(intent))
+ .build());
+ }
+ return results;
+ }
+
+ private List<String> getPhysicalKeyboardBreadCrumb() {
+ if (mPhysicalKeyboardBreadcrumb == null || mPhysicalKeyboardBreadcrumb.isEmpty()) {
+ mPhysicalKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb(
+ mContext, PHYSICAL_KEYBOARD_FRAGMENT,
+ mContext.getString(R.string.physical_keyboard_title));
+ }
+ return mPhysicalKeyboardBreadcrumb;
+ }
+
+
+ private List<String> getVirtualKeyboardBreadCrumb() {
+ if (mVirtualKeyboardBreadcrumb == null || mVirtualKeyboardBreadcrumb.isEmpty()) {
+ final Context context = mContext;
+ mVirtualKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb(
+ context, VIRTUAL_KEYBOARD_FRAGMENT,
+ context.getString(R.string.add_virtual_keyboard));
+ }
+ return mVirtualKeyboardBreadcrumb;
+ }
+
+ private List<InputDevice> getPhysicalFullKeyboards() {
+ final List<InputDevice> keyboards = new ArrayList<>();
+ final int[] deviceIds = InputDevice.getDeviceIds();
+ if (deviceIds != null) {
+ for (int deviceId : deviceIds) {
+ final InputDevice device = InputDevice.getDevice(deviceId);
+ if (isFullPhysicalKeyboard(device)) {
+ keyboards.add(device);
+ }
+ }
+ }
+ return keyboards;
+ }
+
+ @NonNull
+ private static String getSubtypeLocaleNameListAsSentence(
+ @NonNull final List<InputMethodSubtype> subtypes, @NonNull final Context context,
+ @NonNull final InputMethodInfo inputMethodInfo) {
+ if (subtypes.isEmpty()) {
+ return "";
+ }
+ final Locale locale = Locale.getDefault();
+ final int subtypeCount = subtypes.size();
+ final CharSequence[] subtypeNames = new CharSequence[subtypeCount];
+ for (int i = 0; i < subtypeCount; i++) {
+ subtypeNames[i] = subtypes.get(i).getDisplayName(context,
+ inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo()
+ .applicationInfo);
+ }
+ return toSentenceCase(
+ ListFormatter.getInstance(locale).format((Object[]) subtypeNames), locale);
+ }
+
+ private static String toSentenceCase(String str, Locale locale) {
+ if (str.isEmpty()) {
+ return str;
+ }
+ final int firstCodePointLen = str.offsetByCodePoints(0, 1);
+ return str.substring(0, firstCodePointLen).toUpperCase(locale)
+ + str.substring(firstCodePointLen);
+ }
+
+ private static boolean isFullPhysicalKeyboard(InputDevice device) {
+ return device != null && !device.isVirtual() &&
+ (device.getSources() & InputDevice.SOURCE_KEYBOARD)
+ == InputDevice.SOURCE_KEYBOARD
+ && device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC;
+ }
+
+ private static List<InputMethodSubtype> getAllSubtypesOf(final InputMethodInfo imi) {
+ final int subtypeCount = imi.getSubtypeCount();
+ final List<InputMethodSubtype> allSubtypes = new ArrayList<>(subtypeCount);
+ for (int index = 0; index < subtypeCount; index++) {
+ allSubtypes.add(imi.getSubtypeAt(index));
+ }
+ return allSubtypes;
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/query/InstalledAppResultTask.java b/src/com/android/settings/intelligence/search/query/InstalledAppResultTask.java
new file mode 100644
index 0000000..a14850c
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/InstalledAppResultTask.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.query;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.provider.Settings;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.search.AppSearchResult;
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingUtils;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Search loader for installed apps.
+ */
+public class InstalledAppResultTask extends SearchQueryTask.QueryWorker {
+
+ public static final int QUERY_WORKER_ID =
+ SettingsIntelligenceLogProto.SettingsIntelligenceEvent.SEARCH_QUERY_INSTALLED_APPS;
+
+ private final PackageManager mPackageManager;
+ private final String INTENT_SCHEME = "package";
+ private List<String> mBreadcrumb;
+
+ public static SearchQueryTask newTask(Context context, SiteMapManager siteMapManager,
+ String query) {
+ return new SearchQueryTask(new InstalledAppResultTask(context, siteMapManager, query));
+ }
+
+ public InstalledAppResultTask(Context context, SiteMapManager siteMapManager,
+ String query) {
+ super(context, siteMapManager, query);
+ mPackageManager = context.getPackageManager();
+ }
+
+ @Override
+ protected int getQueryWorkerId() {
+ return QUERY_WORKER_ID;
+ }
+
+ @Override
+ protected List<? extends SearchResult> query() {
+ final List<AppSearchResult> results = new ArrayList<>();
+
+ List<ApplicationInfo> appsInfo = mPackageManager.getInstalledApplications(
+ PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS);
+
+ for (ApplicationInfo info : appsInfo) {
+ final CharSequence label = info.loadLabel(mPackageManager);
+ final int wordDiff = SearchQueryUtils.getWordDifference(label.toString(), mQuery);
+ if (wordDiff == SearchQueryUtils.NAME_NO_MATCH) {
+ continue;
+ }
+
+ final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(
+ Uri.fromParts(INTENT_SCHEME, info.packageName, null /* fragment */))
+ .putExtra(DatabaseIndexingUtils.EXTRA_SOURCE_METRICS_CATEGORY,
+ DatabaseIndexingUtils.DASHBOARD_SEARCH_RESULTS);
+
+ final AppSearchResult.Builder builder = new AppSearchResult.Builder();
+ builder.setAppInfo(info)
+ .setDataKey(info.packageName)
+ .setTitle(info.loadLabel(mPackageManager))
+ .setRank(getRank(wordDiff))
+ .addBreadcrumbs(getBreadCrumb())
+ .setPayload(new ResultPayload(intent));
+ results.add(builder.build());
+ }
+
+ Collections.sort(results);
+ return results;
+ }
+
+ private List<String> getBreadCrumb() {
+ if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
+ mBreadcrumb = mSiteMapManager.buildBreadCrumb(
+ mContext, "com.android.settings.applications.ManageApplications",
+ mContext.getString(R.string.applications_settings));
+ }
+ return mBreadcrumb;
+ }
+
+ /**
+ * A temporary ranking scheme for installed apps.
+ *
+ * @param wordDiff difference between query length and app name length.
+ * @return the ranking.
+ */
+ private int getRank(int wordDiff) {
+ if (wordDiff < 6) {
+ return 2;
+ }
+ return 3;
+ }
+}
+
diff --git a/src/com/android/settings/intelligence/search/query/SearchQueryTask.java b/src/com/android/settings/intelligence/search/query/SearchQueryTask.java
new file mode 100644
index 0000000..be0546b
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/SearchQueryTask.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.query;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+
+public class SearchQueryTask extends FutureTask<List<? extends SearchResult>> {
+
+ private final int mId;
+
+ public SearchQueryTask(@NonNull QueryWorker queryWorker) {
+ super(queryWorker);
+ mId = queryWorker.getQueryWorkerId();
+ }
+
+ public int getTaskId() {
+ return mId;
+ }
+
+ public static abstract class QueryWorker implements Callable<List<? extends SearchResult>> {
+
+ protected final Context mContext;
+ protected final SiteMapManager mSiteMapManager;
+ protected final String mQuery;
+
+ public QueryWorker(Context context, SiteMapManager siteMapManager, String query) {
+ mContext = context;
+ mSiteMapManager = siteMapManager;
+ mQuery = query;
+ }
+
+ @Override
+ public List<? extends SearchResult> call() throws Exception {
+ final long startTime = System.currentTimeMillis();
+ try {
+ return query();
+ } finally {
+ final long endTime = System.currentTimeMillis();
+ FeatureFactory.get(mContext).metricsFeatureProvider(mContext)
+ .logEvent(getQueryWorkerId(), endTime - startTime);
+ }
+ }
+
+ protected abstract int getQueryWorkerId();
+
+ protected abstract List<? extends SearchResult> query();
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/query/SearchQueryUtils.java b/src/com/android/settings/intelligence/search/query/SearchQueryUtils.java
new file mode 100644
index 0000000..d7011ee
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/query/SearchQueryUtils.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.query;
+
+import android.text.TextUtils;
+
+/**
+ * Utils for Query-time operations.
+ */
+
+public class SearchQueryUtils {
+
+ public static final int NAME_NO_MATCH = -1;
+
+ /**
+ * Returns "difference" between resultName and query string. resultName must contain all
+ * characters from query as a prefix to a word, in the same order.
+ * If not, returns NAME_NO_MATCH.
+ * If they do match, returns an int value representing how different they are,
+ * and larger values means they are less similar.
+ * <p/>
+ * Example:
+ * resultName: Abcde, query: Abcde, Returns 0
+ * resultName: Abcde, query: abc, Returns 2
+ * resultName: Abcde, query: ab, Returns 3
+ * resultName: Abcde, query: bc, Returns NAME_NO_MATCH
+ * resultName: Abcde, query: xyz, Returns NAME_NO_MATCH
+ * resultName: Abc de, query: de, Returns 4
+ */
+ public static int getWordDifference(String resultName, String query) {
+ if (TextUtils.isEmpty(resultName) || TextUtils.isEmpty(query)) {
+ return NAME_NO_MATCH;
+ }
+
+ final char[] queryTokens = query.toLowerCase().toCharArray();
+ final char[] resultTokens = resultName.toLowerCase().toCharArray();
+ final int resultLength = resultTokens.length;
+ if (queryTokens.length > resultLength) {
+ return NAME_NO_MATCH;
+ }
+
+ int i = 0;
+ int j;
+
+ while (i < resultLength) {
+ j = 0;
+ // Currently matching a prefix
+ while ((i + j < resultLength) && (queryTokens[j] == resultTokens[i + j])) {
+ // Matched the entire query
+ if (++j >= queryTokens.length) {
+ // Use the diff in length as a proxy of how close the 2 words match.
+ // Value range from 0 to infinity.
+ return resultLength - queryTokens.length;
+ }
+ }
+
+ i += j;
+
+ // Remaining string is longer that the query or we have search the whole result name.
+ if (queryTokens.length > resultLength - i) {
+ return NAME_NO_MATCH;
+ }
+
+ // This is the first index where result name and query name are different
+ // Find the next space in the result name or the end of the result name.
+ while ((i < resultLength) && (!Character.isWhitespace(resultTokens[i++]))) ;
+
+ // Find the start of the next word
+ while ((i < resultLength) && !(Character.isLetter(resultTokens[i])
+ || Character.isDigit(resultTokens[i]))) {
+ // Increment in body because we cannot guarantee which condition was true
+ i++;
+ }
+ }
+ return NAME_NO_MATCH;
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/ranking/SearchResultsRankerCallback.java b/src/com/android/settings/intelligence/search/ranking/SearchResultsRankerCallback.java
new file mode 100644
index 0000000..caddffc
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/ranking/SearchResultsRankerCallback.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.ranking;
+
+import android.util.Pair;
+
+import java.util.List;
+
+public interface SearchResultsRankerCallback {
+
+ /**
+ * Called when ranker provides the ranking scores.
+ * @param searchRankingScores Ordered List of Pairs of String and Float corresponding to
+ * stableIds and ranking scores. The list must be descendingly
+ * ordered based on scores.
+ */
+ public void onRankingScoresAvailable(List<Pair<String, Float>> searchRankingScores);
+
+ /**
+ * Called when for any reason ranker fails, which notifies the client to proceed
+ * without ranking results.
+ */
+ public void onRankingFailed();
+}
diff --git a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryController.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryController.java
new file mode 100644
index 0000000..e4c2cc1
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryController.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.savedqueries;
+
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.Loader;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchFragment;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.SearchResultsAdapter;
+
+import java.util.List;
+
+public class SavedQueryController implements LoaderManager.LoaderCallbacks,
+ MenuItem.OnMenuItemClickListener {
+
+ // TODO: make a generic background task manager to handle one-off tasks like this one.
+ private static final String ARG_QUERY = "remove_query";
+ private static final String TAG = "SearchSavedQueryCtrl";
+
+ private static final int MENU_SEARCH_HISTORY = 1000;
+
+ private final Context mContext;
+ private final LoaderManager mLoaderManager;
+ private final SearchFeatureProvider mSearchFeatureProvider;
+ private final SearchResultsAdapter mResultAdapter;
+
+ public SavedQueryController(Context context, LoaderManager loaderManager,
+ SearchResultsAdapter resultsAdapter) {
+ mContext = context;
+ mLoaderManager = loaderManager;
+ mResultAdapter = resultsAdapter;
+ mSearchFeatureProvider = FeatureFactory.get(context)
+ .searchFeatureProvider();
+ }
+
+ @Override
+ public Loader onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case SearchFragment.SearchLoaderId.SAVE_QUERY_TASK:
+ return new SavedQueryRecorder(mContext, args.getString(ARG_QUERY));
+ case SearchFragment.SearchLoaderId.REMOVE_QUERY_TASK:
+ return new SavedQueryRemover(mContext);
+ case SearchFragment.SearchLoaderId.SAVED_QUERIES:
+ return mSearchFeatureProvider.getSavedQueryLoader(mContext);
+ }
+ return null;
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Object data) {
+ switch (loader.getId()) {
+ case SearchFragment.SearchLoaderId.REMOVE_QUERY_TASK:
+ mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.SAVED_QUERIES,
+ null /* args */, this /* callback */);
+ break;
+ case SearchFragment.SearchLoaderId.SAVED_QUERIES:
+ if (SearchFeatureProvider.DEBUG) {
+ Log.d(TAG, "Saved queries loaded");
+ }
+ mResultAdapter.displaySavedQuery((List<SearchResult>) data);
+ break;
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (item.getItemId() != MENU_SEARCH_HISTORY) {
+ return false;
+ }
+ removeQueries();
+ return true;
+ }
+
+ public void buildMenuItem(Menu menu) {
+ final MenuItem item =
+ menu.add(Menu.NONE, MENU_SEARCH_HISTORY, Menu.NONE, R.string.search_clear_history);
+ item.setOnMenuItemClickListener(this);
+ }
+
+ public void saveQuery(String query) {
+ final Bundle args = new Bundle();
+ args.putString(ARG_QUERY, query);
+ mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.SAVE_QUERY_TASK, args,
+ this /* callback */);
+ }
+
+ /**
+ * Remove all saved queries from DB
+ */
+ public void removeQueries() {
+ final Bundle args = new Bundle();
+ mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.REMOVE_QUERY_TASK, args,
+ this /* callback */);
+ }
+
+ public void loadSavedQueries() {
+ if (SearchFeatureProvider.DEBUG) {
+ Log.d(TAG, "loading saved queries");
+ }
+ mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.SAVED_QUERIES, null /* args */,
+ this /* callback */);
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryLoader.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryLoader.java
new file mode 100644
index 0000000..9d0316c
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryLoader.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.savedqueries;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.SavedQueriesColumns;
+import com.android.settings.intelligence.utils.AsyncLoader;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Loader for recently searched queries.
+ */
+public class SavedQueryLoader extends AsyncLoader<List<? extends SearchResult>> {
+
+ // Max number of proposed suggestions
+ @VisibleForTesting
+ static final int MAX_PROPOSED_SUGGESTIONS = 5;
+
+ private final SQLiteDatabase mDatabase;
+
+ public SavedQueryLoader(Context context) {
+ super(context);
+ mDatabase = IndexDatabaseHelper.getInstance(context).getReadableDatabase();
+ }
+
+ @Override
+ protected void onDiscardResult(List<? extends SearchResult> result) {
+
+ }
+
+ @Override
+ public List<? extends SearchResult> loadInBackground() {
+ try (final Cursor cursor = mDatabase.query(
+ IndexDatabaseHelper.Tables.TABLE_SAVED_QUERIES /* table */,
+ new String[]{SavedQueriesColumns.QUERY} /* columns */,
+ null /* selection */,
+ null /* selectionArgs */,
+ null /* groupBy */,
+ null /* having */,
+ "rowId DESC" /* orderBy */,
+ String.valueOf(MAX_PROPOSED_SUGGESTIONS) /* limit */)) {
+ return convertCursorToResult(cursor);
+ }
+ }
+
+ private List<SearchResult> convertCursorToResult(Cursor cursor) {
+ final List<SearchResult> results = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ final SavedQueryPayload payload = new SavedQueryPayload(
+ cursor.getString(cursor.getColumnIndex(SavedQueriesColumns.QUERY)));
+ results.add(new SearchResult.Builder()
+ .setDataKey(payload.query)
+ .setTitle(payload.query)
+ .setPayload(payload)
+ .build());
+ }
+ return results;
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryPayload.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryPayload.java
new file mode 100644
index 0000000..264eee9
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryPayload.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.savedqueries;
+
+import android.os.Parcel;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.settings.intelligence.search.ResultPayload;
+
+/**
+ * {@link ResultPayload} for saved query.
+ */
+public class SavedQueryPayload extends ResultPayload {
+
+ public final String query;
+
+ public SavedQueryPayload(String query) {
+ super(null /* Intent */);
+ this.query = query;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ SavedQueryPayload(Parcel in) {
+ super(null /* Intent */);
+ query = in.readString();
+ }
+
+ @Override
+ public int getType() {
+ return PayloadType.SAVED_QUERY;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(query);
+ }
+
+ public static final Creator<SavedQueryPayload> CREATOR = new Creator<SavedQueryPayload>() {
+ @Override
+ public SavedQueryPayload createFromParcel(Parcel in) {
+ return new SavedQueryPayload(in);
+ }
+
+ @Override
+ public SavedQueryPayload[] newArray(int size) {
+ return new SavedQueryPayload[size];
+ }
+ };
+}
diff --git a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryRecorder.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryRecorder.java
new file mode 100644
index 0000000..618e0e7
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryRecorder.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.savedqueries;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.utils.AsyncLoader;
+
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.Tables.TABLE_SAVED_QUERIES;
+
+/**
+ * A background task to update saved queries.
+ */
+public class SavedQueryRecorder extends AsyncLoader<Void> {
+
+ private static final String LOG_TAG = "SavedQueryRecorder";
+
+ // Max number of saved search queries (who will be used for proposing suggestions)
+ private static long MAX_SAVED_SEARCH_QUERY = 64;
+
+ private final String mQuery;
+
+ public SavedQueryRecorder(Context context, String query) {
+ super(context);
+ mQuery = query;
+ }
+
+ @Override
+ protected void onDiscardResult(Void result) {
+
+ }
+
+ @Override
+ public Void loadInBackground() {
+ final long now = System.currentTimeMillis();
+
+ final ContentValues values = new ContentValues();
+ values.put(IndexDatabaseHelper.SavedQueriesColumns.QUERY, mQuery);
+ values.put(IndexDatabaseHelper.SavedQueriesColumns.TIME_STAMP, now);
+
+ final SQLiteDatabase database = getWritableDatabase();
+ if (database == null) {
+ return null;
+ }
+
+ long lastInsertedRowId;
+ try {
+ // First, delete all saved queries that are the same
+ database.delete(TABLE_SAVED_QUERIES,
+ IndexDatabaseHelper.SavedQueriesColumns.QUERY + " = ?",
+ new String[]{mQuery});
+
+ // Second, insert the saved query
+ lastInsertedRowId = database.insertOrThrow(TABLE_SAVED_QUERIES, null, values);
+
+ // Last, remove "old" saved queries
+ final long delta = lastInsertedRowId - MAX_SAVED_SEARCH_QUERY;
+ if (delta > 0) {
+ int count = database.delete(TABLE_SAVED_QUERIES,
+ "rowId <= ?",
+ new String[]{Long.toString(delta)});
+ Log.d(LOG_TAG, "Deleted '" + count + "' saved Search query(ies)");
+ }
+ } catch (Exception e) {
+ Log.d(LOG_TAG, "Cannot update saved Search queries", e);
+ }
+ return null;
+ }
+
+ private SQLiteDatabase getWritableDatabase() {
+ try {
+ return IndexDatabaseHelper.getInstance(getContext()).getWritableDatabase();
+ } catch (SQLiteException e) {
+ Log.e(LOG_TAG, "Cannot open writable database", e);
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryRemover.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryRemover.java
new file mode 100644
index 0000000..7c600d4
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryRemover.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.savedqueries;
+
+import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.Tables.TABLE_SAVED_QUERIES;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.utils.AsyncLoader;
+
+
+public class SavedQueryRemover extends AsyncLoader<Void> {
+
+ private static final String LOG_TAG = "SavedQueryRemover";
+
+ public SavedQueryRemover(Context context) {
+ super(context);
+ }
+
+ @Override
+ public Void loadInBackground() {
+ final SQLiteDatabase database = getWritableDatabase();
+ try {
+ // First, delete all saved queries that are the same
+ database.delete(TABLE_SAVED_QUERIES,
+ null /* where */,
+ null /* whereArgs */);
+ } catch (Exception e) {
+ Log.d(LOG_TAG, "Cannot update saved Search queries", e);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onDiscardResult(Void result) {
+
+ }
+
+ private SQLiteDatabase getWritableDatabase() {
+ try {
+ return IndexDatabaseHelper.getInstance(getContext()).getWritableDatabase();
+ } catch (SQLiteException e) {
+ Log.e(LOG_TAG, "Cannot open writable database", e);
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryViewHolder.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryViewHolder.java
new file mode 100644
index 0000000..40d94f5
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryViewHolder.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.savedqueries;
+
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
+import com.android.settings.intelligence.search.SearchFragment;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.SearchViewHolder;
+
+public class SavedQueryViewHolder extends SearchViewHolder {
+
+ public final TextView titleView;
+
+ public SavedQueryViewHolder(View view) {
+ super(view);
+ titleView = view.findViewById(android.R.id.title);
+ }
+
+ @Override
+ public int getClickActionMetricName() {
+ return SettingsIntelligenceLogProto.SettingsIntelligenceEvent.CLICK_SAVED_QUERY;
+ }
+
+ @Override
+ public void onBind(final SearchFragment fragment, final SearchResult result) {
+ itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ fragment.onSavedQueryClicked(SavedQueryViewHolder.this,
+ result.title);
+ }
+ });
+ titleView.setText(result.title);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/sitemap/SiteMapManager.java b/src/com/android/settings/intelligence/search/sitemap/SiteMapManager.java
new file mode 100644
index 0000000..315cd4f
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/sitemap/SiteMapManager.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.sitemap;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.SiteMapColumns;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SiteMapManager {
+
+ private static final String TAG = "SiteMapManager";
+ private static final boolean DEBUG_TIMING = false;
+
+ public static final String[] SITE_MAP_COLUMNS = {
+ SiteMapColumns.PARENT_CLASS,
+ SiteMapColumns.PARENT_TITLE,
+ SiteMapColumns.CHILD_CLASS,
+ SiteMapColumns.CHILD_TITLE
+ };
+
+ private final List<SiteMapPair> mPairs = new ArrayList<>();
+
+ private boolean mInitialized;
+
+ /**
+ * Given a fragment class name and its screen title, build a breadcrumb from Settings root to
+ * this screen.
+ * <p/>
+ * Not all screens have a full breadcrumb path leading up to root, it's because either some
+ * page in the breadcrumb path is not indexed, or it's only reachable via search.
+ */
+ @WorkerThread
+ public synchronized List<String> buildBreadCrumb(Context context, String clazz,
+ String screenTitle) {
+ init(context);
+ final long startTime = System.currentTimeMillis();
+ final List<String> breadcrumbs = new ArrayList<>();
+ if (!mInitialized) {
+ Log.w(TAG, "SiteMap is not initialized yet, skipping");
+ return breadcrumbs;
+ }
+ breadcrumbs.add(screenTitle);
+ String currentClass = clazz;
+ String currentTitle = screenTitle;
+ // Look up current page's parent, if found add it to breadcrumb string list, and repeat.
+ while (true) {
+ final SiteMapPair pair = lookUpParent(currentClass, currentTitle);
+ if (pair == null) {
+ if (DEBUG_TIMING) {
+ Log.d(TAG, "BreadCrumb timing: " + (System.currentTimeMillis() - startTime));
+ }
+ return breadcrumbs;
+ }
+ breadcrumbs.add(0, pair.getParentTitle());
+ currentClass = pair.getParentClass();
+ currentTitle = pair.getParentTitle();
+ }
+ }
+
+ /**
+ * Initialize a list of {@link SiteMapPair}s. Each pair knows about a single parent-child
+ * page relationship.
+ */
+ @WorkerThread
+ private synchronized void init(Context context) {
+ if (mInitialized) {
+ // Make sure only init once.
+ return;
+ }
+ final long startTime = System.currentTimeMillis();
+ // First load site map from static index table.
+ final Context appContext = context.getApplicationContext();
+ final SQLiteDatabase db = IndexDatabaseHelper.getInstance(appContext).getReadableDatabase();
+ Cursor sitemap = db.query(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, SITE_MAP_COLUMNS, null,
+ null, null, null, null);
+ while (sitemap.moveToNext()) {
+ final SiteMapPair pair = new SiteMapPair(
+ sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.PARENT_CLASS)),
+ sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.PARENT_TITLE)),
+ sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.CHILD_CLASS)),
+ sitemap.getString(sitemap.getColumnIndex(SiteMapColumns.CHILD_TITLE)));
+ mPairs.add(pair);
+ }
+ sitemap.close();
+ // Done.
+ mInitialized = true;
+ if (DEBUG_TIMING) {
+ Log.d(TAG, "Init timing: " + (System.currentTimeMillis() - startTime));
+ }
+ }
+
+ @WorkerThread
+ private SiteMapPair lookUpParent(String clazz, String title) {
+ for (SiteMapPair pair : mPairs) {
+ if (TextUtils.equals(pair.getChildClass(), clazz)
+ && TextUtils.equals(title, pair.getChildTitle())) {
+ return pair;
+ }
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/sitemap/SiteMapPair.java b/src/com/android/settings/intelligence/search/sitemap/SiteMapPair.java
new file mode 100644
index 0000000..cf30dc4
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/sitemap/SiteMapPair.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.search.sitemap;
+
+import android.content.ContentValues;
+import android.text.TextUtils;
+
+import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
+
+import java.util.Objects;
+
+/**
+ * Data model for a parent-child page pair.
+ * <p/>
+ * A list of {@link SiteMapPair} can represent the breadcrumb for a search result from settings.
+ */
+public class SiteMapPair {
+ private final String mParentClass;
+ private final String mParentTitle;
+ private final String mChildClass;
+ private final String mChildTitle;
+
+ public SiteMapPair(String parentClass, String parentTitle, String childClass,
+ String childTitle) {
+ mParentClass = parentClass;
+ mParentTitle = parentTitle;
+ mChildClass = childClass;
+ mChildTitle = childTitle;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mParentClass, mChildClass);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null || !(other instanceof SiteMapPair)) {
+ return false;
+ }
+ return TextUtils.equals(mParentClass, ((SiteMapPair) other).mParentClass)
+ && TextUtils.equals(mChildClass, ((SiteMapPair) other).mChildClass);
+ }
+
+ public String getParentClass() {
+ return mParentClass;
+ }
+
+ public String getParentTitle() {
+ return mParentTitle;
+ }
+
+ public String getChildClass() {
+ return mChildClass;
+ }
+
+ public String getChildTitle() {
+ return mChildTitle;
+ }
+
+ /**
+ * Converts this object into {@link ContentValues}. The content follows schema in
+ * {@link IndexDatabaseHelper.SiteMapColumns}.
+ */
+ public ContentValues toContentValue() {
+ final ContentValues values = new ContentValues();
+ values.put(IndexDatabaseHelper.SiteMapColumns.DOCID, hashCode());
+ values.put(IndexDatabaseHelper.SiteMapColumns.PARENT_CLASS, mParentClass);
+ values.put(IndexDatabaseHelper.SiteMapColumns.PARENT_TITLE, mParentTitle);
+ values.put(IndexDatabaseHelper.SiteMapColumns.CHILD_CLASS, mChildClass);
+ values.put(IndexDatabaseHelper.SiteMapColumns.CHILD_TITLE, mChildTitle);
+ return values;
+ }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionDismissHandler.java b/src/com/android/settings/intelligence/suggestions/SuggestionDismissHandler.java
deleted file mode 100644
index 400227b..0000000
--- a/src/com/android/settings/intelligence/suggestions/SuggestionDismissHandler.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions;
-
-import android.content.Context;
-
-public class SuggestionDismissHandler {
-
- private static final String IS_DISMISSED = "_is_dismissed";
-
- private static SuggestionDismissHandler sDismissHandler;
-
- private SuggestionDismissHandler() {}
-
- public static SuggestionDismissHandler getInstance() {
- if (sDismissHandler == null) {
- sDismissHandler = new SuggestionDismissHandler();
- }
- return sDismissHandler;
- }
-
- public void markSuggestionDismissed(Context context, String id) {
- SuggestionService.getSharedPrefs(context)
- .edit()
- .putBoolean(getDismissKey(id), true)
- .apply();
- }
-
- public void markSuggestionNotDismissed(Context context, String id) {
- SuggestionService.getSharedPrefs(context)
- .edit()
- .putBoolean(getDismissKey(id), false)
- .apply();
- }
-
- public boolean isSuggestionDismissed(Context context, String id) {
- return SuggestionService.getSharedPrefs(context)
- .getBoolean(getDismissKey(id), false);
- }
-
- private static String getDismissKey(String id) {
- return id + IS_DISMISSED;
- }
-}
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionFeatureProvider.java b/src/com/android/settings/intelligence/suggestions/SuggestionFeatureProvider.java
new file mode 100644
index 0000000..ec89385
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/SuggestionFeatureProvider.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.suggestions;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.service.settings.suggestions.Suggestion;
+
+import com.android.settings.intelligence.suggestions.ranking.SuggestionEventStore;
+import com.android.settings.intelligence.suggestions.ranking.SuggestionFeaturizer;
+import com.android.settings.intelligence.suggestions.ranking.SuggestionRanker;
+
+import java.util.List;
+
+public class SuggestionFeatureProvider {
+
+ private static final String SHARED_PREF_FILENAME = "suggestions";
+ private static final String IS_DISMISSED = "_is_dismissed";
+
+ private SuggestionRanker mRanker;
+
+ /**
+ * Returns the {@link SharedPreferences} to store suggestion related user data.
+ */
+ public SharedPreferences getSharedPrefs(Context context) {
+ return context.getApplicationContext()
+ .getSharedPreferences(SHARED_PREF_FILENAME, Context.MODE_PRIVATE);
+ }
+
+ /**
+ * Returns a list of suggestions that UI should display, sorted based on importance.
+ */
+ public List<Suggestion> getSuggestions(Context context) {
+ final SuggestionParser parser = new SuggestionParser(context);
+ final List<Suggestion> list = parser.getSuggestions();
+
+ final List<Suggestion> rankedSuggestions = getRanker(context).rankRelevantSuggestions(list);
+
+ final SuggestionEventStore eventStore = SuggestionEventStore.get(context);
+ for (Suggestion suggestion : rankedSuggestions) {
+ eventStore.writeEvent(suggestion.getId(), SuggestionEventStore.EVENT_SHOWN);
+ }
+ return rankedSuggestions;
+ }
+
+ /**
+ * Mark a suggestion as dismissed
+ */
+ public void markSuggestionDismissed(Context context, String id) {
+ getSharedPrefs(context)
+ .edit()
+ .putBoolean(getDismissKey(id), true)
+ .apply();
+
+ SuggestionEventStore.get(context).writeEvent(id, SuggestionEventStore.EVENT_DISMISSED);
+ }
+
+ /**
+ * Mark a suggestion as not dismissed
+ */
+ public void markSuggestionNotDismissed(Context context, String id) {
+ getSharedPrefs(context)
+ .edit()
+ .putBoolean(getDismissKey(id), false)
+ .apply();
+ }
+
+ public void markSuggestionLaunched(Context context, String id) {
+ SuggestionEventStore.get(context).writeEvent(id, SuggestionEventStore.EVENT_CLICKED);
+ }
+
+ /**
+ * Whether or not a suggestion is dismissed
+ */
+ public boolean isSuggestionDismissed(Context context, String id) {
+ return getSharedPrefs(context)
+ .getBoolean(getDismissKey(id), false);
+ }
+
+ /**
+ * Returns a manager that knows how to rank suggestions.
+ */
+ protected SuggestionRanker getRanker(Context context) {
+ if (mRanker == null) {
+ mRanker = new SuggestionRanker(context,
+ new SuggestionFeaturizer(context.getApplicationContext()));
+ }
+ return mRanker;
+ }
+
+ private static String getDismissKey(String id) {
+ return id + IS_DISMISSED;
+ }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionParser.java b/src/com/android/settings/intelligence/suggestions/SuggestionParser.java
index c5af82e..f7cecd2 100644
--- a/src/com/android/settings/intelligence/suggestions/SuggestionParser.java
+++ b/src/com/android/settings/intelligence/suggestions/SuggestionParser.java
@@ -30,6 +30,8 @@
import android.util.ArrayMap;
import android.util.Log;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.suggestions.eligibility.CandidateSuggestionFilter;
import com.android.settings.intelligence.suggestions.model.CandidateSuggestion;
import com.android.settings.intelligence.suggestions.model.SuggestionCategory;
import com.android.settings.intelligence.suggestions.model.SuggestionListBuilder;
@@ -58,7 +60,8 @@
mContext = context.getApplicationContext();
mPackageManager = context.getPackageManager();
mAddCache = new ArrayMap<>();
- mSharedPrefs = SuggestionService.getSharedPrefs(mContext);
+ mSharedPrefs = FeatureFactory.get(mContext)
+ .suggestionFeatureProvider().getSharedPrefs(mContext);
}
public List<Suggestion> getSuggestions() {
@@ -96,13 +99,24 @@
probe.addCategory(category.getCategory());
List<ResolveInfo> results = mPackageManager
.queryIntentActivities(probe, PackageManager.GET_META_DATA);
+
+ // Build a list of eligible candidates
+ final List<CandidateSuggestion> eligibleCandidates = new ArrayList<>();
for (ResolveInfo resolved : results) {
final CandidateSuggestion candidate = new CandidateSuggestion(mContext, resolved,
ignoreDismissRule);
if (!candidate.isEligible()) {
continue;
}
+ eligibleCandidates.add(candidate);
+ }
+ // Then remove completed ones
+ final List<CandidateSuggestion> incompleteSuggestions = CandidateSuggestionFilter
+ .getInstance()
+ .filterCandidates(mContext, eligibleCandidates);
+ // Convert the rest to suggestion.
+ for (CandidateSuggestion candidate : incompleteSuggestions) {
final String id = candidate.getId();
Suggestion suggestion = mAddCache.get(id);
if (suggestion == null) {
@@ -137,67 +151,4 @@
+ category.getCategory());
return elapsedTime > category.getExclusiveExpireDaysInMillis();
}
-
-//
-// /**
-// * Gets text associated with the input key from the content provider.
-// * @param context context
-// * @param uriString URI for the content provider
-// * @param providerMap Maps URI authorities to providers
-// * @param key Key mapping to the text in bundle returned by the content provider
-// * @return Text associated with the key, if returned by the content provider
-// */
-// public static String getTextFromUri(Context context, String uriString,
-// Map<String, IContentProvider> providerMap, String key) {
-// Bundle bundle = getBundleFromUri(context, uriString, providerMap);
-// return (bundle != null) ? bundle.getString(key) : null;
-// }
-//
-// private static Bundle getBundleFromUri(Context context, String uriString,
-// Map<String, IContentProvider> providerMap) {
-// if (TextUtils.isEmpty(uriString)) {
-// return null;
-// }
-// Uri uri = Uri.parse(uriString);
-// String method = getMethodFromUri(uri);
-// if (TextUtils.isEmpty(method)) {
-// return null;
-// }
-// IContentProvider provider = getProviderFromUri(context, uri, providerMap);
-// if (provider == null) {
-// return null;
-// }
-// try {
-// return provider.call(context.getPackageName(), method, uriString, null);
-// } catch (RemoteException e) {
-// return null;
-// }
-// }
-//
-// private static IContentProvider getProviderFromUri(Context context, Uri uri,
-// Map<String, IContentProvider> providerMap) {
-// if (uri == null) {
-// return null;
-// }
-// String authority = uri.getAuthority();
-// if (TextUtils.isEmpty(authority)) {
-// return null;
-// }
-// if (!providerMap.containsKey(authority)) {
-// providerMap.put(authority, context.getContentResolver().acquireUnstableProvider(uri));
-// }
-// return providerMap.get(authority);
-// }
-//
-// /** Returns the first path segment of the uri if it exists as the method, otherwise null. */
-// static String getMethodFromUri(Uri uri) {
-// if (uri == null) {
-// return null;
-// }
-// List<String> pathSegments = uri.getPathSegments();
-// if ((pathSegments == null) || pathSegments.isEmpty()) {
-// return null;
-// }
-// return pathSegments.get(0);
-// }
}
diff --git a/src/com/android/settings/intelligence/suggestions/SuggestionService.java b/src/com/android/settings/intelligence/suggestions/SuggestionService.java
index 630a219..2a90afb 100644
--- a/src/com/android/settings/intelligence/suggestions/SuggestionService.java
+++ b/src/com/android/settings/intelligence/suggestions/SuggestionService.java
@@ -16,44 +16,61 @@
package com.android.settings.intelligence.suggestions;
-import android.content.Context;
-import android.content.SharedPreferences;
import android.service.settings.suggestions.Suggestion;
import android.util.Log;
-import com.android.settings.intelligence.suggestions.ranking.SuggestionRanker;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import java.util.ArrayList;
import java.util.List;
public class SuggestionService extends android.service.settings.suggestions.SuggestionService {
private static final String TAG = "SuggestionService";
- private static final String SHARED_PREF_FILENAME = "suggestions";
-
@Override
public List<Suggestion> onGetSuggestions() {
- final SuggestionParser parser = new SuggestionParser(this);
- final List<Suggestion> list = parser.getSuggestions();
- SuggestionRanker.getInstance(this).rankSuggestions(list);
+ final long startTime = System.currentTimeMillis();
+ final List<Suggestion> list = FeatureFactory.get(this)
+ .suggestionFeatureProvider()
+ .getSuggestions(this);
+
+ final List<String> ids = new ArrayList<>(list.size());
+ for (Suggestion suggestion : list) {
+ ids.add(suggestion.getId());
+ }
+ final long endTime = System.currentTimeMillis();
+ FeatureFactory.get(this)
+ .metricsFeatureProvider(this)
+ .logGetSuggestion(ids, endTime - startTime);
return list;
}
@Override
public void onSuggestionDismissed(Suggestion suggestion) {
+ final long startTime = System.currentTimeMillis();
final String id = suggestion.getId();
Log.d(TAG, "dismissing suggestion " + id);
- SuggestionDismissHandler.getInstance()
+ final long endTime = System.currentTimeMillis();
+ FeatureFactory.get(this)
+ .suggestionFeatureProvider()
.markSuggestionDismissed(this /* context */, id);
+ FeatureFactory.get(this)
+ .metricsFeatureProvider(this)
+ .logDismissSuggestion(id, endTime - startTime);
}
@Override
public void onSuggestionLaunched(Suggestion suggestion) {
- Log.d(TAG, "Suggestion launched" + suggestion.getId());
- }
-
- public static SharedPreferences getSharedPrefs(Context context) {
- return context.getApplicationContext()
- .getSharedPreferences(SHARED_PREF_FILENAME, Context.MODE_PRIVATE);
+ final long startTime = System.currentTimeMillis();
+ final String id = suggestion.getId();
+ Log.d(TAG, "Suggestion is launched" + id);
+ final long endTime = System.currentTimeMillis();
+ FeatureFactory.get(this)
+ .suggestionFeatureProvider()
+ .markSuggestionLaunched(this, id);
+ FeatureFactory.get(this)
+ .metricsFeatureProvider(this)
+ .logLaunchSuggestion(id, endTime - startTime);
}
}
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/CandidateSuggestionFilter.java b/src/com/android/settings/intelligence/suggestions/eligibility/CandidateSuggestionFilter.java
new file mode 100644
index 0000000..5f614f6
--- /dev/null
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/CandidateSuggestionFilter.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2017 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.settings.intelligence.suggestions.eligibility;
+
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.settings.intelligence.suggestions.model.CandidateSuggestion;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Filters candidate list to only valid ones.
+ */
+public class CandidateSuggestionFilter {
+
+ private static final String TAG = "CandidateSuggestionFilter";
+ private static final long CHECK_TASK_TIMEOUT_MS = 200;
+
+ private static CandidateSuggestionFilter sChecker;
+ private static ExecutorService sExecutorService;
+
+ public static CandidateSuggestionFilter getInstance() {
+ if (sChecker == null) {
+ sChecker = new CandidateSuggestionFilter();
+ sExecutorService = Executors.newCachedThreadPool();
+ }
+ return sChecker;
+ }
+
+ @NonNull
+ public synchronized List<CandidateSuggestion> filterCandidates(Context context,
+ List<CandidateSuggestion> candidates) {
+ final long startTime = System.currentTimeMillis();
+ final List<CandidateFilterTask> checkTasks = new ArrayList<>();
+ final List<CandidateSuggestion> incompleteCandidates = new ArrayList<>();
+ if (candidates == null) {
+ return incompleteCandidates;
+ }
+ // Put a check task into ExecutorService for each candidate.
+ for (CandidateSuggestion candidate : candidates) {
+ final CandidateFilterTask task = new CandidateFilterTask(context, candidate);
+ sExecutorService.execute(task);
+ checkTasks.add(task);
+ }
+ for (CandidateFilterTask task : checkTasks) {
+ try {
+ final CandidateSuggestion candidate = task.get(CHECK_TASK_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS);
+ if (candidate != null) {
+ incompleteCandidates.add(candidate);
+ }
+ } catch (TimeoutException | InterruptedException | ExecutionException e) {
+ Log.w(TAG, "Error checking completion state for " + task.getId());
+ }
+ }
+ final long endTime = System.currentTimeMillis();
+ Log.d(TAG, "filterCandidates duration: " + (endTime - startTime));
+ return incompleteCandidates;
+ }
+
+ /**
+ * {@link FutureTask} that filters status for a suggestion candidate.
+ * <p/>
+ * If the candidate status is valid, {@link #get()} will return the candidate itself.
+ * Otherwise it returns null.
+ */
+ static class CandidateFilterTask extends FutureTask<CandidateSuggestion> {
+
+ private static final String EXTRA_CANDIDATE_ID = "candidate_id";
+ private static final String RESULT_IS_COMPLETE = "candidate_is_complete";
+
+ private final String mId;
+
+ public CandidateFilterTask(Context context, CandidateSuggestion candidate) {
+ super(new GetSuggestionStatusCallable(context, candidate));
+ mId = candidate.getId();
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ @VisibleForTesting
+ static class GetSuggestionStatusCallable implements Callable<CandidateSuggestion> {
+ @VisibleForTesting
+ static final String CONTENT_PROVIDER_INTENT_ACTION =
+ "com.android.settings.action.SUGGESTION_STATE_PROVIDER";
+ private static final String METHOD_GET_SUGGESTION_STATE = "getSuggestionState";
+
+ private final Context mContext;
+ private final CandidateSuggestion mCandidate;
+
+ public GetSuggestionStatusCallable(Context context, CandidateSuggestion candidate) {
+ mContext = context.getApplicationContext();
+ mCandidate = candidate;
+ }
+
+ @Override
+ public CandidateSuggestion call() throws Exception {
+ // First find if candidate has any state provider.
+ final String packageName = mCandidate.getComponent().getPackageName();
+ final Intent probe = new Intent(CONTENT_PROVIDER_INTENT_ACTION)
+ .setPackage(packageName);
+ final List<ResolveInfo> providers = mContext.getPackageManager()
+ .queryIntentContentProviders(probe, 0 /* flags */);
+ if (providers == null || providers.isEmpty()) {
+ // No provider, let it go through
+ return mCandidate;
+ }
+ final ProviderInfo providerInfo = providers.get(0).providerInfo;
+ if (providerInfo == null || TextUtils.isEmpty(providerInfo.authority)) {
+ // Bad provider - don't let candidate pass through.
+ return null;
+ }
+ // Query candidate state (isComplete)
+ final Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(providerInfo.authority)
+ .build();
+ final Bundle result = mContext.getContentResolver().call(
+ uri, METHOD_GET_SUGGESTION_STATE, null /* args */,
+ buildGetSuggestionStateExtras(mCandidate));
+ final boolean isComplete = result.getBoolean(RESULT_IS_COMPLETE, false);
+ Log.d(TAG, "Suggestion state result " + result);
+ return isComplete ? null : mCandidate;
+ }
+
+ @VisibleForTesting
+ static Bundle buildGetSuggestionStateExtras(CandidateSuggestion candidate) {
+ final Bundle args = new Bundle();
+ final String id = candidate.getId();
+ args.putString(EXTRA_CANDIDATE_ID, id);
+ args.putParcelable(EXTRA_COMPONENT_NAME, candidate.getComponent());
+ return args;
+ }
+ }
+ }
+}
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java b/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java
index a28ae3d..ba24baf 100644
--- a/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/DismissedChecker.java
@@ -23,8 +23,8 @@
import android.text.format.DateUtils;
import android.util.Log;
-import com.android.settings.intelligence.suggestions.SuggestionDismissHandler;
-import com.android.settings.intelligence.suggestions.SuggestionService;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.suggestions.SuggestionFeatureProvider;
public class DismissedChecker {
@@ -42,7 +42,8 @@
// Shared prefs keys for storing dismissed state.
// Index into current dismissed state.
- private static final String SETUP_TIME = "_setup_time";
+ @VisibleForTesting
+ static final String SETUP_TIME = "_setup_time";
// Default dismiss rule for suggestions.
private static final int DEFAULT_FIRST_APPEAR_DAY = 0;
@@ -51,30 +52,37 @@
public static boolean isEligible(Context context, String id, ResolveInfo info,
boolean ignoreAppearRule) {
- final SharedPreferences prefs = SuggestionService.getSharedPrefs(context);
- final SuggestionDismissHandler dismissHandler = SuggestionDismissHandler.getInstance();
+ final SuggestionFeatureProvider featureProvider = FeatureFactory.get(context)
+ .suggestionFeatureProvider();
+ final SharedPreferences prefs = featureProvider.getSharedPrefs(context);
+ final long currentTimeMs = System.currentTimeMillis();
final String keySetupTime = id + SETUP_TIME;
if (!prefs.contains(keySetupTime)) {
prefs.edit()
- .putLong(keySetupTime, System.currentTimeMillis())
+ .putLong(keySetupTime, currentTimeMs)
.apply();
}
// Check if it's already manually dismissed
- final boolean isDismissed = dismissHandler.isSuggestionDismissed(context, id);
+ final boolean isDismissed = featureProvider.isSuggestionDismissed(context, id);
if (isDismissed) {
return false;
}
- // Parse when suggestion should first appear. return true to artificially hide suggestion
- // before then.
+ // Parse when suggestion should first appear. Hide suggestion before then.
int firstAppearDay = ignoreAppearRule
? DEFAULT_FIRST_APPEAR_DAY
: parseAppearDay(info);
- long firstAppearDayInMs = getEndTime(prefs.getLong(keySetupTime, 0), firstAppearDay);
- if (System.currentTimeMillis() >= firstAppearDayInMs) {
+ long setupTime = prefs.getLong(keySetupTime, 0);
+ if (setupTime > currentTimeMs) {
+ // SetupTime is the future, user's date/time is probably wrong at some point.
+ // Force setupTime to be now. So we get a more reasonable firstAppearDay.
+ setupTime = currentTimeMs;
+ }
+ final long firstAppearDayInMs = getFirstAppearTimeMillis(setupTime, firstAppearDay);
+ if (currentTimeMs >= firstAppearDayInMs) {
// Dismiss timeout has passed, undismiss it.
- dismissHandler.markSuggestionNotDismissed(context, id);
+ featureProvider.markSuggestionNotDismissed(context, id);
return true;
}
return false;
@@ -104,8 +112,8 @@
}
}
- private static long getEndTime(long startTime, int daysDelay) {
+ private static long getFirstAppearTimeMillis(long setupTime, int daysDelay) {
long days = daysDelay * DateUtils.DAY_IN_MILLIS;
- return startTime + days;
+ return setupTime + days;
}
}
diff --git a/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java b/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java
index 99efa4f..93cf8aa 100644
--- a/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java
+++ b/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityChecker.java
@@ -48,8 +48,7 @@
}
private static boolean isEnabledInMetadata(Context context, String id, ResolveInfo info) {
- final int isSupportedResource =
- info.activityInfo.metaData.getInt(META_DATA_IS_SUPPORTED);
+ final int isSupportedResource = info.activityInfo.metaData.getInt(META_DATA_IS_SUPPORTED);
try {
final Resources res = context.getPackageManager()
.getResourcesForApplication(info.activityInfo.applicationInfo);
diff --git a/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java b/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java
index 09fd02d..a9ea657 100644
--- a/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java
+++ b/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestion.java
@@ -17,17 +17,19 @@
package com.android.settings.intelligence.suggestions.model;
import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
import android.os.Bundle;
import android.service.settings.suggestions.Suggestion;
+import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
-import android.widget.RemoteViews;
import com.android.settings.intelligence.suggestions.eligibility.AccountEligibilityChecker;
import com.android.settings.intelligence.suggestions.eligibility.ConnectivityEligibilityChecker;
@@ -35,6 +37,8 @@
import com.android.settings.intelligence.suggestions.eligibility.FeatureEligibilityChecker;
import com.android.settings.intelligence.suggestions.eligibility.ProviderEligibilityChecker;
+import java.util.List;
+
/**
* A wrapper to {@link android.content.pm.ResolveInfo} that matches Suggestion signature.
* <p/>
@@ -43,15 +47,20 @@
*/
public class CandidateSuggestion {
- public static final String META_DATA_PREFERENCE_ICON_TINTABLE =
- "com.android.settings.icon_tintable";
+ private static final String TAG = "CandidateSuggestion";
+ /**
+ * Name of the meta-data item that should be set in the AndroidManifest.xml
+ * to specify the title text that should be displayed for the preference.
+ */
+ @VisibleForTesting
public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the summary text that should be displayed for the preference.
*/
+ @VisibleForTesting
public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
/**
@@ -61,23 +70,28 @@
*
* Summary provided by the content provider overrides any static summary.
*/
+ @VisibleForTesting
public static final String META_DATA_PREFERENCE_SUMMARY_URI =
"com.android.settings.summary_uri";
- public static final String META_DATA_PREFERENCE_CUSTOM_VIEW =
- "com.android.settings.custom_view";
-
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the icon that should be displayed for the preference.
*/
+ @VisibleForTesting
public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";
- private static final String TAG = "CandidateSuggestion";
+ /**
+ * Hint for type of suggestion UI to be displayed.
+ */
+ @VisibleForTesting
+ public static final String META_DATA_PREFERENCE_CUSTOM_VIEW =
+ "com.android.settings.custom_view";
private final String mId;
private final Context mContext;
private final ResolveInfo mResolveInfo;
+ private final ComponentName mComponent;
private final Intent mIntent;
private final boolean mIsEligible;
private final boolean mIgnoreAppearRule;
@@ -87,8 +101,9 @@
mContext = context;
mIgnoreAppearRule = ignoreAppearRule;
mResolveInfo = resolveInfo;
- mIntent = new Intent()
- .setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
+ mIntent = new Intent().setClassName(
+ resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
+ mComponent = mIntent.getComponent();
mId = generateId();
mIsEligible = initIsEligible();
}
@@ -97,6 +112,10 @@
return mId;
}
+ public ComponentName getComponent() {
+ return mComponent;
+ }
+
/**
* Whether or not this candidate is eligible for display.
* <p/>
@@ -142,71 +161,121 @@
private void updateBuilder(Suggestion.Builder builder) {
final PackageManager pm = mContext.getPackageManager();
- final ApplicationInfo applicationInfo = mResolveInfo.activityInfo.applicationInfo;
+ final String packageName = mComponent.getPackageName();
- int icon = 0;
- boolean iconTintable = false;
- String title = null;
- String summary = null;
- RemoteViews remoteViews = null;
+ int iconRes = 0;
+ int flags = 0;
+ CharSequence title = null;
+ CharSequence summary = null;
+ Icon icon = null;
// Get the activity's meta-data
try {
- final Resources res = pm.getResourcesForApplication(applicationInfo.packageName);
+ final Resources res = pm.getResourcesForApplication(packageName);
final Bundle metaData = mResolveInfo.activityInfo.metaData;
if (res != null && metaData != null) {
+ // First get override data
+ final Bundle overrideData = getOverrideData(metaData);
+ // Get icon
if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) {
- icon = metaData.getInt(META_DATA_PREFERENCE_ICON);
+ iconRes = metaData.getInt(META_DATA_PREFERENCE_ICON);
+ } else {
+ iconRes = mResolveInfo.activityInfo.icon;
}
- if (metaData.containsKey(META_DATA_PREFERENCE_ICON_TINTABLE)) {
- iconTintable = metaData.getBoolean(META_DATA_PREFERENCE_ICON_TINTABLE);
+ if (iconRes != 0) {
+ icon = Icon.createWithResource(
+ mResolveInfo.activityInfo.packageName, iconRes);
}
- if (metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
+ // Get title
+ title = getStringFromBundle(overrideData, META_DATA_PREFERENCE_TITLE);
+ if (TextUtils.isEmpty(title) && metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
} else {
title = metaData.getString(META_DATA_PREFERENCE_TITLE);
}
}
- if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
+ // Get summary
+ summary = getStringFromBundle(overrideData, META_DATA_PREFERENCE_SUMMARY);
+ if (TextUtils.isEmpty(summary)
+ && metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
} else {
summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY);
}
}
- if (metaData.containsKey(META_DATA_PREFERENCE_CUSTOM_VIEW)) {
- int layoutId = metaData.getInt(META_DATA_PREFERENCE_CUSTOM_VIEW);
- remoteViews = new RemoteViews(applicationInfo.packageName, layoutId);
- }
+ // Detect remote view
+ flags = metaData.containsKey(META_DATA_PREFERENCE_CUSTOM_VIEW)
+ ? Suggestion.FLAG_HAS_BUTTON : 0;
}
} catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
- Log.d(TAG, "Couldn't find info", e);
+ Log.w(TAG, "Couldn't find info", e);
}
// Set the preference title to the activity's label if no
// meta-data is found
if (TextUtils.isEmpty(title)) {
- title = mResolveInfo.activityInfo.loadLabel(pm).toString();
+ title = mResolveInfo.activityInfo.loadLabel(pm);
}
-
- if (icon == 0) {
- icon = mResolveInfo.activityInfo.icon;
- }
- // TODO: Need to use ContentProvider to read dynamic title/summary etc.
- final PendingIntent pendingIntent = PendingIntent
- .getActivity(mContext, 0 /* requestCode */, mIntent, 0 /* flags */);
builder.setTitle(title)
.setSummary(summary)
- .setPendingIntent(pendingIntent);
- // TODO: Need to extend Suggestion and set the following.
- // set icon
- // set icon tintable
- // set remote view
+ .setFlags(flags)
+ .setIcon(icon)
+ .setPendingIntent(PendingIntent
+ .getActivity(mContext, 0 /* requestCode */, mIntent, 0 /* flags */));
+ }
+
+ /**
+ * Extracts a string from bundle.
+ */
+ private CharSequence getStringFromBundle(Bundle bundle, String key) {
+ if (bundle == null || TextUtils.isEmpty(key)) {
+ return null;
+ }
+ return bundle.getString(key);
+ }
+
+ private Bundle getOverrideData(Bundle metadata) {
+ if (metadata == null || !metadata.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
+ Log.d(TAG, "Metadata null or has no info about summary_uri");
+ return null;
+ }
+
+ final String uriString = metadata.getString(META_DATA_PREFERENCE_SUMMARY_URI);
+ final Bundle bundle = getBundleFromUri(uriString);
+ return bundle;
+ }
+
+ /**
+ * Calls method through ContentProvider and expects a bundle in return.
+ */
+ private Bundle getBundleFromUri(String uriString) {
+ final Uri uri = Uri.parse(uriString);
+
+ final String method = getMethodFromUri(uri);
+ if (TextUtils.isEmpty(method)) {
+ return null;
+ }
+ return mContext.getContentResolver().call(uri, method, null /* args */, null /* bundle */);
+ }
+
+ /**
+ * Returns the first path segment of the uri if it exists as the method, otherwise null.
+ */
+ private String getMethodFromUri(Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+ final List<String> pathSegments = uri.getPathSegments();
+ if ((pathSegments == null) || pathSegments.isEmpty()) {
+ return null;
+ }
+ return pathSegments.get(0);
}
private String generateId() {
- return mIntent.getComponent().flattenToString();
+ return mComponent.flattenToString();
}
}
diff --git a/src/com/android/settings/intelligence/suggestions/ranking/EventStore.java b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionEventStore.java
similarity index 64%
rename from src/com/android/settings/intelligence/suggestions/ranking/EventStore.java
rename to src/com/android/settings/intelligence/suggestions/ranking/SuggestionEventStore.java
index f79c513..1f5f7c0 100644
--- a/src/com/android/settings/intelligence/suggestions/ranking/EventStore.java
+++ b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionEventStore.java
@@ -16,8 +16,11 @@
package com.android.settings.intelligence.suggestions.ranking;
+import static android.support.annotation.VisibleForTesting.NONE;
+
import android.content.Context;
import android.content.SharedPreferences;
+import android.support.annotation.VisibleForTesting;
import android.util.Log;
import java.util.Arrays;
@@ -27,7 +30,7 @@
/**
* Copied from packages/apps/Settings/src/.../dashboard/suggestions/EventStore
*/
-public class EventStore {
+public class SuggestionEventStore {
public static final String TAG = "SuggestionEventStore";
public static final String EVENT_SHOWN = "shown";
@@ -36,30 +39,39 @@
public static final String METRIC_LAST_EVENT_TIME = "last_event_time";
public static final String METRIC_COUNT = "count";
- private static final Set<String> EVENTS = new HashSet<String>(
+ private static SuggestionEventStore sEventStore;
+
+ private static final Set<String> EVENTS = new HashSet<>(
Arrays.asList(new String[]{EVENT_SHOWN, EVENT_DISMISSED, EVENT_CLICKED}));
- private static final Set<String> METRICS = new HashSet<String>(
+ private static final Set<String> METRICS = new HashSet<>(
Arrays.asList(new String[]{METRIC_LAST_EVENT_TIME, METRIC_COUNT}));
private final SharedPreferences mSharedPrefs;
- public EventStore(Context context) {
+ public static SuggestionEventStore get(Context context) {
+ if (sEventStore == null) {
+ sEventStore = new SuggestionEventStore(context);
+ }
+ return sEventStore;
+ }
+
+ private SuggestionEventStore(Context context) {
mSharedPrefs = context.getSharedPreferences(TAG, Context.MODE_PRIVATE);
}
/**
* Writes individual log events.
*
- * @param pkgName: Package for which this event is reported.
- * @param eventType: Type of event (one of {@link #EVENTS}).
+ * @param suggestionId Package for which this event is reported.
+ * @param eventType Type of event (one of {@link #EVENTS}).
*/
- public void writeEvent(String pkgName, String eventType) {
+ public void writeEvent(String suggestionId, String eventType) {
if (!EVENTS.contains(eventType)) {
Log.w(TAG, "Reported event type " + eventType + " is not a valid type!");
return;
}
- final String lastTimePrefKey = getPrefKey(pkgName, eventType, METRIC_LAST_EVENT_TIME);
- final String countPrefKey = getPrefKey(pkgName, eventType, METRIC_COUNT);
+ final String lastTimePrefKey = getPrefKey(suggestionId, eventType, METRIC_LAST_EVENT_TIME);
+ final String countPrefKey = getPrefKey(suggestionId, eventType, METRIC_COUNT);
writePref(lastTimePrefKey, System.currentTimeMillis());
writePref(countPrefKey, readPref(countPrefKey, (long) 0) + 1);
}
@@ -67,12 +79,12 @@
/**
* Reads metric of the the reported events (e.g., counts).
*
- * @param pkgName: Package for which this metric is queried.
- * @param eventType: Type of event (one of {@link #EVENTS}).
- * @param metricType: Type of the queried metric (one of {@link #METRICS}).
+ * @param suggestionId Suggestion Id for which this metric is queried.
+ * @param eventType Type of event (one of {@link #EVENTS}).
+ * @param metricType Type of the queried metric (one of {@link #METRICS}).
* @return the corresponding metric.
*/
- public long readMetric(String pkgName, String eventType, String metricType) {
+ public long readMetric(String suggestionId, String eventType, String metricType) {
if (!EVENTS.contains(eventType)) {
Log.w(TAG, "Reported event type " + eventType + " is not a valid event!");
return 0;
@@ -80,7 +92,12 @@
Log.w(TAG, "Required stat type + " + metricType + " is not a valid stat!");
return 0;
}
- return readPref(getPrefKey(pkgName, eventType, metricType), (long) 0);
+ return readPref(getPrefKey(suggestionId, eventType, metricType), (long) 0);
+ }
+
+ @VisibleForTesting(otherwise = NONE)
+ public void clear() {
+ mSharedPrefs.edit().clear().apply();
}
private void writePref(String prefKey, long value) {
@@ -91,10 +108,10 @@
return mSharedPrefs.getLong(prefKey, defaultValue);
}
- private String getPrefKey(String pkgName, String eventType, String statType) {
+ private String getPrefKey(String suggestionId, String eventType, String statType) {
return new StringBuilder()
.append("setting_suggestion_")
- .append(pkgName)
+ .append(suggestionId)
.append("_")
.append(eventType)
.append("_")
diff --git a/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizer.java b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizer.java
index 9844268..07f1a88 100644
--- a/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizer.java
+++ b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizer.java
@@ -16,6 +16,7 @@
package com.android.settings.intelligence.suggestions.ranking;
+import android.content.Context;
import android.service.settings.suggestions.Suggestion;
import java.util.ArrayList;
@@ -47,16 +48,10 @@
public static final double TIME_NORMALIZATION_FACTOR = 2e10;
public static final double COUNT_NORMALIZATION_FACTOR = 500;
- private final EventStore mEventStore;
+ private final SuggestionEventStore mEventStore;
- /**
- * Constructor
- *
- * @param eventStore An instance of {@code EventStore} which maintains the recorded suggestion
- * events.
- */
- public SuggestionFeaturizer(EventStore eventStore) {
- mEventStore = eventStore;
+ public SuggestionFeaturizer(Context context) {
+ mEventStore = SuggestionEventStore.get(context);
}
/**
@@ -68,19 +63,22 @@
public Map<String, Map<String, Double>> featurize(List<Suggestion> suggestions) {
Map<String, Map<String, Double>> features = new HashMap<>();
Long curTimeMs = System.currentTimeMillis();
- List<String> pkgNames = new ArrayList<>(suggestions.size());
+ final List<String> suggestionIds = new ArrayList<>(suggestions.size());
for (Suggestion suggestion : suggestions) {
- pkgNames.add(suggestion.getId());
+ suggestionIds.add(suggestion.getId());
}
- for (String pkgName : pkgNames) {
+ for (String id : suggestionIds) {
Map<String, Double> featureMap = new HashMap<>();
- features.put(pkgName, featureMap);
- Long lastShownTime = mEventStore
- .readMetric(pkgName, EventStore.EVENT_SHOWN, EventStore.METRIC_LAST_EVENT_TIME);
- Long lastDismissedTime = mEventStore.readMetric(pkgName, EventStore.EVENT_DISMISSED,
- EventStore.METRIC_LAST_EVENT_TIME);
- Long lastClickedTime = mEventStore.readMetric(pkgName, EventStore.EVENT_CLICKED,
- EventStore.METRIC_LAST_EVENT_TIME);
+ features.put(id, featureMap);
+ Long lastShownTime = mEventStore.readMetric(id,
+ SuggestionEventStore.EVENT_SHOWN,
+ SuggestionEventStore.METRIC_LAST_EVENT_TIME);
+ Long lastDismissedTime = mEventStore.readMetric(id,
+ SuggestionEventStore.EVENT_DISMISSED,
+ SuggestionEventStore.METRIC_LAST_EVENT_TIME);
+ Long lastClickedTime = mEventStore.readMetric(id,
+ SuggestionEventStore.EVENT_CLICKED,
+ SuggestionEventStore.METRIC_LAST_EVENT_TIME);
featureMap.put(FEATURE_IS_SHOWN, booleanToDouble(lastShownTime > 0));
featureMap.put(FEATURE_IS_DISMISSED, booleanToDouble(lastDismissedTime > 0));
featureMap.put(FEATURE_IS_CLICKED, booleanToDouble(lastClickedTime > 0));
@@ -90,12 +88,12 @@
normalizedTimeDiff(curTimeMs, lastDismissedTime));
featureMap.put(FEATURE_TIME_FROM_LAST_CLICKED,
normalizedTimeDiff(curTimeMs, lastClickedTime));
- featureMap.put(FEATURE_SHOWN_COUNT, normalizedCount(mEventStore
- .readMetric(pkgName, EventStore.EVENT_SHOWN, EventStore.METRIC_COUNT)));
- featureMap.put(FEATURE_DISMISSED_COUNT, normalizedCount(mEventStore
- .readMetric(pkgName, EventStore.EVENT_DISMISSED, EventStore.METRIC_COUNT)));
- featureMap.put(FEATURE_CLICKED_COUNT, normalizedCount(mEventStore
- .readMetric(pkgName, EventStore.EVENT_CLICKED, EventStore.METRIC_COUNT)));
+ featureMap.put(FEATURE_SHOWN_COUNT, normalizedCount(mEventStore.readMetric(id,
+ SuggestionEventStore.EVENT_SHOWN, SuggestionEventStore.METRIC_COUNT)));
+ featureMap.put(FEATURE_DISMISSED_COUNT, normalizedCount(mEventStore.readMetric(id,
+ SuggestionEventStore.EVENT_DISMISSED, SuggestionEventStore.METRIC_COUNT)));
+ featureMap.put(FEATURE_CLICKED_COUNT, normalizedCount(mEventStore.readMetric(id,
+ SuggestionEventStore.EVENT_CLICKED, SuggestionEventStore.METRIC_COUNT)));
}
return features;
}
diff --git a/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRanker.java b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRanker.java
index 887f922..e62262e 100644
--- a/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRanker.java
+++ b/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRanker.java
@@ -20,6 +20,9 @@
import android.service.settings.suggestions.Suggestion;
import android.support.annotation.VisibleForTesting;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+
+import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -30,6 +33,7 @@
* Copied from packages/apps/Settings/src/.../dashboard/suggestions/SuggestionRanker
*/
public class SuggestionRanker {
+
private static final String TAG = "SuggestionRanker";
// The following coefficients form a linear model, which mixes the features to obtain a
@@ -46,39 +50,48 @@
put(SuggestionFeaturizer.FEATURE_SHOWN_COUNT, -2.35993512546);
}};
- private static SuggestionRanker sInstance;
-
+ private final long mMaxSuggestionsDisplayCount;
private final SuggestionFeaturizer mSuggestionFeaturizer;
- private final Map<Suggestion, Double> relevanceMetrics;
+ private final Map<Suggestion, Double> mRelevanceMetrics;
Comparator<Suggestion> suggestionComparator = new Comparator<Suggestion>() {
@Override
public int compare(Suggestion suggestion1, Suggestion suggestion2) {
- return relevanceMetrics.get(suggestion1) < relevanceMetrics.get(suggestion2) ? 1 : -1;
+ return mRelevanceMetrics.get(suggestion1) < mRelevanceMetrics.get(suggestion2) ? 1 : -1;
}
};
- @VisibleForTesting
- SuggestionRanker(SuggestionFeaturizer suggestionFeaturizer) {
+ public SuggestionRanker(Context context, SuggestionFeaturizer suggestionFeaturizer) {
mSuggestionFeaturizer = suggestionFeaturizer;
- relevanceMetrics = new HashMap<>();
+ mRelevanceMetrics = new HashMap<>();
+ mMaxSuggestionsDisplayCount = FeatureFactory.get(context).experimentFeatureProvider()
+ .getMaxSuggestionDisplayCount(context);
}
- public static SuggestionRanker getInstance(Context context) {
- if (sInstance == null) {
- sInstance = new SuggestionRanker(
- new SuggestionFeaturizer(new EventStore(context.getApplicationContext())));
- }
- return sInstance;
- }
-
- public void rankSuggestions(List<Suggestion> suggestions) {
- relevanceMetrics.clear();
+ /**
+ * Filter out suggestions that are not relevant at the moment, and rank the rest.
+ *
+ * @return a list of suggestion ranked by relevance.
+ */
+ public List<Suggestion> rankRelevantSuggestions(List<Suggestion> suggestions) {
+ mRelevanceMetrics.clear();
Map<String, Map<String, Double>> features = mSuggestionFeaturizer.featurize(suggestions);
for (Suggestion suggestion : suggestions) {
- relevanceMetrics.put(suggestion, getRelevanceMetric(features.get(suggestion.getId())));
+ mRelevanceMetrics.put(suggestion, getRelevanceMetric(features.get(suggestion.getId())));
}
- Collections.sort(suggestions, suggestionComparator);
+ final List<Suggestion> rankedSuggestions = new ArrayList<>();
+ rankedSuggestions.addAll(suggestions);
+ Collections.sort(rankedSuggestions, suggestionComparator);
+
+ if (rankedSuggestions.size() < mMaxSuggestionsDisplayCount) {
+ return rankedSuggestions;
+ } else {
+ final List<Suggestion> relevantSuggestions = new ArrayList<>();
+ for (int i = 0; i < mMaxSuggestionsDisplayCount; i++) {
+ relevantSuggestions.add(rankedSuggestions.get(i));
+ }
+ return relevantSuggestions;
+ }
}
@VisibleForTesting
diff --git a/src/com/android/settings/intelligence/utils/AsyncLoader.java b/src/com/android/settings/intelligence/utils/AsyncLoader.java
new file mode 100644
index 0000000..54b62b6
--- /dev/null
+++ b/src/com/android/settings/intelligence/utils/AsyncLoader.java
@@ -0,0 +1,92 @@
+package com.android.settings.intelligence.utils;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+
+/**
+ * This class fills in some boilerplate for AsyncTaskLoader to actually load things.
+ *
+ * Subclasses need to implement {@link AsyncLoader#loadInBackground()} to perform the actual
+ * background task, and {@link AsyncLoader#onDiscardResult(T)} to clean up previously loaded
+ * results.
+ *
+ * This loader is based on the MailAsyncTaskLoader from the AOSP EmailUnified repo.
+ */
+public abstract class AsyncLoader<T> extends AsyncTaskLoader<T> {
+ private T mResult;
+
+ public AsyncLoader(final Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mResult != null) {
+ deliverResult(mResult);
+ }
+
+ if (takeContentChanged() || mResult == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ public void deliverResult(final T data) {
+ if (isReset()) {
+ if (data != null) {
+ onDiscardResult(data);
+ }
+ return;
+ }
+
+ final T oldResult = mResult;
+ mResult = data;
+
+ if (isStarted()) {
+ super.deliverResult(data);
+ }
+
+ if (oldResult != null && oldResult != mResult) {
+ onDiscardResult(oldResult);
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ onStopLoading();
+
+ if (mResult != null) {
+ onDiscardResult(mResult);
+ }
+ mResult = null;
+ }
+
+ @Override
+ public void onCanceled(final T data) {
+ super.onCanceled(data);
+
+ if (data != null) {
+ onDiscardResult(data);
+ }
+ }
+
+ /**
+ * Called when discarding the load results so subclasses can take care of clean-up or
+ * recycling tasks. This is not called if the same result (by way of pointer equality) is
+ * returned again by a subsequent call to loadInBackground, or if result is null.
+ *
+ * Note that this may be called concurrently with loadInBackground(), and in some circumstances
+ * may be called more than once for a given object.
+ *
+ * @param result The value returned from {@link AsyncLoader#loadInBackground()} which
+ * is to be discarded.
+ */
+ protected abstract void onDiscardResult(final T result);
+}
\ No newline at end of file
diff --git a/tests/Android.mk b/tests/Android.mk
deleted file mode 100644
index fd297e3..0000000
--- a/tests/Android.mk
+++ /dev/null
@@ -1,5 +0,0 @@
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all makefiles in subdirectories
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/robotests/Android.mk b/tests/robotests/Android.mk
deleted file mode 100644
index c8ec19f..0000000
--- a/tests/robotests/Android.mk
+++ /dev/null
@@ -1,40 +0,0 @@
-#############################################
-# Turbo Robolectric test target. #
-#############################################
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-# Include the testing libraries (JUnit4 + Robolectric libs).
-LOCAL_STATIC_JAVA_LIBRARIES := \
- mockito-robolectric-prebuilt \
- truth-prebuilt
-
-LOCAL_JAVA_LIBRARIES := \
- junit \
- platform-robolectric-3.4.2-prebuilt \
- sdk_vcurrent
-
-LOCAL_INSTRUMENTATION_FOR := SettingsIntelligence
-LOCAL_MODULE := SettingsIntelligenceRoboTests
-
-LOCAL_MODULE_TAGS := optional
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#############################################################
-# Turbo runner target to run the previous target. #
-#############################################################
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := RunSettingsIntelligenceRoboTests
-
-LOCAL_SDK_VERSION := system_current
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- SettingsIntelligenceRoboTests
-
-LOCAL_TEST_PACKAGE := SettingsIntelligence
-
-include prebuilts/misc/common/robolectric/3.4.2/run_robotests.mk
diff --git a/tests/robotests/src/android/service/settings/suggestions/Suggestion.java b/tests/robotests/src/android/service/settings/suggestions/Suggestion.java
deleted file mode 100644
index f670f89..0000000
--- a/tests/robotests/src/android/service/settings/suggestions/Suggestion.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2017 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 android.service.settings.suggestions;
-
-import android.app.PendingIntent;
-import android.os.Parcel;
-import android.text.TextUtils;
-
-public class Suggestion {
- private final String mId;
- private final CharSequence mTitle;
- private final CharSequence mSummary;
- private final PendingIntent mPendingIntent;
-
- /**
- * Gets the id for the suggestion object.
- */
- public String getId() {
- return mId;
- }
-
- /**
- * Title of the suggestion that is shown to the user.
- */
- public CharSequence getTitle() {
- return mTitle;
- }
-
- /**
- * Optional summary describing what this suggestion controls.
- */
- public CharSequence getSummary() {
- return mSummary;
- }
-
- /**
- * The Intent to launch when the suggestion is activated.
- */
- public PendingIntent getPendingIntent() {
- return mPendingIntent;
- }
-
- private Suggestion(Builder builder) {
- mTitle = builder.mTitle;
- mSummary = builder.mSummary;
- mPendingIntent = builder.mPendingIntent;
- mId = builder.mId;
- }
-
- /**
- * Builder class for {@link Suggestion}.
- */
- public static class Builder {
- private final String mId;
- private CharSequence mTitle;
- private CharSequence mSummary;
- private PendingIntent mPendingIntent;
-
- public Builder(String id) {
- if (TextUtils.isEmpty(id)) {
- throw new IllegalArgumentException("Suggestion id cannot be empty");
- }
- mId = id;
- }
-
- /**
- * Sets suggestion title
- */
-
- public Builder setTitle(CharSequence title) {
- mTitle = title;
- return this;
- }
-
- /**
- * Sets suggestion summary
- */
- public Builder setSummary(CharSequence summary) {
- mSummary = summary;
- return this;
- }
-
- /**
- * Sets suggestion intent
- */
- public Builder setPendingIntent(PendingIntent pendingIntent) {
- mPendingIntent = pendingIntent;
- return this;
- }
-
- /**
- * Builds an immutable {@link Suggestion} object.
- */
- public Suggestion build() {
- return new Suggestion(this /* builder */);
- }
- }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionParserTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionParserTest.java
deleted file mode 100644
index c993b6b..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionParserTest.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions;
-
-import static com.android.settings.intelligence.suggestions.model.CandidateSuggestionTest.newInfo;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-import com.android.settings.intelligence.suggestions.model.SuggestionCategoryRegistry;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowPackageManager;
-
-import java.util.List;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionParserTest {
-
- private Context mContext;
- private ShadowPackageManager mPackageManager;
- private SuggestionParser mSuggestionParser;
- private ResolveInfo mInfo1;
- private ResolveInfo mInfo1Dupe;
- private ResolveInfo mInfo2;
- private ResolveInfo mInfo3;
- private ResolveInfo mInfo4;
-
- private Intent exclusiveIntent1;
- private Intent exclusiveIntent2;
- private Intent regularIntent;
-
- @Before
- public void setUp() {
- mContext = RuntimeEnvironment.application;
- mPackageManager = Shadows.shadowOf(mContext.getPackageManager());
- mSuggestionParser = new SuggestionParser(mContext);
-
- mInfo1 = newInfo(mContext, "Class1", true /* systemApp */,
- null /* summaryUri */, "title4", 0 /* titleResId */);
- mInfo1Dupe = newInfo(mContext, "Class1", true /* systemApp */,
- null /* summaryUri */, "title4", 0 /* titleResId */);
- mInfo2 = newInfo(mContext, "Class2", true /* systemApp */,
- null /* summaryUri */, "title4", 0 /* titleResId */);
- mInfo3 = newInfo(mContext, "Class3", true /* systemApp */,
- null /* summaryUri */, "title4", 0 /* titleResId */);
- mInfo4 = newInfo(mContext, "Class4", true /* systemApp */,
- null /* summaryUri */, "title4", 0 /* titleResId */);
- mInfo4.activityInfo.applicationInfo.packageName = "ineligible";
-
- exclusiveIntent1 = new Intent(Intent.ACTION_MAIN).addCategory(
- SuggestionCategoryRegistry.CATEGORIES.get(0).getCategory());
- exclusiveIntent2 = new Intent(Intent.ACTION_MAIN).addCategory(
- SuggestionCategoryRegistry.CATEGORIES.get(1).getCategory());
- regularIntent = new Intent(Intent.ACTION_MAIN).addCategory(
- SuggestionCategoryRegistry.CATEGORIES.get(2).getCategory());
- }
-
- @Test
- public void testGetSuggestions_exclusive() {
- mPackageManager.addResolveInfoForIntent(exclusiveIntent1, mInfo1);
- mPackageManager.addResolveInfoForIntent(exclusiveIntent1, mInfo1Dupe);
- mPackageManager.addResolveInfoForIntent(exclusiveIntent2, mInfo2);
- mPackageManager.addResolveInfoForIntent(regularIntent, mInfo3);
- final List<Suggestion> suggestions = mSuggestionParser.getSuggestions();
-
- // info1
- assertThat(suggestions).hasSize(1);
- }
-
- @Test
- public void testGetSuggestion_onlyRegularCategoryAndNoDupe() {
- mPackageManager.addResolveInfoForIntent(regularIntent, mInfo1);
- mPackageManager.addResolveInfoForIntent(regularIntent, mInfo1Dupe);
- mPackageManager.addResolveInfoForIntent(regularIntent, mInfo2);
- mPackageManager.addResolveInfoForIntent(regularIntent, mInfo3);
- mPackageManager.addResolveInfoForIntent(regularIntent, mInfo4);
-
- final List<Suggestion> suggestions = mSuggestionParser.getSuggestions();
-
- // info1, info2, info3 (info4 is skip because its package name is ineligible)
- assertThat(suggestions).hasSize(3);
- }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionServiceTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionServiceTest.java
deleted file mode 100644
index 5ed7665..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/SuggestionServiceTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.Robolectric;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.android.controller.ServiceController;
-import org.robolectric.annotation.Config;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionServiceTest {
-
- private SuggestionService mService;
- private ServiceController<SuggestionService> mServiceController;
-
- @Before
- public void setUp() {
- mServiceController = Robolectric.buildService(SuggestionService.class);
- mService = mServiceController.create().get();
- }
-
- @Test
- public void getSuggestion_shouldReturnNonNull() {
- assertThat(mService.onGetSuggestions()).isNotNull();
- }
-
- @Test
- public void dismissSuggestion_shouldDismiss() {
- final String id = "id1";
- final Suggestion suggestion = new Suggestion.Builder(id).build();
-
- // Not dismissed
- assertThat(SuggestionDismissHandler.getInstance().isSuggestionDismissed(mService, id))
- .isFalse();
-
- // Dismiss
- mService.onSuggestionDismissed(suggestion);
-
- // Dismissed
- assertThat(SuggestionDismissHandler.getInstance().isSuggestionDismissed(mService, id))
- .isTrue();
- }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityCheckerTest.java
deleted file mode 100644
index cfec40a..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/AccountEligibilityCheckerTest.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions.eligibility;
-
-import static com.android.settings.intelligence.suggestions.eligibility.AccountEligibilityChecker
- .META_DATA_REQUIRE_ACCOUNT;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowAccountManager;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class AccountEligibilityCheckerTest {
-
- private static final String ID = "test";
- private Context mContext;
- private ResolveInfo mInfo;
-
- @Before
- public void setUp() {
- mContext = RuntimeEnvironment.application;
- mInfo = new ResolveInfo();
- mInfo.activityInfo = new ActivityInfo();
- mInfo.activityInfo.metaData = new Bundle();
- mInfo.activityInfo.applicationInfo = new ApplicationInfo();
- mInfo.activityInfo.applicationInfo.packageName =
- RuntimeEnvironment.application.getPackageName();
- }
-
- @After
- public void tearDown() {
- final ShadowAccountManager shadowAccountManager = Shadows.shadowOf(
- AccountManager.get(mContext));
- shadowAccountManager.removeAllAccounts();
- }
-
- @Test
- public void isEligible_noAccountRequirement_shouldReturnTrue() {
- assertThat(AccountEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isTrue();
- }
-
- @Test
- public void isEligible_failRequirement_shouldReturnFalse() {
- // Require android.com account but AccountManager doesn't have it.
- mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_ACCOUNT, "android.com");
-
- assertThat(AccountEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isFalse();
- }
-
- @Test
- public void isEligible_passRequirement_shouldReturnTrue() {
- mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_ACCOUNT, "android.com");
- final Account account = new Account("TEST", "android.com");
-
- final ShadowAccountManager shadowAccountManager = Shadows.shadowOf(
- AccountManager.get(mContext));
- shadowAccountManager.addAccount(account);
-
- assertThat(AccountEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isTrue();
- }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityCheckerTest.java
deleted file mode 100644
index 42949d3..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ConnectivityEligibilityCheckerTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions.eligibility;
-
-import static com.android.settings.intelligence.suggestions.eligibility
- .ConnectivityEligibilityChecker.META_DATA_IS_CONNECTION_REQUIRED;
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.Bundle;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowConnectivityManager;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class ConnectivityEligibilityCheckerTest {
-
- private static final String ID = "test";
- private Context mContext;
- private ResolveInfo mInfo;
- private ShadowConnectivityManager mConnectivityManager;
-
- @Before
- public void setUp() {
- mContext = RuntimeEnvironment.application;
- mInfo = new ResolveInfo();
- mInfo.activityInfo = new ActivityInfo();
- mInfo.activityInfo.metaData = new Bundle();
- mInfo.activityInfo.applicationInfo = new ApplicationInfo();
- mInfo.activityInfo.applicationInfo.packageName =
- RuntimeEnvironment.application.getPackageName();
-
- mConnectivityManager = Shadows.shadowOf(
- (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE));
- }
-
- @After
- public void tearDown() {
- mConnectivityManager.setActiveNetworkInfo(null);
- }
-
- @Test
- public void isEligible_noRequirement_shouldReturnTrue() {
- assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isTrue();
-
- mInfo.activityInfo.metaData.putBoolean(META_DATA_IS_CONNECTION_REQUIRED, false);
- assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isTrue();
- }
-
- @Test
- public void isEligible_hasConnection_shouldReturnTrue() {
- mInfo.activityInfo.metaData.putBoolean(META_DATA_IS_CONNECTION_REQUIRED, true);
-
- final NetworkInfo networkInfo = mock(NetworkInfo.class);
- when(networkInfo.isConnectedOrConnecting())
- .thenReturn(true);
-
- mConnectivityManager.setActiveNetworkInfo(networkInfo);
-
- assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isTrue();
- }
-
- @Test
- public void isEligible_noConnection_shouldReturnFalse() {
- mInfo.activityInfo.metaData.putBoolean(META_DATA_IS_CONNECTION_REQUIRED, true);
-
- mConnectivityManager.setActiveNetworkInfo(null);
-
- assertThat(ConnectivityEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isFalse();
- }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/DismissedCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/DismissedCheckerTest.java
deleted file mode 100644
index f172553..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/DismissedCheckerTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions.eligibility;
-
-import static com.android.settings.intelligence.suggestions.eligibility.DismissedChecker
- .META_DATA_DISMISS_CONTROL;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-
-import com.android.settings.intelligence.TestConfig;
-import com.android.settings.intelligence.suggestions.SuggestionDismissHandler;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class DismissedCheckerTest {
- private static final String ID = "test";
- private Context mContext;
- private ResolveInfo mInfo;
-
- @Before
- public void setUp() {
- mContext = RuntimeEnvironment.application;
- mInfo = new ResolveInfo();
- mInfo.activityInfo = new ActivityInfo();
- mInfo.activityInfo.metaData = new Bundle();
- mInfo.activityInfo.applicationInfo = new ApplicationInfo();
- mInfo.activityInfo.applicationInfo.packageName =
- RuntimeEnvironment.application.getPackageName();
- }
-
- @Test
- public void isEligible_newSuggestion_noRule_shouldReturnTrue() {
- assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
- .isTrue();
- }
-
- @Test
- public void isEligible_newSuggestion_hasFutureRule_shouldReturnFalse() {
- mInfo.activityInfo.metaData.putString(META_DATA_DISMISS_CONTROL, "10");
-
- assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, false /* ignoreAppearRule */))
- .isFalse();
- }
-
- @Test
- public void isEligible_newSuggestion_ignoreFutureRule_shouldReturnFalse() {
- mInfo.activityInfo.metaData.putString(META_DATA_DISMISS_CONTROL, "10");
-
- assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
- .isTrue();
- }
-
- @Test
- public void isEligible_newSuggestion_hasPastRule_shouldReturnTrue() {
- mInfo.activityInfo.metaData.putString(META_DATA_DISMISS_CONTROL, "-10");
-
- assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, false /* ignoreAppearRule */))
- .isTrue();
- assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
- .isTrue();
- }
-
- @Test
- public void isEligible_dismissedSuggestion_shouldReturnFalse() {
- SuggestionDismissHandler.getInstance().markSuggestionDismissed(mContext, ID);
-
- assertThat(DismissedChecker.isEligible(mContext, ID, mInfo, true /* ignoreAppearRule */))
- .isFalse();
- }
-
-}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityCheckerTest.java
deleted file mode 100644
index 6c92e92..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/FeatureEligibilityCheckerTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions.eligibility;
-
-
-import static com.android.settings.intelligence.suggestions.eligibility.FeatureEligibilityChecker
- .META_DATA_REQUIRE_FEATURE;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.FeatureInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowPackageManager;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class FeatureEligibilityCheckerTest {
-
- private static final String ID = "test";
- private Context mContext;
- private ResolveInfo mInfo;
- private ShadowPackageManager mPackageManager;
-
- @Before
- public void setUp() {
- mContext = RuntimeEnvironment.application;
- mInfo = new ResolveInfo();
- mInfo.activityInfo = new ActivityInfo();
- mInfo.activityInfo.metaData = new Bundle();
- mInfo.activityInfo.applicationInfo = new ApplicationInfo();
- mInfo.activityInfo.applicationInfo.packageName =
- RuntimeEnvironment.application.getPackageName();
- mPackageManager = Shadows.shadowOf(mContext.getPackageManager());
- }
-
- @After
- public void tearDown() {
- mPackageManager.clearSystemAvailableFeatures();
- }
-
- @Test
- public void isEligible_noRequirement_shouldReturnTrue() {
- assertThat(FeatureEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isTrue();
- }
-
- @Test
- public void isEligible_failRequirement_shouldReturnFalse() {
- mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_FEATURE, "test_feature");
-
- assertThat(FeatureEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isFalse();
- }
-
- @Test
- public void isEligible_passRequirement_shouldReturnTrue() {
- final FeatureInfo featureInfo = new FeatureInfo();
- featureInfo.name = "fingerprint";
- mInfo.activityInfo.metaData.putString(META_DATA_REQUIRE_FEATURE, featureInfo.name);
- mPackageManager.addSystemAvailableFeature(featureInfo);
-
- assertThat(FeatureEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isFalse();
- }
-}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityCheckerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityCheckerTest.java
deleted file mode 100644
index b4c0efd..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/eligibility/ProviderEligibilityCheckerTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions.eligibility;
-
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class ProviderEligibilityCheckerTest {
-
- private static final String ID = "test";
- private Context mContext;
- private ResolveInfo mInfo;
-
- @Before
- public void setUp() {
- mContext = RuntimeEnvironment.application;
- mInfo = new ResolveInfo();
- mInfo.activityInfo = new ActivityInfo();
- mInfo.activityInfo.metaData = new Bundle();
- mInfo.activityInfo.applicationInfo = new ApplicationInfo();
- mInfo.activityInfo.applicationInfo.packageName =
- RuntimeEnvironment.application.getPackageName();
- }
-
- @Test
- public void isEligible_systemFlagSet_shouldReturnTrue() {
- mInfo.activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
-
- assertThat(ProviderEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isTrue();
- }
-
- @Test
- public void isEligible_systemFlagNotSet_shouldReturnFalse() {
- mInfo.activityInfo.applicationInfo.flags &= ~ApplicationInfo.FLAG_SYSTEM;
-
- assertThat(ProviderEligibilityChecker.isEligible(mContext, ID, mInfo))
- .isFalse();
- }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestionTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestionTest.java
deleted file mode 100644
index 87c4440..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/CandidateSuggestionTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions.model;
-
-import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
- .META_DATA_PREFERENCE_ICON;
-import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
- .META_DATA_PREFERENCE_SUMMARY;
-import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
- .META_DATA_PREFERENCE_SUMMARY_URI;
-import static com.android.settings.intelligence.suggestions.model.CandidateSuggestion
- .META_DATA_PREFERENCE_TITLE;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class CandidateSuggestionTest {
-
- private static final String PACKAGE_NAME = "pkg";
- private static final String CLASS_NAME = "class";
-
- private Context mContext;
- private ResolveInfo mInfo;
-
- @Before
- public void setUp() {
- mContext = RuntimeEnvironment.application;
- mInfo = new ResolveInfo();
- mInfo.activityInfo = new ActivityInfo();
- mInfo.activityInfo.metaData = new Bundle();
- mInfo.activityInfo.packageName = PACKAGE_NAME;
- mInfo.activityInfo.name = CLASS_NAME;
-
- mInfo.activityInfo.applicationInfo = new ApplicationInfo();
- mInfo.activityInfo.applicationInfo.packageName =
- RuntimeEnvironment.application.getPackageName();
- }
-
- @Test
- public void getId_shouldUseComponentName() {
- final CandidateSuggestion candidate =
- new CandidateSuggestion(mContext, mInfo, true /* ignoreAppearRule */);
-
- assertThat(candidate.getId())
- .contains(PACKAGE_NAME + "/" + CLASS_NAME);
- }
-
- @Test
- public void parseMetadata_eligibleSuggestion() {
- final ResolveInfo info = newInfo(mContext, "class", true /* systemApp */,
- null /*summaryUri */, "title", 0 /* titleResId */);
- Suggestion suggestion = new CandidateSuggestion(
- mContext, info, false /* ignoreAppearRule*/)
- .toSuggestion();
- assertThat(suggestion.getId()).isEqualTo(mContext.getPackageName() + "/class");
- assertThat(suggestion.getTitle()).isEqualTo("title");
- assertThat(suggestion.getSummary()).isEqualTo("static-summary");
- }
-
- @Test
- public void parseMetadata_ineligibleSuggestion() {
- final ResolveInfo info = newInfo(mContext, "class", false /* systemApp */,
- null /*summaryUri */, "title", 0 /* titleResId */);
- final CandidateSuggestion candidate = new CandidateSuggestion(
- mContext, info, false /* ignoreAppearRule*/);
-
- assertThat(candidate.isEligible()).isFalse();
- assertThat(candidate.toSuggestion()).isNull();
- }
-
- public static ResolveInfo newInfo(Context context, String className, boolean systemApp,
- String summaryUri, String title, int titleResId) {
- final ResolveInfo info = new ResolveInfo();
- info.activityInfo = new ActivityInfo();
- info.activityInfo.applicationInfo = new ApplicationInfo();
- if (systemApp) {
- info.activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
- }
- info.activityInfo.packageName = context.getPackageName();
- info.activityInfo.applicationInfo.packageName = info.activityInfo.packageName;
- info.activityInfo.name = className;
- info.activityInfo.metaData = new Bundle();
- info.activityInfo.metaData.putInt(META_DATA_PREFERENCE_ICON, 314159);
- info.activityInfo.metaData.putString(META_DATA_PREFERENCE_SUMMARY, "static-summary");
- if (summaryUri != null) {
- info.activityInfo.metaData.putString(META_DATA_PREFERENCE_SUMMARY_URI, summaryUri);
- }
- if (titleResId != 0) {
- info.activityInfo.metaData.putInt(META_DATA_PREFERENCE_TITLE, titleResId);
- } else if (title != null) {
- info.activityInfo.metaData.putString(META_DATA_PREFERENCE_TITLE, title);
- }
- return info;
- }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistryTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistryTest.java
deleted file mode 100644
index 3069ae8..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionCategoryRegistryTest.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions.model;
-
-import static com.android.settings.intelligence.suggestions.model.SuggestionCategoryRegistry
- .CATEGORIES;
-import static com.google.common.truth.Truth.assertThat;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionCategoryRegistryTest {
-
- @Test
- public void getCategories_shouldHave10Categories() {
- assertThat(CATEGORIES)
- .hasSize(10);
- }
-
- @Test
- public void verifyExclusiveCategories() {
- final List<String> exclusiveCategories = new ArrayList<>();
- exclusiveCategories.add(SuggestionCategoryRegistry.CATEGORY_KEY_DEFERRED_SETUP);
- exclusiveCategories.add(SuggestionCategoryRegistry.CATEGORY_KEY_FIRST_IMPRESSION);
-
- int exclusiveCount = 0;
- for (SuggestionCategory category : CATEGORIES) {
- if (category.isExclusive()) {
- exclusiveCount++;
- assertThat(exclusiveCategories).contains(category.getCategory());
- }
- }
- assertThat(exclusiveCount).isEqualTo(exclusiveCategories.size());
- }
-
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilderTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilderTest.java
deleted file mode 100644
index 47eda56..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/model/SuggestionListBuilderTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions.model;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.util.Arrays;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionListBuilderTest {
-
- private SuggestionListBuilder mBuilder;
- private SuggestionCategory mCategory1;
- private SuggestionCategory mCategory2;
- private Suggestion mSuggestion1;
- private Suggestion mSuggestion2;
-
- @Before
- public void setUp() {
- mSuggestion1 = new Suggestion.Builder("id1")
- .setTitle("title1")
- .setSummary("summary1")
- .build();
- mCategory1 = SuggestionCategoryRegistry.CATEGORIES.get(3);
- mCategory2 = SuggestionCategoryRegistry.CATEGORIES.get(4);
- mSuggestion2 = new Suggestion.Builder("id2")
- .setTitle("title2")
- .setSummary("summary2")
- .build();
- mBuilder = new SuggestionListBuilder();
- }
-
- @Test
- public void dedupe_shouldSkipSameSuggestion() {
- mBuilder.addSuggestions(mCategory1, Arrays.asList(mSuggestion1));
- mBuilder.addSuggestions(mCategory2, Arrays.asList(mSuggestion1));
-
- assertThat(mBuilder.build()).hasSize(1);
- }
-
- @Test
- public void dedupe_shouldContainDifferentSuggestion() {
- mBuilder.addSuggestions(mCategory1, Arrays.asList(mSuggestion1, mSuggestion2));
-
- assertThat(mBuilder.build()).hasSize(2);
- }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/EventStoreTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/EventStoreTest.java
deleted file mode 100644
index 6680f20..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/EventStoreTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions.ranking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class EventStoreTest {
- private EventStore mEventStore;
-
- @Before
- public void setUp() {
- mEventStore = new EventStore(RuntimeEnvironment.application);
- }
-
- @Test
- public void testWriteRead() {
- mEventStore.writeEvent("pkg", EventStore.EVENT_SHOWN);
- long timeMs = System.currentTimeMillis();
- assertThat(mEventStore.readMetric("pkg", EventStore.EVENT_SHOWN, EventStore.METRIC_COUNT))
- .isEqualTo(1);
- assertThat(Math.abs(timeMs - mEventStore.readMetric("pkg", EventStore.EVENT_SHOWN,
- EventStore.METRIC_LAST_EVENT_TIME)) < 10000)
- .isTrue();
- }
-
- @Test
- public void testWriteRead_shouldHaveLatestValues() {
- mEventStore.writeEvent("pkg", EventStore.EVENT_DISMISSED);
- mEventStore.writeEvent("pkg", EventStore.EVENT_DISMISSED);
- assertThat(
- mEventStore.readMetric("pkg", EventStore.EVENT_DISMISSED, EventStore.METRIC_COUNT))
- .isEqualTo(2);
- }
-
- @Test
- public void testWriteRead_shouldReturnDefaultIfNotAvailable() {
- assertThat(mEventStore.readMetric("pkg", EventStore.EVENT_SHOWN, EventStore.METRIC_COUNT))
- .isEqualTo(0);
- assertThat(mEventStore.readMetric("pkg", EventStore.EVENT_SHOWN,
- EventStore.METRIC_LAST_EVENT_TIME))
- .isEqualTo(0);
- }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizerTest.java
deleted file mode 100644
index 1f349d3..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionFeaturizerTest.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2017 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.settings.intelligence.suggestions.ranking;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionFeaturizerTest {
-
- private EventStore mEventStore;
- private SuggestionFeaturizer mSuggestionFeaturizer;
-
- @Before
- public void setUp() {
- mEventStore = new EventStore(RuntimeEnvironment.application);
- mSuggestionFeaturizer = new SuggestionFeaturizer(mEventStore);
- }
-
- @Test
- public void testFeaturize_singlePackage() {
- mEventStore.writeEvent("pkg", EventStore.EVENT_DISMISSED);
- mEventStore.writeEvent("pkg", EventStore.EVENT_SHOWN);
- mEventStore.writeEvent("pkg", EventStore.EVENT_SHOWN);
- final Map<String, Double> features = mSuggestionFeaturizer
- .featurize(Arrays.asList(new Suggestion.Builder("pkg").build()))
- .get("pkg");
- assertThat(features.get(SuggestionFeaturizer.FEATURE_IS_SHOWN)).isEqualTo(1.0);
- assertThat(features.get(SuggestionFeaturizer.FEATURE_IS_DISMISSED)).isEqualTo(1.0);
- assertThat(features.get(SuggestionFeaturizer.FEATURE_IS_CLICKED)).isEqualTo(0.0);
-
- assertThat(features.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN)).isLessThan
- (1.0);
- assertThat(features.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED))
- .isLessThan(1.0);
- assertThat(features.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED))
- .isEqualTo(1.0);
- assertThat(features.get(SuggestionFeaturizer.FEATURE_SHOWN_COUNT))
- .isEqualTo(2.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR);
- assertThat(features.get(SuggestionFeaturizer.FEATURE_DISMISSED_COUNT))
- .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR);
- assertThat(features.get(SuggestionFeaturizer.FEATURE_CLICKED_COUNT)).isEqualTo(0.0);
- }
-
- @Test
- public void testFeaturize_multiplePackages() {
- mEventStore.writeEvent("pkg1", EventStore.EVENT_DISMISSED);
- mEventStore.writeEvent("pkg2", EventStore.EVENT_SHOWN);
- mEventStore.writeEvent("pkg1", EventStore.EVENT_SHOWN);
- final List<Suggestion> suggestions = new ArrayList<Suggestion>() {
- {
- add(new Suggestion.Builder("pkg1").build());
- add(new Suggestion.Builder("pkg2").build());
- }
- };
- final Map<String, Map<String, Double>> features = mSuggestionFeaturizer
- .featurize(suggestions);
- final Map<String, Double> features1 = features.get("pkg1");
- final Map<String, Double> features2 = features.get("pkg2");
-
- assertThat(features1.get(SuggestionFeaturizer.FEATURE_IS_SHOWN)).isEqualTo(1.0);
- assertThat(features1.get(SuggestionFeaturizer.FEATURE_IS_DISMISSED)).isEqualTo(1.0);
- assertThat(features1.get(SuggestionFeaturizer.FEATURE_IS_CLICKED)).isEqualTo(0.0);
- assertThat(features1.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN))
- .isLessThan(1.0);
- assertThat(features1.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED))
- .isLessThan(1.0);
- assertThat(features1.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED))
- .isEqualTo(1.0);
- assertThat(features1.get(SuggestionFeaturizer.FEATURE_SHOWN_COUNT))
- .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR);
- assertThat(features1.get(SuggestionFeaturizer.FEATURE_DISMISSED_COUNT))
- .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR);
- assertThat(features1.get(SuggestionFeaturizer.FEATURE_CLICKED_COUNT)).isEqualTo(0.0);
-
- assertThat(features2.get(SuggestionFeaturizer.FEATURE_IS_SHOWN)).isEqualTo(1.0);
- assertThat(features2.get(SuggestionFeaturizer.FEATURE_IS_DISMISSED)).isEqualTo(0.0);
- assertThat(features2.get(SuggestionFeaturizer.FEATURE_IS_CLICKED)).isEqualTo(0.0);
- assertThat(features2.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN))
- .isLessThan(1.0);
- assertThat(features2.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED))
- .isEqualTo(1.0);
- assertThat(features2.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED))
- .isEqualTo(1.0);
- assertThat(features2.get(SuggestionFeaturizer.FEATURE_SHOWN_COUNT))
- .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR);
- assertThat(features2.get(SuggestionFeaturizer.FEATURE_DISMISSED_COUNT)).isEqualTo(0.0);
- assertThat(features2.get(SuggestionFeaturizer.FEATURE_CLICKED_COUNT)).isEqualTo(0.0);
- }
-}
diff --git a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRankerTest.java b/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRankerTest.java
deleted file mode 100644
index f27f719..0000000
--- a/tests/robotests/src/com/android/settings/intelligence/suggestions/ranking/SuggestionRankerTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.android.settings.intelligence.suggestions.ranking;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Matchers.same;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
-
-import android.service.settings.suggestions.Suggestion;
-
-import com.android.settings.intelligence.TestConfig;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
-public class SuggestionRankerTest {
-
- @Mock
- private SuggestionRanker mSuggestionRanker;
- @Mock
- private SuggestionFeaturizer mSuggestionFeaturizer;
-
- private Map<String, Map<String, Double>> mFeatures;
- private List<Suggestion> mSuggestions;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mFeatures = new HashMap<>();
- mFeatures.put("pkg1", new HashMap<String, Double>());
- mFeatures.put("pkg2", new HashMap<String, Double>());
- mFeatures.put("pkg3", new HashMap<String, Double>());
- mSuggestions = new ArrayList<>();
-
- mSuggestions.add(new Suggestion.Builder("pkg1").build());
- mSuggestions.add(new Suggestion.Builder("pkg2").build());
- mSuggestions.add(new Suggestion.Builder("pkg3").build());
-
- mSuggestionFeaturizer = mock(SuggestionFeaturizer.class);
- mSuggestionRanker = new SuggestionRanker(mSuggestionFeaturizer);
- when(mSuggestionFeaturizer.featurize(mSuggestions)).thenReturn(mFeatures);
- mSuggestionRanker = spy(mSuggestionRanker);
- when(mSuggestionRanker.getRelevanceMetric(same(mFeatures.get("pkg1")))).thenReturn(0.9);
- when(mSuggestionRanker.getRelevanceMetric(same(mFeatures.get("pkg2")))).thenReturn(0.1);
- when(mSuggestionRanker.getRelevanceMetric(same(mFeatures.get("pkg3")))).thenReturn(0.5);
- }
-
- @Test
- public void testRank() {
- final List<Suggestion> expectedOrderdList = new ArrayList<>();
- expectedOrderdList.add(mSuggestions.get(0)); // relevance = 0.9
- expectedOrderdList.add(mSuggestions.get(2)); // relevance = 0.5
- expectedOrderdList.add(mSuggestions.get(1)); // relevance = 0.1
-
- mSuggestionRanker.rankSuggestions(mSuggestions);
- assertThat(mSuggestions).isEqualTo(expectedOrderdList);
- }
-}
\ No newline at end of file