diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 05:19:37 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 05:19:37 +0000 |
commit | df790e56c8f7d2a45d612b1eb6409c2e3fd4eeb6 (patch) | |
tree | a729ed1d2538977e1f4007efefbf842cf61df5c0 | |
parent | 0831985355c5aaf222a76402a082498ba52c0f7d (diff) | |
parent | 1c5d34d5176f212b7abc0192fc724f88c75de622 (diff) | |
download | QuickSearchBox-android14-mainline-sdkext-release.tar.gz |
Snap for 10453563 from 1c5d34d5176f212b7abc0192fc724f88c75de622 to mainline-sdkext-releaseaml_sdk_341510000aml_sdk_341410000aml_sdk_341110080aml_sdk_341110000aml_sdk_341010000aml_sdk_340912010android14-mainline-sdkext-release
Change-Id: I822e2a27d322258896fa55777c82b51313013aaf
192 files changed, 9674 insertions, 10960 deletions
@@ -1,6 +1,4 @@ -// // Copyright (C) 2009 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 @@ -22,18 +20,22 @@ package { android_app { name: "QuickSearchBox", sdk_version: "current", - static_libs: [ - "guava", - "android-common", - ], srcs: [ - "src/**/*.java", + "src/**/*.kt", "src/**/*.logtags", ], + static_libs: [ + "guava", + "android-common", + "androidx.core_core", + "kotlinx_coroutines", + ], certificate: "shared", product_specific: true, resource_dirs: ["res"], optimize: { proguard_flags_files: ["proguard.flags"], }, -} + + kotlincflags: ["-Werror"], +}
\ No newline at end of file @@ -1,24 +0,0 @@ -load("@rules_android//rules:rules.bzl", "android_binary") -load("//build/make/tools:event_log_tags.bzl", "event_log_tags") - -event_log_tags( - name = "genlogtags", - srcs = glob(["src/**/*.logtags"]), -) - -android_binary( - name = "QuickSearchBox", - srcs = glob(["src/**/*.java"]) + [ - ":genlogtags", - ], - custom_package = "com.android.quicksearchbox", - javacopts = ["-Xep:ArrayToString:OFF"], - manifest = "AndroidManifest.xml", - # TODO(182591919): uncomment the below once android rules are integrated with r8. - # proguard_specs = ["proguard.flags"], - resource_files = glob(["res/**"]), - deps = [ - "//external/guava", - "//frameworks/ex/common:android-common", - ], -) @@ -1,3 +1,5 @@ # Default code reviewers picked from top 3 or more developers. # Please update this list if you find better candidates. rtenneti@google.com +amithds@google.com +iankaz@google.com
\ No newline at end of file diff --git a/src/com/android/quicksearchbox/AbstractInternalSource.java b/src/com/android/quicksearchbox/AbstractInternalSource.java deleted file mode 100644 index 5567452..0000000 --- a/src/com/android/quicksearchbox/AbstractInternalSource.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import com.android.quicksearchbox.util.NamedTaskExecutor; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Handler; - -/** - * Abstract implementation of a source that is not backed by a searchable activity. - */ -public abstract class AbstractInternalSource extends AbstractSource { - - public AbstractInternalSource(Context context, Handler uiThread, NamedTaskExecutor iconLoader) { - super(context, uiThread, iconLoader); - } - - @Override - public String getSuggestUri() { - return null; - } - - @Override - public boolean canRead() { - return true; - } - - @Override - public String getDefaultIntentData() { - return null; - } - - @Override - protected String getIconPackage() { - return getContext().getPackageName(); - } - - @Override - public int getQueryThreshold() { - return 0; - } - - @Override - public Drawable getSourceIcon() { - return getContext().getResources().getDrawable(getSourceIconResource()); - } - - @Override - public Uri getSourceIconUri() { - return Uri.parse("android.resource://" + getContext().getPackageName() - + "/" + getSourceIconResource()); - } - - protected abstract int getSourceIconResource(); - - @Override - public boolean queryAfterZeroResults() { - return true; - } - -} diff --git a/src/com/android/quicksearchbox/AbstractInternalSource.kt b/src/com/android/quicksearchbox/AbstractInternalSource.kt new file mode 100644 index 0000000..3e142ad --- /dev/null +++ b/src/com/android/quicksearchbox/AbstractInternalSource.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.Context +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Handler +import com.android.quicksearchbox.util.NamedTaskExecutor + +/** Abstract implementation of a source that is not backed by a searchable activity. */ +abstract class AbstractInternalSource( + context: Context?, + uiThread: Handler?, + iconLoader: NamedTaskExecutor +) : AbstractSource(context, uiThread, iconLoader) { + @get:Override + override val suggestUri: String? + get() = null + + @Override + override fun canRead(): Boolean { + return true + } + + override val defaultIntentData: String? + get() = null + + @get:Override + override val iconPackage: String + get() = context!!.getPackageName() + + @get:Override + override val queryThreshold: Int + get() = 0 + + @get:Override + override val sourceIcon: Drawable + get() = context?.getResources()!!.getDrawable(sourceIconResource, null) + + @get:Override + override val sourceIconUri: Uri + get() = + Uri.parse( + "android.resource://" + context!!.getPackageName().toString() + "/" + sourceIconResource + ) + protected abstract val sourceIconResource: Int + + @Override + override fun queryAfterZeroResults(): Boolean { + return true + } +} diff --git a/src/com/android/quicksearchbox/AbstractSource.java b/src/com/android/quicksearchbox/AbstractSource.java deleted file mode 100644 index f8c6d0c..0000000 --- a/src/com/android/quicksearchbox/AbstractSource.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import com.android.quicksearchbox.util.NamedTaskExecutor; -import com.android.quicksearchbox.util.NowOrLater; - -import android.app.SearchManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.util.Log; - -/** - * Abstract suggestion source implementation. - */ -public abstract class AbstractSource implements Source { - - private static final String TAG = "QSB.AbstractSource"; - - private final Context mContext; - private final Handler mUiThread; - - private IconLoader mIconLoader; - - private final NamedTaskExecutor mIconLoaderExecutor; - - public AbstractSource(Context context, Handler uiThread, NamedTaskExecutor iconLoader) { - mContext = context; - mUiThread = uiThread; - mIconLoaderExecutor = iconLoader; - } - - protected Context getContext() { - return mContext; - } - - protected IconLoader getIconLoader() { - if (mIconLoader == null) { - String iconPackage = getIconPackage(); - mIconLoader = new CachingIconLoader( - new PackageIconLoader(mContext, iconPackage, mUiThread, mIconLoaderExecutor)); - } - return mIconLoader; - } - - protected abstract String getIconPackage(); - - @Override - public NowOrLater<Drawable> getIcon(String drawableId) { - return getIconLoader().getIcon(drawableId); - } - - @Override - public Uri getIconUri(String drawableId) { - return getIconLoader().getIconUri(drawableId); - } - - @Override - public Intent createSearchIntent(String query, Bundle appData) { - return createSourceSearchIntent(getIntentComponent(), query, appData); - } - - public static Intent createSourceSearchIntent(ComponentName activity, String query, - Bundle appData) { - if (activity == null) { - Log.w(TAG, "Tried to create search intent with no target activity"); - return null; - } - Intent intent = new Intent(Intent.ACTION_SEARCH); - intent.setComponent(activity); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // We need CLEAR_TOP to avoid reusing an old task that has other activities - // on top of the one we want. - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.putExtra(SearchManager.USER_QUERY, query); - intent.putExtra(SearchManager.QUERY, query); - if (appData != null) { - intent.putExtra(SearchManager.APP_DATA, appData); - } - return intent; - } - - protected Intent createVoiceWebSearchIntent(Bundle appData) { - return QsbApplication.get(mContext).getVoiceSearch() - .createVoiceWebSearchIntent(appData); - } - - @Override - public Source getRoot() { - return this; - } - - @Override - public boolean equals(Object o) { - if (o != null && o instanceof Source) { - Source s = ((Source) o).getRoot(); - if (s.getClass().equals(this.getClass())) { - return s.getName().equals(getName()); - } - } - return false; - } - - @Override - public int hashCode() { - return getName().hashCode(); - } - - @Override - public String toString() { - return "Source{name=" + getName() + "}"; - } - -} diff --git a/src/com/android/quicksearchbox/AbstractSource.kt b/src/com/android/quicksearchbox/AbstractSource.kt new file mode 100644 index 0000000..a10f1bd --- /dev/null +++ b/src/com/android/quicksearchbox/AbstractSource.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.SearchManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.util.Log +import com.android.quicksearchbox.util.NamedTaskExecutor +import com.android.quicksearchbox.util.NowOrLater + +/** Abstract suggestion source implementation. */ +abstract class AbstractSource( + context: Context?, + uiThread: Handler?, + iconLoader: NamedTaskExecutor +) : Source { + private val mContext: Context? + private val mUiThread: Handler? + private var mIconLoader: IconLoader? = null + private val mIconLoaderExecutor: NamedTaskExecutor + protected val context: Context? + get() = mContext + protected val iconLoader: IconLoader? + get() { + if (mIconLoader == null) { + val iconPackage = iconPackage + mIconLoader = + CachingIconLoader( + PackageIconLoader(mContext, iconPackage, mUiThread, mIconLoaderExecutor) + ) + } + return mIconLoader + } + protected abstract val iconPackage: String + + @Override + override fun getIcon(drawableId: String?): NowOrLater<Drawable?>? { + return iconLoader?.getIcon(drawableId) + } + + @Override + override fun getIconUri(drawableId: String?): Uri? { + return iconLoader?.getIconUri(drawableId) + } + + @Override + override fun createSearchIntent(query: String?, appData: Bundle?): Intent? { + return createSourceSearchIntent(intentComponent, query, appData) + } + + protected fun createVoiceWebSearchIntent(appData: Bundle?): Intent? { + return QsbApplication.get(mContext).voiceSearch?.createVoiceWebSearchIntent(appData) + } + + override fun getRoot(): Source { + return this + } + + @Override + override fun equals(other: Any?): Boolean { + if (other is Source) { + val s: Source = other.getRoot() + if (s::class == this::class) { + return s.name.equals(name) + } + } + return false + } + + @Override + override fun hashCode(): Int { + return name.hashCode() + } + + @Override + override fun toString(): String { + return "Source{name=" + name.toString() + "}" + } + + companion object { + private const val TAG = "QSB.AbstractSource" + + @JvmStatic + fun createSourceSearchIntent( + activity: ComponentName?, + query: String?, + appData: Bundle? + ): Intent? { + if (activity == null) { + Log.w(TAG, "Tried to create search intent with no target activity") + return null + } + val intent = Intent(Intent.ACTION_SEARCH) + intent.setComponent(activity) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // We need CLEAR_TOP to avoid reusing an old task that has other activities + // on top of the one we want. + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.putExtra(SearchManager.USER_QUERY, query) + intent.putExtra(SearchManager.QUERY, query) + if (appData != null) { + intent.putExtra(SearchManager.APP_DATA, appData) + } + return intent + } + } + + init { + mContext = context + mUiThread = uiThread + mIconLoaderExecutor = iconLoader + } +} diff --git a/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.java b/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.java deleted file mode 100644 index 78fc1c2..0000000 --- a/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -/** - * A SuggestionCursor that delegates all calls to other suggestions. - */ -public abstract class AbstractSuggestionCursorWrapper extends AbstractSuggestionWrapper - implements SuggestionCursor { - - private final String mUserQuery; - - public AbstractSuggestionCursorWrapper(String userQuery) { - mUserQuery = userQuery; - } - - public String getUserQuery() { - return mUserQuery; - } -} diff --git a/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.kt b/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.kt new file mode 100644 index 0000000..5e00434 --- /dev/null +++ b/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +/** A SuggestionCursor that delegates all calls to other suggestions. */ +abstract class AbstractSuggestionCursorWrapper(override val userQuery: String) : + AbstractSuggestionWrapper(), SuggestionCursor diff --git a/src/com/android/quicksearchbox/AbstractSuggestionExtras.java b/src/com/android/quicksearchbox/AbstractSuggestionExtras.java deleted file mode 100644 index f2c5690..0000000 --- a/src/com/android/quicksearchbox/AbstractSuggestionExtras.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import org.json.JSONException; - -import java.util.Collection; -import java.util.HashSet; - -/** - * Abstract SuggestionExtras supporting flattening to JSON. - */ -public abstract class AbstractSuggestionExtras implements SuggestionExtras { - - private final SuggestionExtras mMore; - - protected AbstractSuggestionExtras(SuggestionExtras more) { - mMore = more; - } - - public Collection<String> getExtraColumnNames() { - HashSet<String> columns = new HashSet<String>(); - columns.addAll(doGetExtraColumnNames()); - if (mMore != null) { - columns.addAll(mMore.getExtraColumnNames()); - } - return columns; - } - - protected abstract Collection<String> doGetExtraColumnNames(); - - public String getExtra(String columnName) { - String extra = doGetExtra(columnName); - if (extra == null && mMore != null) { - extra = mMore.getExtra(columnName); - } - return extra; - } - - protected abstract String doGetExtra(String columnName); - - public String toJsonString() throws JSONException { - return new JsonBackedSuggestionExtras(this).toString(); - } - -} diff --git a/src/com/android/quicksearchbox/AbstractSuggestionExtras.kt b/src/com/android/quicksearchbox/AbstractSuggestionExtras.kt new file mode 100644 index 0000000..04905cb --- /dev/null +++ b/src/com/android/quicksearchbox/AbstractSuggestionExtras.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import kotlin.collections.HashSet +import org.json.JSONException + +/** Abstract SuggestionExtras supporting flattening to JSON. */ +abstract class AbstractSuggestionExtras +protected constructor(private val mMore: SuggestionExtras?) : SuggestionExtras { + @get:Override + override val extraColumnNames: Collection<String> + get() { + val columns: HashSet<String> = HashSet<String>() + columns.addAll(doGetExtraColumnNames()) + if (mMore != null) { + columns.addAll(mMore.extraColumnNames) + } + return columns + } + + protected abstract fun doGetExtraColumnNames(): Collection<String> + override fun getExtra(columnName: String?): String? { + var extra = doGetExtra(columnName) + if (extra == null && mMore != null) { + extra = mMore.getExtra(columnName) + } + return extra + } + + protected abstract fun doGetExtra(columnName: String?): String? + + @Throws(JSONException::class) + override fun toJsonString(): String? { + return JsonBackedSuggestionExtras(this).toString() + } +} diff --git a/src/com/android/quicksearchbox/AbstractSuggestionWrapper.java b/src/com/android/quicksearchbox/AbstractSuggestionWrapper.java deleted file mode 100644 index a8e4f2b..0000000 --- a/src/com/android/quicksearchbox/AbstractSuggestionWrapper.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.content.ComponentName; - -/** - * A Suggestion that delegates all calls to other suggestions. - */ -public abstract class AbstractSuggestionWrapper implements Suggestion { - - /** - * Gets the current suggestion. - */ - protected abstract Suggestion current(); - - public String getShortcutId() { - return current().getShortcutId(); - } - - public String getSuggestionFormat() { - return current().getSuggestionFormat(); - } - - public String getSuggestionIcon1() { - return current().getSuggestionIcon1(); - } - - public String getSuggestionIcon2() { - return current().getSuggestionIcon2(); - } - - public String getSuggestionIntentAction() { - return current().getSuggestionIntentAction(); - } - - public ComponentName getSuggestionIntentComponent() { - return current().getSuggestionIntentComponent(); - } - - public String getSuggestionIntentDataString() { - return current().getSuggestionIntentDataString(); - } - - public String getSuggestionIntentExtraData() { - return current().getSuggestionIntentExtraData(); - } - - public String getSuggestionLogType() { - return current().getSuggestionLogType(); - } - - public String getSuggestionQuery() { - return current().getSuggestionQuery(); - } - - public Source getSuggestionSource() { - return current().getSuggestionSource(); - } - - public String getSuggestionText1() { - return current().getSuggestionText1(); - } - - public String getSuggestionText2() { - return current().getSuggestionText2(); - } - - public String getSuggestionText2Url() { - return current().getSuggestionText2Url(); - } - - public boolean isSpinnerWhileRefreshing() { - return current().isSpinnerWhileRefreshing(); - } - - public boolean isSuggestionShortcut() { - return current().isSuggestionShortcut(); - } - - public boolean isWebSearchSuggestion() { - return current().isWebSearchSuggestion(); - } - - public boolean isHistorySuggestion() { - return current().isHistorySuggestion(); - } - - public SuggestionExtras getExtras() { - return current().getExtras(); - } - -} diff --git a/src/com/android/quicksearchbox/AbstractSuggestionWrapper.kt b/src/com/android/quicksearchbox/AbstractSuggestionWrapper.kt new file mode 100644 index 0000000..d57e230 --- /dev/null +++ b/src/com/android/quicksearchbox/AbstractSuggestionWrapper.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.ComponentName + +/** A Suggestion that delegates all calls to other suggestions. */ +abstract class AbstractSuggestionWrapper : Suggestion { + /** Gets the current suggestion. */ + protected abstract fun current(): Suggestion? + override val shortcutId: String? + get() = current()?.shortcutId + override val suggestionFormat: String? + get() = current()?.suggestionFormat + override val suggestionIcon1: String? + get() = current()?.suggestionIcon1 + override val suggestionIcon2: String? + get() = current()?.suggestionIcon2 + override val suggestionIntentAction: String? + get() = current()?.suggestionIntentAction + override val suggestionIntentComponent: ComponentName? + get() = current()?.suggestionIntentComponent + override val suggestionIntentDataString: String? + get() = current()?.suggestionIntentDataString + override val suggestionIntentExtraData: String? + get() = current()?.suggestionIntentExtraData + override val suggestionLogType: String? + get() = current()?.suggestionLogType + override val suggestionQuery: String? + get() = current()?.suggestionQuery + override val suggestionSource: Source? + get() = current()?.suggestionSource + override val suggestionText1: String? + get() = current()?.suggestionText1 + override val suggestionText2: String? + get() = current()?.suggestionText2 + override val suggestionText2Url: String? + get() = current()?.suggestionText2Url + override val isSpinnerWhileRefreshing: Boolean + get() = current()?.isSpinnerWhileRefreshing == true + override val isSuggestionShortcut: Boolean + get() = current()?.isSuggestionShortcut == true + override val isWebSearchSuggestion: Boolean + get() = current()?.isWebSearchSuggestion == true + override val isHistorySuggestion: Boolean + get() = current()?.isHistorySuggestion == true + override val extras: SuggestionExtras? + get() = current()?.extras +} diff --git a/src/com/android/quicksearchbox/CachingIconLoader.java b/src/com/android/quicksearchbox/CachingIconLoader.java deleted file mode 100644 index ea45b40..0000000 --- a/src/com/android/quicksearchbox/CachingIconLoader.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import com.android.quicksearchbox.util.CachedLater; -import com.android.quicksearchbox.util.Consumer; -import com.android.quicksearchbox.util.Now; -import com.android.quicksearchbox.util.NowOrLater; -import com.android.quicksearchbox.util.NowOrLaterWrapper; - -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import java.util.WeakHashMap; - -/** - * Icon loader that caches the results of another icon loader. - * - */ -public class CachingIconLoader implements IconLoader { - - private static final boolean DBG = false; - private static final String TAG = "QSB.CachingIconLoader"; - - private final IconLoader mWrapped; - - private final WeakHashMap<String, Entry> mIconCache; - - /** - * Creates a new caching icon loader. - * - * @param wrapped IconLoader whose results will be cached. - */ - public CachingIconLoader(IconLoader wrapped) { - mWrapped = wrapped; - mIconCache = new WeakHashMap<String, Entry>(); - } - - public NowOrLater<Drawable> getIcon(String drawableId) { - if (DBG) Log.d(TAG, "getIcon(" + drawableId + ")"); - if (TextUtils.isEmpty(drawableId) || "0".equals(drawableId)) { - return new Now<Drawable>(null); - } - Entry newEntry = null; - NowOrLater<Drawable.ConstantState> drawableState; - synchronized (this) { - drawableState = queryCache(drawableId); - if (drawableState == null) { - newEntry = new Entry(); - storeInIconCache(drawableId, newEntry); - } - } - if (drawableState != null) { - return new NowOrLaterWrapper<Drawable.ConstantState, Drawable>(drawableState){ - @Override - public Drawable get(Drawable.ConstantState value) { - return value == null ? null : value.newDrawable(); - }}; - } - NowOrLater<Drawable> drawable = mWrapped.getIcon(drawableId); - newEntry.set(drawable); - storeInIconCache(drawableId, newEntry); - return drawable; - } - - public Uri getIconUri(String drawableId) { - return mWrapped.getIconUri(drawableId); - } - - private synchronized NowOrLater<Drawable.ConstantState> queryCache(String drawableId) { - NowOrLater<Drawable.ConstantState> cached = mIconCache.get(drawableId); - if (DBG) { - if (cached != null) Log.d(TAG, "Found icon in cache: " + drawableId); - } - return cached; - } - - private synchronized void storeInIconCache(String resourceUri, Entry drawable) { - if (drawable != null) { - mIconCache.put(resourceUri, drawable); - } - } - - private static class Entry extends CachedLater<Drawable.ConstantState> - implements Consumer<Drawable>{ - private NowOrLater<Drawable> mDrawable; - private boolean mGotDrawable; - private boolean mCreateRequested; - - public Entry() { - } - - public synchronized void set(NowOrLater<Drawable> drawable) { - if (mGotDrawable) throw new IllegalStateException("set() may only be called once."); - mGotDrawable = true; - mDrawable = drawable; - if (mCreateRequested) { - getLater(); - } - } - - @Override - protected synchronized void create() { - if (!mCreateRequested) { - mCreateRequested = true; - if (mGotDrawable) { - getLater(); - } - } - } - - private void getLater() { - NowOrLater<Drawable> drawable = mDrawable; - mDrawable = null; - drawable.getLater(this); - } - - public boolean consume(Drawable value) { - store(value == null ? null : value.getConstantState()); - return true; - } - } - -} diff --git a/src/com/android/quicksearchbox/CachingIconLoader.kt b/src/com/android/quicksearchbox/CachingIconLoader.kt new file mode 100644 index 0000000..afc039b --- /dev/null +++ b/src/com/android/quicksearchbox/CachingIconLoader.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.graphics.drawable.Drawable +import android.net.Uri +import android.text.TextUtils +import android.util.Log +import com.android.quicksearchbox.util.* +import java.util.WeakHashMap + +/** Icon loader that caches the results of another icon loader. */ +class CachingIconLoader(private val mWrapped: IconLoader) : IconLoader { + private val mIconCache: WeakHashMap<String, Entry> + override fun getIcon(drawableId: String?): NowOrLater<Drawable?>? { + if (DBG) Log.d(TAG, "getIcon($drawableId)") + if (TextUtils.isEmpty(drawableId) || "0".equals(drawableId)) { + return Now<Drawable>(null) + } + var newEntry: Entry? = null + var drawableState: NowOrLater<Drawable.ConstantState?>? + synchronized(this) { + drawableState = queryCache(drawableId) + if (drawableState == null) { + newEntry = Entry() + storeInIconCache(drawableId, newEntry) + } + } + if (drawableState != null) { + return object : NowOrLaterWrapper<Drawable.ConstantState?, Drawable?>(drawableState!!) { + @Override + override operator fun get(value: Drawable.ConstantState?): Drawable? { + return if (value == null) null else value.newDrawable() + } + } + } + val drawable: NowOrLater<Drawable?>? = mWrapped.getIcon(drawableId) + newEntry?.set(drawable) + storeInIconCache(drawableId, newEntry) + return drawable!! + } + + override fun getIconUri(drawableId: String?): Uri? { + return mWrapped.getIconUri(drawableId) + } + + @Synchronized + private fun queryCache(drawableId: String?): NowOrLater<Drawable.ConstantState?>? { + val cached: Entry? = mIconCache.get(drawableId) + if (DBG) { + if (cached != null) Log.d(TAG, "Found icon in cache: $drawableId") + } + return cached + } + + @Synchronized + private fun storeInIconCache(resourceUri: String?, drawable: Entry?) { + if (drawable != null) { + mIconCache.put(resourceUri, drawable) + } + } + + private class Entry : CachedLater<Drawable.ConstantState?>(), Consumer<Drawable?> { + private var mDrawable: NowOrLater<Drawable?>? = null + private var mGotDrawable = false + private var mCreateRequested = false + + @Synchronized + fun set(drawable: NowOrLater<Drawable?>?) { + if (mGotDrawable) throw IllegalStateException("set() may only be called once.") + mGotDrawable = true + mDrawable = drawable + if (mCreateRequested) { + later + } + } + + @Override + @Synchronized + override fun create() { + if (!mCreateRequested) { + mCreateRequested = true + if (mGotDrawable) { + later + } + } + } + + private val later: Unit + get() { + val drawable: NowOrLater<Drawable?>? = mDrawable + mDrawable = null + drawable!!.getLater(this) + } + + override fun consume(value: Drawable?): Boolean { + store(if (value == null) null else value.getConstantState()) + return true + } + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.CachingIconLoader" + } + + /** + * Creates a new caching icon loader. + * + * @param wrapped IconLoader whose results will be cached. + */ + init { + mIconCache = WeakHashMap<String, Entry>() + } +} diff --git a/src/com/android/quicksearchbox/Config.java b/src/com/android/quicksearchbox/Config.java deleted file mode 100644 index 678d411..0000000 --- a/src/com/android/quicksearchbox/Config.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import android.app.AlarmManager; -import android.content.Context; -import android.net.Uri; -import android.os.Process; -import android.util.Log; - -import java.util.HashSet; - -/** - * Provides values for configurable parameters in all of QSB. - * - * All the methods in this class return fixed default values. Subclasses may - * make these values server-side settable. - * - */ -public class Config { - - private static final String TAG = "QSB.Config"; - private static final boolean DBG = false; - - protected static final long SECOND_MILLIS = 1000L; - protected static final long MINUTE_MILLIS = 60L * SECOND_MILLIS; - protected static final long DAY_MILLIS = 86400000L; - - private static final int NUM_PROMOTED_SOURCES = 3; - private static final int MAX_RESULTS_PER_SOURCE = 50; - private static final long SOURCE_TIMEOUT_MILLIS = 10000; - - private static final int QUERY_THREAD_PRIORITY = - Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE; - - private static final long MAX_STAT_AGE_MILLIS = 30 * DAY_MILLIS; - private static final int MIN_CLICKS_FOR_SOURCE_RANKING = 3; - - private static final int NUM_WEB_CORPUS_THREADS = 2; - - private static final int LATENCY_LOG_FREQUENCY = 1000; - - private static final long TYPING_SUGGESTIONS_UPDATE_DELAY_MILLIS = 100; - private static final long PUBLISH_RESULT_DELAY_MILLIS = 200; - - private static final long VOICE_SEARCH_HINT_ACTIVE_PERIOD = 7L * DAY_MILLIS; - - private static final long VOICE_SEARCH_HINT_UPDATE_INTERVAL - = AlarmManager.INTERVAL_FIFTEEN_MINUTES; - - private static final long VOICE_SEARCH_HINT_SHOW_PERIOD_MILLIS - = AlarmManager.INTERVAL_HOUR * 2; - - private static final long VOICE_SEARCH_HINT_CHANGE_PERIOD = 2L * MINUTE_MILLIS; - - private static final long VOICE_SEARCH_HINT_VISIBLE_PERIOD = 6L * MINUTE_MILLIS; - - private static final int HTTP_CONNECT_TIMEOUT_MILLIS = 4000; - private static final int HTTP_READ_TIMEOUT_MILLIS = 4000; - - private static final String USER_AGENT = "Android/1.0"; - - private final Context mContext; - private HashSet<String> mDefaultCorpora; - private HashSet<String> mHiddenCorpora; - private HashSet<String> mDefaultCorporaSuggestUris; - - /** - * Creates a new config that uses hard-coded default values. - */ - public Config(Context context) { - mContext = context; - } - - protected Context getContext() { - return mContext; - } - - /** - * Releases any resources used by the configuration object. - * - * Default implementation does nothing. - */ - public void close() { - } - - private HashSet<String> loadResourceStringSet(int res) { - HashSet<String> set = new HashSet<String>(); - String[] items = mContext.getResources().getStringArray(res); - for (String item : items) { - set.add(item); - } - return set; - } - - /** - * The number of promoted sources. - */ - public int getNumPromotedSources() { - return NUM_PROMOTED_SOURCES; - } - - /** - * The number of suggestions visible above the onscreen keyboard. - */ - public int getNumSuggestionsAboveKeyboard() { - // Get the list of default corpora from a resource, which allows vendor overlays. - return mContext.getResources().getInteger(R.integer.num_suggestions_above_keyboard); - } - - /** - * The maximum number of suggestions to promote. - */ - public int getMaxPromotedSuggestions() { - return mContext.getResources().getInteger(R.integer.max_promoted_suggestions); - } - - public int getMaxPromotedResults() { - return mContext.getResources().getInteger(R.integer.max_promoted_results); - } - - /** - * The number of results to ask each source for. - */ - public int getMaxResultsPerSource() { - return MAX_RESULTS_PER_SOURCE; - } - - /** - * The maximum number of shortcuts to show for the web source in All mode. - */ - public int getMaxShortcutsPerWebSource() { - return mContext.getResources().getInteger(R.integer.max_shortcuts_per_web_source); - } - - /** - * The maximum number of shortcuts to show for each non-web source in All mode. - */ - public int getMaxShortcutsPerNonWebSource() { - return mContext.getResources().getInteger(R.integer.max_shortcuts_per_non_web_source); - } - - /** - * Gets the maximum number of shortcuts that will be shown from the given source. - */ - public int getMaxShortcuts(String sourceName) { - return getMaxShortcutsPerNonWebSource(); - } - - /** - * The timeout for querying each source, in milliseconds. - */ - public long getSourceTimeoutMillis() { - return SOURCE_TIMEOUT_MILLIS; - } - - /** - * The priority of query threads. - * - * @return A thread priority, as defined in {@link Process}. - */ - public int getQueryThreadPriority() { - return QUERY_THREAD_PRIORITY; - } - - /** - * The maximum age of log data used for shortcuts. - */ - public long getMaxStatAgeMillis(){ - return MAX_STAT_AGE_MILLIS; - } - - /** - * The minimum number of clicks needed to rank a source. - */ - public int getMinClicksForSourceRanking(){ - return MIN_CLICKS_FOR_SOURCE_RANKING; - } - - public int getNumWebCorpusThreads() { - return NUM_WEB_CORPUS_THREADS; - } - - /** - * How often query latency should be logged. - * - * @return An integer in the range 0-1000. 0 means that no latency events - * should be logged. 1000 means that all latency events should be logged. - */ - public int getLatencyLogFrequency() { - return LATENCY_LOG_FREQUENCY; - } - - /** - * The delay in milliseconds before suggestions are updated while typing. - * If a new character is typed before this timeout expires, the timeout is reset. - */ - public long getTypingUpdateSuggestionsDelayMillis() { - return TYPING_SUGGESTIONS_UPDATE_DELAY_MILLIS; - } - - public boolean allowVoiceSearchHints() { - return true; - } - - /** - * The period of time for which after installing voice search we should consider showing voice - * search hints. - * - * @return The period in milliseconds. - */ - public long getVoiceSearchHintActivePeriod() { - return VOICE_SEARCH_HINT_ACTIVE_PERIOD; - } - - /** - * The time interval at which we should consider whether or not to show some voice search hints. - * - * @return The period in milliseconds. - */ - public long getVoiceSearchHintUpdatePeriod() { - return VOICE_SEARCH_HINT_UPDATE_INTERVAL; - } - - /** - * The time interval at which, on average, voice search hints are displayed. - * - * @return The period in milliseconds. - */ - public long getVoiceSearchHintShowPeriod() { - return VOICE_SEARCH_HINT_SHOW_PERIOD_MILLIS; - } - - /** - * The amount of time for which voice search hints are displayed in one go. - * - * @return The period in milliseconds. - */ - public long getVoiceSearchHintVisibleTime() { - return VOICE_SEARCH_HINT_VISIBLE_PERIOD; - } - - /** - * The period that we change voice search hints at while they're being displayed. - * - * @return The period in milliseconds. - */ - public long getVoiceSearchHintChangePeriod() { - return VOICE_SEARCH_HINT_CHANGE_PERIOD; - } - - public boolean showSuggestionsForZeroQuery() { - // Get the list of default corpora from a resource, which allows vendor overlays. - return mContext.getResources().getBoolean(R.bool.show_zero_query_suggestions); - } - - public boolean showShortcutsForZeroQuery() { - // Get the list of default corpora from a resource, which allows vendor overlays. - return mContext.getResources().getBoolean(R.bool.show_zero_query_shortcuts); - } - - public boolean showScrollingSuggestions() { - return mContext.getResources().getBoolean(R.bool.show_scrolling_suggestions); - } - - public boolean showScrollingResults() { - return mContext.getResources().getBoolean(R.bool.show_scrolling_results); - } - - public Uri getHelpUrl(String activity) { - return null; - } - - public int getHttpConnectTimeout() { - return HTTP_CONNECT_TIMEOUT_MILLIS; - } - - public int getHttpReadTimeout() { - return HTTP_READ_TIMEOUT_MILLIS; - } - - public String getUserAgent() { - return USER_AGENT; - } -} diff --git a/src/com/android/quicksearchbox/Config.kt b/src/com/android/quicksearchbox/Config.kt new file mode 100644 index 0000000..5a44058 --- /dev/null +++ b/src/com/android/quicksearchbox/Config.kt @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.AlarmManager +import android.content.Context +import android.net.Uri +import android.os.Process +import java.util.HashSet + +/** + * Provides values for configurable parameters in all of QSB. + * + * All the methods in this class return fixed default values. Subclasses may make these values + * server-side settable. + */ +class Config(context: Context?) { + private val mContext: Context? + private val mDefaultCorpora: HashSet<String>? = null + private val mHiddenCorpora: HashSet<String>? = null + private val mDefaultCorporaSuggestUris: HashSet<String>? = null + protected val context: Context? + get() = mContext + + /** + * Releases any resources used by the configuration object. + * + * Default implementation does nothing. + */ + fun close() {} + private fun loadResourceStringSet(res: Int): HashSet<String> { + val set: HashSet<String> = HashSet<String>() + val items: Array<String> = mContext?.getResources()!!.getStringArray(res) + for (item in items) { + set.add(item) + } + return set + } + + /** The number of promoted sources. */ + val numPromotedSources: Int + get() = + NUM_PROMOTED_SOURCES // Get the list of default corpora from a resource, which allows vendor + // overlays. + + /** The number of suggestions visible above the onscreen keyboard. */ + val numSuggestionsAboveKeyboard: Int + get() = // Get the list of default corpora from a resource, which allows vendor overlays. + mContext?.getResources()!!.getInteger(R.integer.num_suggestions_above_keyboard) + + /** The maximum number of suggestions to promote. */ + val maxPromotedSuggestions: Int + get() = mContext?.getResources()!!.getInteger(R.integer.max_promoted_suggestions) + + val maxPromotedResults: Int + get() = mContext?.getResources()!!.getInteger(R.integer.max_promoted_results) + + /** The number of results to ask each source for. */ + val maxResultsPerSource: Int + get() = MAX_RESULTS_PER_SOURCE + + /** The maximum number of shortcuts to show for the web source in All mode. */ + val maxShortcutsPerWebSource: Int + get() = mContext?.getResources()!!.getInteger(R.integer.max_shortcuts_per_web_source) + + /** The maximum number of shortcuts to show for each non-web source in All mode. */ + val maxShortcutsPerNonWebSource: Int + get() = mContext?.getResources()!!.getInteger(R.integer.max_shortcuts_per_non_web_source) + + /** Gets the maximum number of shortcuts that will be shown from the given source. */ + @Suppress("UNUSED_PARAMETER") + fun getMaxShortcuts(sourceName: String?): Int { + return maxShortcutsPerNonWebSource + } + + /** The timeout for querying each source, in milliseconds. */ + val sourceTimeoutMillis: Long + get() = SOURCE_TIMEOUT_MILLIS + + /** + * The priority of query threads. + * + * @return A thread priority, as defined in [Process]. + */ + val queryThreadPriority: Int + get() = QUERY_THREAD_PRIORITY + + /** The maximum age of log data used for shortcuts. */ + val maxStatAgeMillis: Long + get() = MAX_STAT_AGE_MILLIS + + /** The minimum number of clicks needed to rank a source. */ + val minClicksForSourceRanking: Int + get() = MIN_CLICKS_FOR_SOURCE_RANKING + + val numWebCorpusThreads: Int + get() = NUM_WEB_CORPUS_THREADS + + /** + * How often query latency should be logged. + * + * @return An integer in the range 0-1000. 0 means that no latency events should be logged. 1000 + * means that all latency events should be logged. + */ + val latencyLogFrequency: Int + get() = LATENCY_LOG_FREQUENCY + + /** + * The delay in milliseconds before suggestions are updated while typing. If a new character is + * typed before this timeout expires, the timeout is reset. + */ + val typingUpdateSuggestionsDelayMillis: Long + get() = TYPING_SUGGESTIONS_UPDATE_DELAY_MILLIS + + fun allowVoiceSearchHints(): Boolean { + return true + } + + /** + * The period of time for which after installing voice search we should consider showing voice + * search hints. + * + * @return The period in milliseconds. + */ + val voiceSearchHintActivePeriod: Long + get() = VOICE_SEARCH_HINT_ACTIVE_PERIOD + + /** + * The time interval at which we should consider whether or not to show some voice search hints. + * + * @return The period in milliseconds. + */ + val voiceSearchHintUpdatePeriod: Long + get() = VOICE_SEARCH_HINT_UPDATE_INTERVAL + + /** + * The time interval at which, on average, voice search hints are displayed. + * + * @return The period in milliseconds. + */ + val voiceSearchHintShowPeriod: Long + get() = VOICE_SEARCH_HINT_SHOW_PERIOD_MILLIS + + /** + * The amount of time for which voice search hints are displayed in one go. + * + * @return The period in milliseconds. + */ + val voiceSearchHintVisibleTime: Long + get() = VOICE_SEARCH_HINT_VISIBLE_PERIOD + + /** + * The period that we change voice search hints at while they're being displayed. + * + * @return The period in milliseconds. + */ + val voiceSearchHintChangePeriod: Long + get() = VOICE_SEARCH_HINT_CHANGE_PERIOD + + fun showSuggestionsForZeroQuery(): Boolean { + // Get the list of default corpora from a resource, which allows vendor overlays. + return mContext?.getResources()!!.getBoolean(R.bool.show_zero_query_suggestions) + } + + fun showShortcutsForZeroQuery(): Boolean { + // Get the list of default corpora from a resource, which allows vendor overlays. + return mContext?.getResources()!!.getBoolean(R.bool.show_zero_query_shortcuts) + } + + fun showScrollingSuggestions(): Boolean { + return mContext?.getResources()!!.getBoolean(R.bool.show_scrolling_suggestions) + } + + fun showScrollingResults(): Boolean { + return mContext?.getResources()!!.getBoolean(R.bool.show_scrolling_results) + } + + @Suppress("UNUSED_PARAMETER") + fun getHelpUrl(activity: String?): Uri? { + return null + } + + val httpConnectTimeout: Int + get() = HTTP_CONNECT_TIMEOUT_MILLIS + + val httpReadTimeout: Int + get() = HTTP_READ_TIMEOUT_MILLIS + + val userAgent: String + get() = USER_AGENT + + companion object { + protected const val SECOND_MILLIS = 1000L + + @JvmField protected val MINUTE_MILLIS: Long = 60L * SECOND_MILLIS + private val VOICE_SEARCH_HINT_CHANGE_PERIOD: Long = 2L * MINUTE_MILLIS + private val VOICE_SEARCH_HINT_VISIBLE_PERIOD: Long = 6L * MINUTE_MILLIS + protected const val DAY_MILLIS = 86400000L + private const val TAG = "QSB.Config" + private const val DBG = false + private const val NUM_PROMOTED_SOURCES = 3 + private const val MAX_RESULTS_PER_SOURCE = 50 + private const val SOURCE_TIMEOUT_MILLIS: Long = 10000 + private val QUERY_THREAD_PRIORITY: Int = + Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE + private val MAX_STAT_AGE_MILLIS: Long = 30 * DAY_MILLIS + private const val MIN_CLICKS_FOR_SOURCE_RANKING = 3 + private const val NUM_WEB_CORPUS_THREADS = 2 + private const val LATENCY_LOG_FREQUENCY = 1000 + private const val TYPING_SUGGESTIONS_UPDATE_DELAY_MILLIS: Long = 100 + private const val PUBLISH_RESULT_DELAY_MILLIS: Long = 200 + private val VOICE_SEARCH_HINT_ACTIVE_PERIOD: Long = 7L * DAY_MILLIS + private val VOICE_SEARCH_HINT_UPDATE_INTERVAL: Long = AlarmManager.INTERVAL_FIFTEEN_MINUTES + private val VOICE_SEARCH_HINT_SHOW_PERIOD_MILLIS: Long = AlarmManager.INTERVAL_HOUR * 2 + private const val HTTP_CONNECT_TIMEOUT_MILLIS = 4000 + private const val HTTP_READ_TIMEOUT_MILLIS = 4000 + private const val USER_AGENT = "Android/1.0" + } + + /** Creates a new config that uses hard-coded default values. */ + init { + mContext = context + } +} diff --git a/src/com/android/quicksearchbox/CursorBackedSourceResult.java b/src/com/android/quicksearchbox/CursorBackedSourceResult.java deleted file mode 100644 index 7c2fe9f..0000000 --- a/src/com/android/quicksearchbox/CursorBackedSourceResult.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.content.ComponentName; -import android.database.Cursor; - -import com.android.quicksearchbox.google.GoogleSource; - -import java.util.Collection; - -public class CursorBackedSourceResult extends CursorBackedSuggestionCursor - implements SourceResult { - - private final GoogleSource mSource; - - public CursorBackedSourceResult(GoogleSource source, String userQuery) { - this(source, userQuery, null); - } - - public CursorBackedSourceResult(GoogleSource source, String userQuery, Cursor cursor) { - super(userQuery, cursor); - mSource = source; - } - - public GoogleSource getSource() { - return mSource; - } - - @Override - public GoogleSource getSuggestionSource() { - return mSource; - } - - @Override - public ComponentName getSuggestionIntentComponent() { - return mSource.getIntentComponent(); - } - - public boolean isSuggestionShortcut() { - return false; - } - - public boolean isHistorySuggestion() { - return false; - } - - @Override - public String toString() { - return mSource + "[" + getUserQuery() + "]"; - } - - @Override - public SuggestionExtras getExtras() { - if (mCursor == null) return null; - return CursorBackedSuggestionExtras.createExtrasIfNecessary(mCursor, getPosition()); - } - - public Collection<String> getExtraColumns() { - if (mCursor == null) return null; - return CursorBackedSuggestionExtras.getExtraColumns(mCursor); - } - -}
\ No newline at end of file diff --git a/src/com/android/quicksearchbox/CursorBackedSourceResult.kt b/src/com/android/quicksearchbox/CursorBackedSourceResult.kt new file mode 100644 index 0000000..098fcb4 --- /dev/null +++ b/src/com/android/quicksearchbox/CursorBackedSourceResult.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.ComponentName +import android.database.Cursor +import com.android.quicksearchbox.google.GoogleSource +import kotlin.collections.Collection + +class CursorBackedSourceResult( + override val suggestionSource: GoogleSource?, + userQuery: String?, + cursor: Cursor? +) : CursorBackedSuggestionCursor(userQuery, cursor), SourceResult { + + constructor(source: GoogleSource?, userQuery: String?) : this(source, userQuery, null) + + override val source: Source? + get() = suggestionSource + + @get:Override + override val suggestionIntentComponent: ComponentName? + get() = suggestionSource?.intentComponent + + override val isSuggestionShortcut: Boolean + get() = false + + override val isHistorySuggestion: Boolean + get() = false + + @Override + override fun toString(): String { + return suggestionSource.toString() + "[" + userQuery + "]" + } + + @get:Override + override val extras: SuggestionExtras? + get() = + if (mCursor == null) null + else CursorBackedSuggestionExtras.createExtrasIfNecessary(mCursor, position)!! + + override val extraColumns: Collection<String>? + get() = if (mCursor == null) null else CursorBackedSuggestionExtras.getExtraColumns(mCursor)!! +} diff --git a/src/com/android/quicksearchbox/CursorBackedSuggestionCursor.java b/src/com/android/quicksearchbox/CursorBackedSuggestionCursor.java deleted file mode 100644 index aa35164..0000000 --- a/src/com/android/quicksearchbox/CursorBackedSuggestionCursor.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import android.app.SearchManager; -import android.content.ComponentName; -import android.content.Intent; -import android.database.Cursor; -import android.database.DataSetObserver; -import android.net.Uri; -import android.util.Log; - -public abstract class CursorBackedSuggestionCursor implements SuggestionCursor { - - private static final boolean DBG = false; - protected static final String TAG = "QSB.CursorBackedSuggestionCursor"; - - public static final String SUGGEST_COLUMN_LOG_TYPE = "suggest_log_type"; - - private final String mUserQuery; - - /** The suggestions, or {@code null} if the suggestions query failed. */ - protected final Cursor mCursor; - - /** Column index of {@link SearchManager#SUGGEST_COLUMN_FORMAT} in @{link mCursor}. */ - private final int mFormatCol; - - /** Column index of {@link SearchManager#SUGGEST_COLUMN_TEXT_1} in @{link mCursor}. */ - private final int mText1Col; - - /** Column index of {@link SearchManager#SUGGEST_COLUMN_TEXT_2} in @{link mCursor}. */ - private final int mText2Col; - - /** Column index of {@link SearchManager#SUGGEST_COLUMN_TEXT_2_URL} in @{link mCursor}. */ - private final int mText2UrlCol; - - /** Column index of {@link SearchManager#SUGGEST_COLUMN_ICON_1} in @{link mCursor}. */ - private final int mIcon1Col; - - /** Column index of {@link SearchManager#SUGGEST_COLUMN_ICON_1} in @{link mCursor}. */ - private final int mIcon2Col; - - /** Column index of {@link SearchManager#SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING} - * in @{link mCursor}. - **/ - private final int mRefreshSpinnerCol; - - /** True if this result has been closed. */ - private boolean mClosed = false; - - public CursorBackedSuggestionCursor(String userQuery, Cursor cursor) { - mUserQuery = userQuery; - mCursor = cursor; - mFormatCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT); - mText1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1); - mText2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); - mText2UrlCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL); - mIcon1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); - mIcon2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2); - mRefreshSpinnerCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING); - } - - public String getUserQuery() { - return mUserQuery; - } - - public abstract Source getSuggestionSource(); - - public String getSuggestionLogType() { - return getStringOrNull(SUGGEST_COLUMN_LOG_TYPE); - } - - public void close() { - if (DBG) Log.d(TAG, "close()"); - if (mClosed) { - throw new IllegalStateException("Double close()"); - } - mClosed = true; - if (mCursor != null) { - try { - mCursor.close(); - } catch (RuntimeException ex) { - // all operations on cross-process cursors can throw random exceptions - Log.e(TAG, "close() failed, ", ex); - } - } - } - - @Override - protected void finalize() { - if (!mClosed) { - Log.e(TAG, "LEAK! Finalized without being closed: " + toString()); - } - } - - public int getCount() { - if (mClosed) { - throw new IllegalStateException("getCount() after close()"); - } - if (mCursor == null) return 0; - try { - return mCursor.getCount(); - } catch (RuntimeException ex) { - // all operations on cross-process cursors can throw random exceptions - Log.e(TAG, "getCount() failed, ", ex); - return 0; - } - } - - public void moveTo(int pos) { - if (mClosed) { - throw new IllegalStateException("moveTo(" + pos + ") after close()"); - } - try { - if (!mCursor.moveToPosition(pos)) { - Log.e(TAG, "moveToPosition(" + pos + ") failed, count=" + getCount()); - } - } catch (RuntimeException ex) { - // all operations on cross-process cursors can throw random exceptions - Log.e(TAG, "moveToPosition() failed, ", ex); - } - } - - public boolean moveToNext() { - if (mClosed) { - throw new IllegalStateException("moveToNext() after close()"); - } - try { - return mCursor.moveToNext(); - } catch (RuntimeException ex) { - // all operations on cross-process cursors can throw random exceptions - Log.e(TAG, "moveToNext() failed, ", ex); - return false; - } - } - - public int getPosition() { - if (mClosed) { - throw new IllegalStateException("getPosition after close()"); - } - try { - return mCursor.getPosition(); - } catch (RuntimeException ex) { - // all operations on cross-process cursors can throw random exceptions - Log.e(TAG, "getPosition() failed, ", ex); - return -1; - } - } - - public String getShortcutId() { - return getStringOrNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID); - } - - public String getSuggestionFormat() { - return getStringOrNull(mFormatCol); - } - - public String getSuggestionText1() { - return getStringOrNull(mText1Col); - } - - public String getSuggestionText2() { - return getStringOrNull(mText2Col); - } - - public String getSuggestionText2Url() { - return getStringOrNull(mText2UrlCol); - } - - public String getSuggestionIcon1() { - return getStringOrNull(mIcon1Col); - } - - public String getSuggestionIcon2() { - return getStringOrNull(mIcon2Col); - } - - public boolean isSpinnerWhileRefreshing() { - return "true".equals(getStringOrNull(mRefreshSpinnerCol)); - } - - /** - * Gets the intent action for the current suggestion. - */ - public String getSuggestionIntentAction() { - String action = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_ACTION); - if (action != null) return action; - return getSuggestionSource().getDefaultIntentAction(); - } - - public abstract ComponentName getSuggestionIntentComponent(); - - /** - * Gets the query for the current suggestion. - */ - public String getSuggestionQuery() { - return getStringOrNull(SearchManager.SUGGEST_COLUMN_QUERY); - } - - public String getSuggestionIntentDataString() { - // use specific data if supplied, or default data if supplied - String data = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA); - if (data == null) { - data = getSuggestionSource().getDefaultIntentData(); - } - // then, if an ID was provided, append it. - if (data != null) { - String id = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); - if (id != null) { - data = data + "/" + Uri.encode(id); - } - } - return data; - } - - /** - * Gets the intent extra data for the current suggestion. - */ - public String getSuggestionIntentExtraData() { - return getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); - } - - public boolean isWebSearchSuggestion() { - return Intent.ACTION_WEB_SEARCH.equals(getSuggestionIntentAction()); - } - - /** - * Gets the index of a column in {@link #mCursor} by name. - * - * @return The index, or {@code -1} if the column was not found. - */ - protected int getColumnIndex(String colName) { - if (mCursor == null) return -1; - try { - return mCursor.getColumnIndex(colName); - } catch (RuntimeException ex) { - // all operations on cross-process cursors can throw random exceptions - Log.e(TAG, "getColumnIndex() failed, ", ex); - return -1; - } - } - - /** - * Gets the string value of a column in {@link #mCursor} by column index. - * - * @param col Column index. - * @return The string value, or {@code null}. - */ - protected String getStringOrNull(int col) { - if (mCursor == null) return null; - if (col == -1) { - return null; - } - try { - return mCursor.getString(col); - } catch (RuntimeException ex) { - // all operations on cross-process cursors can throw random exceptions - Log.e(TAG, "getString() failed, ", ex); - return null; - } - } - - /** - * Gets the string value of a column in {@link #mCursor} by column name. - * - * @param colName Column name. - * @return The string value, or {@code null}. - */ - protected String getStringOrNull(String colName) { - int col = getColumnIndex(colName); - return getStringOrNull(col); - } - - public void registerDataSetObserver(DataSetObserver observer) { - // We don't watch Cursor-backed SuggestionCursors for changes - } - - public void unregisterDataSetObserver(DataSetObserver observer) { - // We don't watch Cursor-backed SuggestionCursors for changes - } - - @Override - public String toString() { - return getClass().getSimpleName() + "[" + mUserQuery + "]"; - } - -} diff --git a/src/com/android/quicksearchbox/CursorBackedSuggestionCursor.kt b/src/com/android/quicksearchbox/CursorBackedSuggestionCursor.kt new file mode 100644 index 0000000..43776d6 --- /dev/null +++ b/src/com/android/quicksearchbox/CursorBackedSuggestionCursor.kt @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.SearchManager +import android.content.ComponentName +import android.content.Intent +import android.database.Cursor +import android.database.DataSetObserver +import android.net.Uri +import android.util.Log + +abstract class CursorBackedSuggestionCursor(override val userQuery: String?, cursor: Cursor?) : + SuggestionCursor { + + /** The suggestions, or `null` if the suggestions query failed. */ + @JvmField protected val mCursor: Cursor? + + /** Column index of [SearchManager.SUGGEST_COLUMN_FORMAT] in @{link mCursor}. */ + private val mFormatCol: Int + + /** Column index of [SearchManager.SUGGEST_COLUMN_TEXT_1] in @{link mCursor}. */ + private val mText1Col: Int + + /** Column index of [SearchManager.SUGGEST_COLUMN_TEXT_2] in @{link mCursor}. */ + private val mText2Col: Int + + /** Column index of [SearchManager.SUGGEST_COLUMN_TEXT_2_URL] in @{link mCursor}. */ + private val mText2UrlCol: Int + + /** Column index of [SearchManager.SUGGEST_COLUMN_ICON_1] in @{link mCursor}. */ + private val mIcon1Col: Int + + /** Column index of [SearchManager.SUGGEST_COLUMN_ICON_1] in @{link mCursor}. */ + private val mIcon2Col: Int + + /** Column index of [SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING] in @{link mCursor}. */ + private val mRefreshSpinnerCol: Int + + /** True if this result has been closed. */ + private var mClosed = false + abstract override val suggestionSource: Source? + override val suggestionLogType: String? + get() = getStringOrNull(SUGGEST_COLUMN_LOG_TYPE) + + override fun close() { + if (DBG) Log.d(TAG, "close()") + if (mClosed) { + throw IllegalStateException("Double close()") + } + mClosed = true + if (mCursor != null) { + try { + mCursor.close() + } catch (ex: RuntimeException) { + // all operations on cross-process cursors can throw random exceptions + Log.e(TAG, "close() failed, ", ex) + } + } + } + + @Override + protected fun finalize() { + if (!mClosed) { + Log.e(TAG, "LEAK! Finalized without being closed: " + toString()) + } + } + + override val count: Int + get() { + if (mClosed) { + throw IllegalStateException("getCount() after close()") + } + return if (mCursor == null) 0 + else + try { + mCursor.getCount() + } catch (ex: RuntimeException) { + // all operations on cross-process cursors can throw random exceptions + Log.e(TAG, "getCount() failed, ", ex) + 0 + } + } + + override fun moveTo(pos: Int) { + if (mClosed) { + throw IllegalStateException("moveTo($pos) after close()") + } + try { + if (!mCursor!!.moveToPosition(pos)) { + Log.e(TAG, "moveToPosition($pos) failed, count=$count") + } + } catch (ex: RuntimeException) { + // all operations on cross-process cursors can throw random exceptions + Log.e(TAG, "moveToPosition() failed, ", ex) + } + } + + override fun moveToNext(): Boolean { + if (mClosed) { + throw IllegalStateException("moveToNext() after close()") + } + return try { + mCursor!!.moveToNext() + } catch (ex: RuntimeException) { + // all operations on cross-process cursors can throw random exceptions + Log.e(TAG, "moveToNext() failed, ", ex) + false + } + } + + override val position: Int + get() { + if (mClosed) { + throw IllegalStateException("get() on position after close()") + } + return try { + mCursor!!.position + } catch (ex: RuntimeException) { + // all operations on cross-process cursors can throw random exceptions + Log.e(TAG, "get() on position failed, ", ex) + -1 + } + } + override val shortcutId: String? + get() = getStringOrNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID) + override val suggestionFormat: String? + get() = getStringOrNull(mFormatCol) + override val suggestionText1: String? + get() = getStringOrNull(mText1Col) + override val suggestionText2: String? + get() = getStringOrNull(mText2Col) + override val suggestionText2Url: String? + get() = getStringOrNull(mText2UrlCol) + override val suggestionIcon1: String? + get() = getStringOrNull(mIcon1Col) + override val suggestionIcon2: String? + get() = getStringOrNull(mIcon2Col) + override val isSpinnerWhileRefreshing: Boolean + get() = "true".equals(getStringOrNull(mRefreshSpinnerCol)) + + /** Gets the intent action for the current suggestion. */ + override val suggestionIntentAction: String? + get() { + val action: String? = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_ACTION) + return action + } + abstract override val suggestionIntentComponent: ComponentName? + + /** Gets the query for the current suggestion. */ + override val suggestionQuery: String? + get() = getStringOrNull(SearchManager.SUGGEST_COLUMN_QUERY) + + override val suggestionIntentDataString: String? + get() { + // use specific data if supplied, or default data if supplied + var data: String? = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA) + if (data == null) { + data = suggestionSource?.defaultIntentData + } + // then, if an ID was provided, append it. + if (data != null) { + val id: String? = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID) + if (id != null) { + data = data.toString() + "/" + Uri.encode(id) + } + } + return data + } + + /** Gets the intent extra data for the current suggestion. */ + override val suggestionIntentExtraData: String? + get() = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA) + override val isWebSearchSuggestion: Boolean + get() = Intent.ACTION_WEB_SEARCH.equals(suggestionIntentAction) + + /** + * Gets the index of a column in [.mCursor] by name. + * + * @return The index, or `-1` if the column was not found. + */ + protected fun getColumnIndex(colName: String?): Int { + return if (mCursor == null) -1 + else + try { + mCursor.getColumnIndex(colName) + } catch (ex: RuntimeException) { + // all operations on cross-process cursors can throw random exceptions + Log.e(TAG, "getColumnIndex() failed, ", ex) + -1 + } + } + + /** + * Gets the string value of a column in [.mCursor] by column index. + * + * @param col Column index. + * @return The string value, or `null`. + */ + protected fun getStringOrNull(col: Int): String? { + if (mCursor == null) return null + return if (col == -1) { + null + } else + try { + mCursor.getString(col) + } catch (ex: RuntimeException) { + // all operations on cross-process cursors can throw random exceptions + Log.e(TAG, "getString() failed, ", ex) + null + } + } + + /** + * Gets the string value of a column in [.mCursor] by column name. + * + * @param colName Column name. + * @return The string value, or `null`. + */ + protected fun getStringOrNull(colName: String?): String? { + val col = getColumnIndex(colName) + return getStringOrNull(col) + } + + override fun registerDataSetObserver(observer: DataSetObserver?) { + // We don't watch Cursor-backed SuggestionCursors for changes + } + + override fun unregisterDataSetObserver(observer: DataSetObserver?) { + // We don't watch Cursor-backed SuggestionCursors for changes + } + + @Override + override fun toString(): String { + return this::class.simpleName.toString() + "[" + userQuery + "]" + } + + companion object { + private const val DBG = false + protected const val TAG = "QSB.CursorBackedSuggestionCursor" + const val SUGGEST_COLUMN_LOG_TYPE = "suggest_log_type" + } + + init { + mCursor = cursor + mFormatCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT) + mText1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1) + mText2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2) + mText2UrlCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL) + mIcon1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1) + mIcon2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2) + mRefreshSpinnerCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING) + } +} diff --git a/src/com/android/quicksearchbox/CursorBackedSuggestionExtras.java b/src/com/android/quicksearchbox/CursorBackedSuggestionExtras.java deleted file mode 100644 index b6d85ff..0000000 --- a/src/com/android/quicksearchbox/CursorBackedSuggestionExtras.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.database.Cursor; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; - -/** - * SuggestionExtras taking values from the extra columns in a suggestion cursor. - */ -public class CursorBackedSuggestionExtras extends AbstractSuggestionExtras { - private static final String TAG = "QSB.CursorBackedSuggestionExtras"; - - private static final HashSet<String> DEFAULT_COLUMNS = new HashSet<String>(); - static { - DEFAULT_COLUMNS.addAll(Arrays.asList(SuggestionCursorBackedCursor.COLUMNS)); - } - - private final Cursor mCursor; - private final int mCursorPosition; - private final List<String> mExtraColumns; - - static CursorBackedSuggestionExtras createExtrasIfNecessary(Cursor cursor, int position) { - List<String> extraColumns = getExtraColumns(cursor); - if (extraColumns != null) { - return new CursorBackedSuggestionExtras(cursor, position, extraColumns); - } else { - return null; - } - } - - static String[] getCursorColumns(Cursor cursor) { - try { - return cursor.getColumnNames(); - } catch (RuntimeException ex) { - // all operations on cross-process cursors can throw random exceptions - Log.e(TAG, "getColumnNames() failed, ", ex); - return null; - } - } - - static boolean cursorContainsExtras(Cursor cursor) { - String[] columns = getCursorColumns(cursor); - for (String cursorColumn : columns) { - if (!DEFAULT_COLUMNS.contains(cursorColumn)) { - return true; - } - } - return false; - } - - static List<String> getExtraColumns(Cursor cursor) { - String[] columns = getCursorColumns(cursor); - if (columns == null) return null; - List<String> extraColumns = null; - for (String cursorColumn : columns) { - if (!DEFAULT_COLUMNS.contains(cursorColumn)) { - if (extraColumns == null) { - extraColumns = new ArrayList<String>(); - } - extraColumns.add(cursorColumn); - } - } - return extraColumns; - } - - private CursorBackedSuggestionExtras(Cursor cursor, int position, List<String> extraColumns) { - super(null); - mCursor = cursor; - mCursorPosition = position; - mExtraColumns = extraColumns; - } - - @Override - public String doGetExtra(String columnName) { - try { - mCursor.moveToPosition(mCursorPosition); - int columnIdx = mCursor.getColumnIndex(columnName); - if (columnIdx < 0) return null; - return mCursor.getString(columnIdx); - } catch (RuntimeException ex) { - // all operations on cross-process cursors can throw random exceptions - Log.e(TAG, "getExtra(" + columnName + ") failed, ", ex); - return null; - } - } - - @Override - public List<String> doGetExtraColumnNames() { - return mExtraColumns; - } - -} diff --git a/src/com/android/quicksearchbox/CursorBackedSuggestionExtras.kt b/src/com/android/quicksearchbox/CursorBackedSuggestionExtras.kt new file mode 100644 index 0000000..2766488 --- /dev/null +++ b/src/com/android/quicksearchbox/CursorBackedSuggestionExtras.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.database.Cursor +import android.util.Log +import kotlin.Array +import kotlin.collections.ArrayList +import kotlin.collections.HashSet + +/** SuggestionExtras taking values from the extra columns in a suggestion cursor. */ +class CursorBackedSuggestionExtras +private constructor(cursor: Cursor?, position: Int, extraColumns: List<String>) : + AbstractSuggestionExtras(null) { + companion object { + private const val TAG = "QSB.CursorBackedSuggestionExtras" + private val DEFAULT_COLUMNS: HashSet<String> = HashSet<String>() + @JvmStatic + fun createExtrasIfNecessary(cursor: Cursor?, position: Int): CursorBackedSuggestionExtras? { + val extraColumns: List<String>? = + CursorBackedSuggestionExtras.Companion.getExtraColumns(cursor) + return if (extraColumns != null) { + CursorBackedSuggestionExtras(cursor, position, extraColumns) + } else { + null + } + } + + @JvmStatic + fun getCursorColumns(cursor: Cursor?): Array<String>? { + return try { + cursor?.getColumnNames() + } catch (ex: RuntimeException) { + // all operations on cross-process cursors can throw random exceptions + Log.e(CursorBackedSuggestionExtras.Companion.TAG, "getColumnNames() failed, ", ex) + null + } + } + + fun cursorContainsExtras(cursor: Cursor?): Boolean { + val columns: Array<String>? = CursorBackedSuggestionExtras.Companion.getCursorColumns(cursor) + if (columns != null) { + for (cursorColumn in columns) { + if (!CursorBackedSuggestionExtras.Companion.DEFAULT_COLUMNS.contains(cursorColumn)) { + return true + } + } + } + return false + } + + @JvmStatic + fun getExtraColumns(cursor: Cursor?): List<String>? { + val columns: Array<String> = + CursorBackedSuggestionExtras.Companion.getCursorColumns(cursor) ?: return null + var extraColumns: ArrayList<String>? = null + for (cursorColumn in columns) { + if (!CursorBackedSuggestionExtras.Companion.DEFAULT_COLUMNS.contains(cursorColumn)) { + if (extraColumns == null) { + extraColumns = arrayListOf<String>() + } + extraColumns.add(cursorColumn) + } + } + return extraColumns + } + + init { + CursorBackedSuggestionExtras.Companion.DEFAULT_COLUMNS.addAll( + SuggestionCursorBackedCursor.COLUMNS.asList() + ) + } + } + + private val mCursor: Cursor? + private val mCursorPosition: Int + private val mExtraColumns: List<String> + @Override + override fun doGetExtra(columnName: String?): String? { + return try { + mCursor?.moveToPosition(mCursorPosition) + val columnIdx: Int = mCursor!!.getColumnIndex(columnName) + if (columnIdx < 0) null else mCursor.getString(columnIdx) + } catch (ex: RuntimeException) { + // all operations on cross-process cursors can throw random exceptions + Log.e(CursorBackedSuggestionExtras.Companion.TAG, "getExtra($columnName) failed, ", ex) + null + } + } + + @Override + public override fun doGetExtraColumnNames(): List<String> { + return mExtraColumns + } + + init { + mCursor = cursor + mCursorPosition = position + mExtraColumns = extraColumns + } +} diff --git a/src/com/android/quicksearchbox/DialogActivity.java b/src/com/android/quicksearchbox/DialogActivity.java deleted file mode 100644 index 276b688..0000000 --- a/src/com/android/quicksearchbox/DialogActivity.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.app.Activity; -import android.os.Bundle; -import android.view.View; -import android.view.Window; -import android.widget.FrameLayout; -import android.widget.TextView; - -/** - * Activity that looks like a dialog window. - */ -public abstract class DialogActivity extends Activity { - - protected TextView mTitleView; - protected FrameLayout mContentFrame; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getWindow().requestFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.dialog_activity); - mTitleView = (TextView) findViewById(R.id.alertTitle); - mContentFrame = (FrameLayout) findViewById(R.id.content); - } - - public void setHeading(int titleRes) { - mTitleView.setText(titleRes); - } - - public void setHeading(CharSequence title) { - mTitleView.setText(title); - } - - public void setDialogContent(int layoutRes) { - mContentFrame.removeAllViews(); - getLayoutInflater().inflate(layoutRes, mContentFrame); - } - - public void setDialogContent(View content) { - mContentFrame.removeAllViews(); - mContentFrame.addView(content); - } - - public View getDialogContent() { - if (mContentFrame.getChildCount() > 0) { - return mContentFrame.getChildAt(0); - } else { - return null; - } - } - -} diff --git a/src/com/android/quicksearchbox/DialogActivity.kt b/src/com/android/quicksearchbox/DialogActivity.kt new file mode 100644 index 0000000..ffbb376 --- /dev/null +++ b/src/com/android/quicksearchbox/DialogActivity.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.Activity +import android.os.Bundle +import android.view.View +import android.view.Window +import android.widget.FrameLayout +import android.widget.TextView + +/** Activity that looks like a dialog window. */ +abstract class DialogActivity : Activity() { + @JvmField protected var mTitleView: TextView? = null + + @JvmField protected var mContentFrame: FrameLayout? = null + + @Override + protected override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + getWindow().requestFeature(Window.FEATURE_NO_TITLE) + setContentView(R.layout.dialog_activity) + mTitleView = findViewById(R.id.alertTitle) as TextView? + mContentFrame = findViewById(R.id.content) as FrameLayout? + } + + fun setHeading(titleRes: Int) { + mTitleView?.setText(titleRes) + } + + fun setHeading(title: CharSequence?) { + mTitleView?.setText(title) + } + + fun setDialogContent(layoutRes: Int) { + mContentFrame?.removeAllViews() + getLayoutInflater().inflate(layoutRes, mContentFrame) + } + + fun setDialogContent(content: View?) { + mContentFrame?.removeAllViews() + mContentFrame?.addView(content) + } +} diff --git a/src/com/android/quicksearchbox/EventLogLogger.java b/src/com/android/quicksearchbox/EventLogLogger.java deleted file mode 100644 index 17ec0d1..0000000 --- a/src/com/android/quicksearchbox/EventLogLogger.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import android.content.Context; -import android.util.EventLog; - -import java.util.Collection; -import java.util.List; -import java.util.Random; - -/** - * Logs events to {@link EventLog}. - */ -public class EventLogLogger implements Logger { - - private static final char LIST_SEPARATOR = '|'; - - private final Context mContext; - - private final Config mConfig; - - private final String mPackageName; - - private final Random mRandom; - - public EventLogLogger(Context context, Config config) { - mContext = context; - mConfig = config; - mPackageName = mContext.getPackageName(); - mRandom = new Random(); - } - - protected Context getContext() { - return mContext; - } - - protected int getVersionCode() { - return QsbApplication.get(getContext()).getVersionCode(); - } - - protected Config getConfig() { - return mConfig; - } - - @Override - public void logStart(int onCreateLatency, int latency, String intentSource) { - // TODO: Add more info to startMethod - String startMethod = intentSource; - EventLogTags.writeQsbStart(mPackageName, getVersionCode(), startMethod, - latency, null, null, onCreateLatency); - } - - @Override - public void logSuggestionClick(long id, SuggestionCursor suggestionCursor, int clickType) { - String suggestions = getSuggestions(suggestionCursor); - int numChars = suggestionCursor.getUserQuery().length(); - EventLogTags.writeQsbClick(id, suggestions, null, numChars, - clickType); - } - - @Override - public void logSearch(int startMethod, int numChars) { - EventLogTags.writeQsbSearch(null, startMethod, numChars); - } - - @Override - public void logVoiceSearch() { - EventLogTags.writeQsbVoiceSearch(null); - } - - @Override - public void logExit(SuggestionCursor suggestionCursor, int numChars) { - String suggestions = getSuggestions(suggestionCursor); - EventLogTags.writeQsbExit(suggestions, numChars); - } - - @Override - public void logLatency(SourceResult result) { - } - - private String getSuggestions(SuggestionCursor cursor) { - StringBuilder sb = new StringBuilder(); - final int count = cursor == null ? 0 : cursor.getCount(); - for (int i = 0; i < count; i++) { - if (i > 0) sb.append(LIST_SEPARATOR); - cursor.moveTo(i); - String source = cursor.getSuggestionSource().getName(); - String type = cursor.getSuggestionLogType(); - if (type == null) type = ""; - String shortcut = cursor.isSuggestionShortcut() ? "shortcut" : ""; - sb.append(source).append(':').append(type).append(':').append(shortcut); - } - return sb.toString(); - } - -} diff --git a/src/com/android/quicksearchbox/EventLogLogger.kt b/src/com/android/quicksearchbox/EventLogLogger.kt new file mode 100644 index 0000000..a367b4e --- /dev/null +++ b/src/com/android/quicksearchbox/EventLogLogger.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.Context +import android.util.EventLog +import java.util.Random +import kotlin.text.StringBuilder + +/** Logs events to [EventLog]. */ +class EventLogLogger(context: Context?, config: Config) : Logger { + private val mContext: Context? + protected val config: Config + private val mPackageName: String + private val mRandom: Random + protected val context: Context? + get() = mContext + protected val versionCode: Long + get() = QsbApplication.get(context).versionCode + + @Override + override fun logStart(onCreateLatency: Int, latency: Int, intentSource: String?) { + // TODO: Add more info to startMethod + EventLogTags.writeQsbStart( + mPackageName, + versionCode.toInt(), + intentSource, + latency, + null, + null, + onCreateLatency + ) + } + + @Override + override fun logSuggestionClick( + suggestionId: Long, + suggestionCursor: SuggestionCursor?, + clickType: Int + ) { + val suggestions = getSuggestions(suggestionCursor) + val numChars: Int = suggestionCursor!!.userQuery!!.length + EventLogTags.writeQsbClick(suggestionId, suggestions, null, numChars, clickType) + } + + @Override + override fun logSearch(startMethod: Int, numChars: Int) { + EventLogTags.writeQsbSearch(null, startMethod, numChars) + } + + @Override + override fun logVoiceSearch() { + EventLogTags.writeQsbVoiceSearch(null) + } + + @Override + override fun logExit(suggestionCursor: SuggestionCursor?, numChars: Int) { + val suggestions = getSuggestions(suggestionCursor) + EventLogTags.writeQsbExit(suggestions, numChars) + } + + @Override override fun logLatency(result: SourceResult?) {} + + private fun getSuggestions(cursor: SuggestionCursor?): String { + val sb = StringBuilder() + val count = cursor?.count ?: 0 + for (i in 0 until count) { + if (i > 0) sb.append(LIST_SEPARATOR) + cursor!!.moveTo(i) + val source: String? = cursor.suggestionSource?.name + var type: String? = cursor.suggestionLogType + if (type == null) type = "" + val shortcut = if (cursor.isSuggestionShortcut) "shortcut" else "" + sb.append(source).append(":").append(type).append(":").append(shortcut) + } + return sb.toString() + } + + companion object { + private const val LIST_SEPARATOR = '|' + } + + init { + mContext = context + this.config = config + mPackageName = mContext!!.getPackageName() + mRandom = Random() + } +} diff --git a/src/com/android/quicksearchbox/Help.java b/src/com/android/quicksearchbox/Help.java deleted file mode 100644 index 9c86811..0000000 --- a/src/com/android/quicksearchbox/Help.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quicksearchbox; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -/** - * Handles app help. - */ -public class Help { - - private final Context mContext; - private final Config mConfig; - - public Help(Context context, Config config) { - mContext = context; - mConfig = config; - } - - public void addHelpMenuItem(Menu menu, String activityName) { - addHelpMenuItem(menu, activityName, false); - } - - public void addHelpMenuItem(Menu menu, String activityName, boolean showAsAction) { - Intent helpIntent = getHelpIntent(activityName); - if (helpIntent != null) { - MenuInflater inflater = new MenuInflater(mContext); - inflater.inflate(R.menu.help, menu); - MenuItem item = menu.findItem(R.id.menu_help); - item.setIntent(helpIntent); - if (showAsAction) { - item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - } - } - } - - private Intent getHelpIntent(String activityName) { - Uri helpUrl = mConfig.getHelpUrl(activityName); - if (helpUrl == null) return null; - return new Intent(Intent.ACTION_VIEW, helpUrl); - } - -} diff --git a/src/com/android/quicksearchbox/Help.kt b/src/com/android/quicksearchbox/Help.kt new file mode 100644 index 0000000..ec0a773 --- /dev/null +++ b/src/com/android/quicksearchbox/Help.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem + +/** Handles app help. */ +class Help(context: Context?, config: Config) { + private val mContext: Context? + private val mConfig: Config + fun addHelpMenuItem(menu: Menu, activityName: String?) { + addHelpMenuItem(menu, activityName, false) + } + + fun addHelpMenuItem(menu: Menu, activityName: String?, showAsAction: Boolean) { + val helpIntent: Intent? = getHelpIntent(activityName) + if (helpIntent != null) { + val inflater = MenuInflater(mContext) + inflater.inflate(R.menu.help, menu) + val item: MenuItem = menu.findItem(R.id.menu_help) + item.setIntent(helpIntent) + if (showAsAction) { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + } + } + } + + private fun getHelpIntent(activityName: String?): Intent? { + val helpUrl: Uri = mConfig.getHelpUrl(activityName) ?: return null + return Intent(Intent.ACTION_VIEW, helpUrl) + } + + init { + mContext = context + mConfig = config + } +} diff --git a/src/com/android/quicksearchbox/IconLoader.java b/src/com/android/quicksearchbox/IconLoader.java deleted file mode 100644 index 191ca33..0000000 --- a/src/com/android/quicksearchbox/IconLoader.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import com.android.quicksearchbox.util.NowOrLater; - -import android.content.ContentResolver; -import android.graphics.drawable.Drawable; -import android.net.Uri; - -/** - * Interface for icon loaders. - * - */ -public interface IconLoader { - - /** - * Gets a drawable given an ID. - * - * The ID could be just the string value of a resource id - * (e.g., "2130837524"), in which case we will try to retrieve a drawable from - * the provider's resources. If the ID is not an integer, it is - * treated as a Uri and opened with - * {@link ContentResolver#openOutputStream(android.net.Uri, String)}. - * - * All resources and URIs are read using the suggestion provider's context. - * - * @return a {@link NowOrLater} for retrieving the icon. If the ID is not formatted as expected, - * or no drawable can be found for the provided value, the value from this will be null. - * - * @param drawableId a string like "2130837524", - * "android.resource://com.android.alarmclock/2130837524", - * or "content://contacts/photos/253". - */ - NowOrLater<Drawable> getIcon(String drawableId); - - /** - * Converts a drawable ID to a Uri that can be used from other packages. - */ - Uri getIconUri(String drawableId); - -} diff --git a/src/com/android/quicksearchbox/IconLoader.kt b/src/com/android/quicksearchbox/IconLoader.kt new file mode 100644 index 0000000..0752c70 --- /dev/null +++ b/src/com/android/quicksearchbox/IconLoader.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.ContentResolver +import android.graphics.drawable.Drawable +import android.net.Uri +import com.android.quicksearchbox.util.NowOrLater + +/** Interface for icon loaders. */ +interface IconLoader { + /** + * Gets a drawable given an ID. + * + * The ID could be just the string value of a resource id (e.g., "2130837524"), in which case we + * will try to retrieve a drawable from the provider's resources. If the ID is not an integer, it + * is treated as a Uri and opened with [ContentResolver.openOutputStream]. + * + * All resources and URIs are read using the suggestion provider's context. + * + * @return a [NowOrLater] for retrieving the icon. If the ID is not formatted as expected, or no + * drawable can be found for the provided value, the value from this will be null. + * + * @param drawableId a string like "2130837524", + * "android.resource://com.android.alarmclock/2130837524", or "content://contacts/photos/253". + */ + fun getIcon(drawableId: String?): NowOrLater<Drawable?>? + + /** Converts a drawable ID to a Uri that can be used from other packages. */ + fun getIconUri(drawableId: String?): Uri? +} diff --git a/src/com/android/quicksearchbox/JsonBackedSuggestionExtras.java b/src/com/android/quicksearchbox/JsonBackedSuggestionExtras.java deleted file mode 100644 index 418a0b0..0000000 --- a/src/com/android/quicksearchbox/JsonBackedSuggestionExtras.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.util.Log; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; - -/** - * SuggestionExtras taking values from a {@link JSONObject}. - */ -public class JsonBackedSuggestionExtras implements SuggestionExtras { - private static final String TAG = "QSB.JsonBackedSuggestionExtras"; - - private final JSONObject mExtras; - private final Collection<String> mColumns; - - public JsonBackedSuggestionExtras(String json) throws JSONException { - mExtras = new JSONObject(json); - mColumns = new ArrayList<String>(mExtras.length()); - Iterator<String> it = mExtras.keys(); - while (it.hasNext()) { - mColumns.add(it.next()); - } - } - - public JsonBackedSuggestionExtras(SuggestionExtras extras) throws JSONException { - mExtras = new JSONObject(); - mColumns = extras.getExtraColumnNames(); - for (String column : extras.getExtraColumnNames()) { - String value = extras.getExtra(column); - mExtras.put(column, value == null ? JSONObject.NULL : value); - } - } - - public String getExtra(String columnName) { - try { - if (mExtras.isNull(columnName)) { - return null; - } else { - return mExtras.getString(columnName); - } - } catch (JSONException e) { - Log.w(TAG, "Could not extract JSON extra", e); - return null; - } - } - - public Collection<String> getExtraColumnNames() { - return mColumns; - } - - @Override - public String toString() { - return mExtras.toString(); - } - - public String toJsonString() { - return toString(); - } - -} diff --git a/src/com/android/quicksearchbox/JsonBackedSuggestionExtras.kt b/src/com/android/quicksearchbox/JsonBackedSuggestionExtras.kt new file mode 100644 index 0000000..0baed07 --- /dev/null +++ b/src/com/android/quicksearchbox/JsonBackedSuggestionExtras.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.util.Log +import java.util.ArrayList +import org.json.JSONException +import org.json.JSONObject + +/** SuggestionExtras taking values from a [JSONObject]. */ +class JsonBackedSuggestionExtras : SuggestionExtras { + private val mExtras: JSONObject + override val extraColumnNames: Collection<String> + + constructor(json: String?) { + mExtras = JSONObject(json!!) + extraColumnNames = ArrayList<String>(mExtras.length()) + val it: Iterator<String> = mExtras.keys() + while (it.hasNext()) { + extraColumnNames.add(it.next()) + } + } + + constructor(extras: SuggestionExtras) { + mExtras = JSONObject() + extraColumnNames = extras.extraColumnNames + for (column in extras.extraColumnNames) { + val value = extras.getExtra(column) + mExtras.put(column, value ?: JSONObject.NULL) + } + } + + override fun getExtra(columnName: String?): String? { + return try { + if (mExtras.isNull(columnName)) { + null + } else { + mExtras.getString(columnName!!) + } + } catch (e: JSONException) { + Log.w(TAG, "Could not extract JSON extra", e) + null + } + } + + @Override + override fun toString(): String { + return mExtras.toString() + } + + override fun toJsonString(): String? { + return toString() + } + + companion object { + private const val TAG = "QSB.JsonBackedSuggestionExtras" + } +} diff --git a/src/com/android/quicksearchbox/LatencyTracker.java b/src/com/android/quicksearchbox/LatencyTracker.java deleted file mode 100644 index 6c9472c..0000000 --- a/src/com/android/quicksearchbox/LatencyTracker.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import android.os.SystemClock; - -/** - * Tracks latency in wall-clock time. Since {@link #getLatency} returns an {@code int}, - * latencies over 2^31 ms (~ 25 days) cannot be measured. - * This class uses {@link SystemClock#uptimeMillis} which does not advance during deep sleep. - */ -public class LatencyTracker { - - /** - * Start time, in milliseconds as returned by {@link SystemClock#uptimeMillis}. - */ - private long mStartTime; - - /** - * Creates a new latency tracker and sets the start time. - */ - public LatencyTracker() { - mStartTime = SystemClock.uptimeMillis(); - } - - /** - * Resets the start time. - */ - public void reset() { - mStartTime = SystemClock.uptimeMillis(); - } - - /** - * Gets the number of milliseconds since the object was created, or {@link #reset} was called. - */ - public int getLatency() { - long now = SystemClock.uptimeMillis(); - return (int) (now - mStartTime); - } - -} diff --git a/src/com/android/quicksearchbox/LatencyTracker.kt b/src/com/android/quicksearchbox/LatencyTracker.kt new file mode 100644 index 0000000..53e7467 --- /dev/null +++ b/src/com/android/quicksearchbox/LatencyTracker.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.os.SystemClock + +/** + * Tracks latency in wall-clock time. Since [.getLatency] returns an `int`, latencies over 2^31 ms + * (~ 25 days) cannot be measured. This class uses [SystemClock.uptimeMillis] which does not advance + * during deep sleep. + */ +class LatencyTracker { + /** Start time, in milliseconds as returned by [SystemClock.uptimeMillis]. */ + private var mStartTime: Long + + /** Resets the start time. */ + fun reset() { + mStartTime = SystemClock.uptimeMillis() + } + + /** Gets the number of milliseconds since the object was created, or [.reset] was called. */ + val latency: Int + get() { + val now: Long = SystemClock.uptimeMillis() + return (now - mStartTime).toInt() + } + + /** Creates a new latency tracker and sets the start time. */ + init { + mStartTime = SystemClock.uptimeMillis() + } +} diff --git a/src/com/android/quicksearchbox/LevenshteinSuggestionFormatter.java b/src/com/android/quicksearchbox/LevenshteinSuggestionFormatter.java deleted file mode 100644 index dc12db1..0000000 --- a/src/com/android/quicksearchbox/LevenshteinSuggestionFormatter.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import com.android.quicksearchbox.util.LevenshteinDistance; -import com.android.quicksearchbox.util.LevenshteinDistance.Token; -import com.google.common.annotations.VisibleForTesting; - -import android.text.SpannableString; -import android.text.Spanned; -import android.util.Log; - -/** - * Suggestion formatter using the Levenshtein distance (minumum edit distance) to calculate the - * formatting. - */ -public class LevenshteinSuggestionFormatter extends SuggestionFormatter { - private static final boolean DBG = false; - private static final String TAG = "QSB.LevenshteinSuggestionFormatter"; - - public LevenshteinSuggestionFormatter(TextAppearanceFactory spanFactory) { - super(spanFactory); - } - - @Override - public Spanned formatSuggestion(String query, String suggestion) { - if (DBG) Log.d(TAG, "formatSuggestion('" + query + "', '" + suggestion + "')"); - query = normalizeQuery(query); - final Token[] queryTokens = tokenize(query); - final Token[] suggestionTokens = tokenize(suggestion); - final int[] matches = findMatches(queryTokens, suggestionTokens); - if (DBG){ - Log.d(TAG, "source = " + queryTokens); - Log.d(TAG, "target = " + suggestionTokens); - Log.d(TAG, "matches = " + matches); - } - final SpannableString str = new SpannableString(suggestion); - - final int matchesLen = matches.length; - for (int i = 0; i < matchesLen; ++i) { - final Token t = suggestionTokens[i]; - int sourceLen = 0; - int thisMatch = matches[i]; - if (thisMatch >= 0) { - sourceLen = queryTokens[thisMatch].length(); - } - applySuggestedTextStyle(str, t.mStart + sourceLen, t.mEnd); - applyQueryTextStyle(str, t.mStart, t.mStart + sourceLen); - } - - return str; - } - - private String normalizeQuery(String query) { - return query.toLowerCase(); - } - - /** - * Finds which tokens in the target match tokens in the source. - * - * @param source List of source tokens (i.e. user query) - * @param target List of target tokens (i.e. suggestion) - * @return The indices into source which target tokens correspond to. A non-negative value n at - * position i means that target token i matches source token n. A negative value means that - * the target token i does not match any source token. - */ - @VisibleForTesting - int[] findMatches(Token[] source, Token[] target) { - final LevenshteinDistance table = new LevenshteinDistance(source, target); - table.calculate(); - final int targetLen = target.length; - final int[] result = new int[targetLen]; - LevenshteinDistance.EditOperation[] ops = table.getTargetOperations(); - for (int i = 0; i < targetLen; ++i) { - if (ops[i].getType() == LevenshteinDistance.EDIT_UNCHANGED) { - result[i] = ops[i].getPosition(); - } else { - result[i] = -1; - } - } - return result; - } - - @VisibleForTesting - Token[] tokenize(final String seq) { - int pos = 0; - final int len = seq.length(); - final char[] chars = seq.toCharArray(); - // There can't be more tokens than characters, make an array that is large enough - Token[] tokens = new Token[len]; - int tokenCount = 0; - while (pos < len) { - while (pos < len && (chars[pos] == ' ' || chars[pos] == '\t')) { - pos++; - } - int start = pos; - while (pos < len && !(chars[pos] == ' ' || chars[pos] == '\t')) { - pos++; - } - int end = pos; - if (start != end) { - tokens[tokenCount++] = new Token(chars, start, end); - } - } - // Create a token array of the right size and return - Token[] ret = new Token[tokenCount]; - System.arraycopy(tokens, 0, ret, 0, tokenCount); - return ret; - } - -} diff --git a/src/com/android/quicksearchbox/LevenshteinSuggestionFormatter.kt b/src/com/android/quicksearchbox/LevenshteinSuggestionFormatter.kt new file mode 100644 index 0000000..de1cd22 --- /dev/null +++ b/src/com/android/quicksearchbox/LevenshteinSuggestionFormatter.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.text.SpannableString +import android.text.Spanned +import android.util.Log +import com.android.quicksearchbox.util.LevenshteinDistance +import com.android.quicksearchbox.util.LevenshteinDistance.Token +import com.google.common.annotations.VisibleForTesting +import java.util.Locale + +/** + * Suggestion formatter using the Levenshtein distance (minimum edit distance) to calculate the + * formatting. + */ +class LevenshteinSuggestionFormatter(spanFactory: TextAppearanceFactory?) : + SuggestionFormatter(spanFactory!!) { + @Override + override fun formatSuggestion(query: String?, suggestion: String?): Spanned { + var mQuery = query + if (DBG) Log.d(TAG, "formatSuggestion('$mQuery', '$suggestion')") + mQuery = normalizeQuery(mQuery) + val queryTokens: Array<Token?> = tokenize(mQuery) + val suggestionTokens: Array<Token?> = tokenize(suggestion) + val matches = findMatches(queryTokens, suggestionTokens) + if (DBG) { + Log.d(TAG, "source = $queryTokens") + Log.d(TAG, "target = $suggestionTokens") + Log.d(TAG, "matches = $matches") + } + val str = SpannableString(suggestion) + val matchesLen = matches.size + for (i in 0 until matchesLen) { + val t: Token? = suggestionTokens[i] + var sourceLen = 0 + val thisMatch = matches[i] + if (thisMatch >= 0) { + sourceLen = queryTokens[thisMatch]!!.length + } + applySuggestedTextStyle(str, t!!.mStart + sourceLen, t.mEnd) + applyQueryTextStyle(str, t.mStart, t.mStart + sourceLen) + } + return str + } + + private fun normalizeQuery(query: String?): String? { + return query?.lowercase(Locale.getDefault()) + } + + /** + * Finds which tokens in the target match tokens in the source. + * + * @param source List of source tokens (i.e. user query) + * @param target List of target tokens (i.e. suggestion) + * @return The indices into source which target tokens correspond to. A non-negative value n at + * position i means that target token i matches source token n. A negative value means that the + * target token i does not match any source token. + */ + @VisibleForTesting + fun findMatches(source: Array<Token?>?, target: Array<Token?>): IntArray { + val table = LevenshteinDistance(source, target) + table.calculate() + val targetLen = target.size + val result = IntArray(targetLen) + val ops: Array<LevenshteinDistance.EditOperation?> = table.targetOperations + for (i in 0 until targetLen) { + if (ops[i]!!.type == LevenshteinDistance.EDIT_UNCHANGED) { + result[i] = ops[i]!!.position + } else { + result[i] = -1 + } + } + return result + } + + @VisibleForTesting + fun tokenize(seq: String?): Array<Token?> { + var pos = 0 + val len: Int = seq!!.length + val chars = seq.toCharArray() + // There can't be more tokens than characters, make an array that is large enough + val tokens: Array<Token?> = arrayOfNulls<Token>(len) + var tokenCount = 0 + while (pos < len) { + while (pos < len && (chars[pos] == ' ' || chars[pos] == '\t')) { + pos++ + } + val start = pos + while (pos < len && !(chars[pos] == ' ' || chars[pos] == '\t')) { + pos++ + } + val end = pos + if (start != end) { + tokens[tokenCount++] = Token(chars, start, end) + } + } + // Create a token array of the right size and return + val ret: Array<Token?> = arrayOfNulls<Token>(tokenCount) + System.arraycopy(tokens, 0, ret, 0, tokenCount) + return ret + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.LevenshteinSuggestionFormatter" + } +} diff --git a/src/com/android/quicksearchbox/ListSuggestionCursor.java b/src/com/android/quicksearchbox/ListSuggestionCursor.java deleted file mode 100644 index 863be31..0000000 --- a/src/com/android/quicksearchbox/ListSuggestionCursor.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import com.google.common.annotations.VisibleForTesting; - -import android.database.DataSetObservable; -import android.database.DataSetObserver; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; - -/** - * A SuggestionCursor that is backed by a list of Suggestions. - */ -public class ListSuggestionCursor extends AbstractSuggestionCursorWrapper { - - private static final int DEFAULT_CAPACITY = 16; - - private final DataSetObservable mDataSetObservable = new DataSetObservable(); - - private final ArrayList<Entry> mSuggestions; - - private HashSet<String> mExtraColumns; - - private int mPos = 0; - - public ListSuggestionCursor(String userQuery) { - this(userQuery, DEFAULT_CAPACITY); - } - - @VisibleForTesting - public ListSuggestionCursor(String userQuery, Suggestion...suggestions) { - this(userQuery, suggestions.length); - for (Suggestion suggestion : suggestions) { - add(suggestion); - } - } - - public ListSuggestionCursor(String userQuery, int capacity) { - super(userQuery); - mSuggestions = new ArrayList<Entry>(capacity); - } - - /** - * Adds a suggestion from another suggestion cursor. - * - * @return {@code true} if the suggestion was added. - */ - public boolean add(Suggestion suggestion) { - mSuggestions.add(new Entry(suggestion)); - return true; - } - - public void close() { - mSuggestions.clear(); - } - - public int getPosition() { - return mPos; - } - - public void moveTo(int pos) { - mPos = pos; - } - - public boolean moveToNext() { - int size = mSuggestions.size(); - if (mPos >= size) { - // Already past the end - return false; - } - mPos++; - return mPos < size; - } - - public void removeRow() { - mSuggestions.remove(mPos); - } - - public void replaceRow(Suggestion suggestion) { - mSuggestions.set(mPos, new Entry(suggestion)); - } - - public int getCount() { - return mSuggestions.size(); - } - - @Override - protected Suggestion current() { - return mSuggestions.get(mPos).get(); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "{[" + getUserQuery() + "] " + mSuggestions + "}"; - } - - /** - * Register an observer that is called when changes happen to this data set. - * - * @param observer gets notified when the data set changes. - */ - public void registerDataSetObserver(DataSetObserver observer) { - mDataSetObservable.registerObserver(observer); - } - - /** - * Unregister an observer that has previously been registered with - * {@link #registerDataSetObserver(DataSetObserver)} - * - * @param observer the observer to unregister. - */ - public void unregisterDataSetObserver(DataSetObserver observer) { - mDataSetObservable.unregisterObserver(observer); - } - - protected void notifyDataSetChanged() { - mDataSetObservable.notifyChanged(); - } - - @Override - public SuggestionExtras getExtras() { - // override with caching to avoid re-parsing the extras - return mSuggestions.get(mPos).getExtras(); - } - - public Collection<String> getExtraColumns() { - if (mExtraColumns == null) { - mExtraColumns = new HashSet<String>(); - for (Entry e : mSuggestions) { - SuggestionExtras extras = e.getExtras(); - Collection<String> extraColumns = extras == null ? null - : extras.getExtraColumnNames(); - if (extraColumns != null) { - for (String column : extras.getExtraColumnNames()) { - mExtraColumns.add(column); - } - } - } - } - return mExtraColumns.isEmpty() ? null : mExtraColumns; - } - - /** - * This class exists purely to cache the suggestion extras. - */ - private static class Entry { - private final Suggestion mSuggestion; - private SuggestionExtras mExtras; - public Entry(Suggestion s) { - mSuggestion = s; - } - public Suggestion get() { - return mSuggestion; - } - public SuggestionExtras getExtras() { - if (mExtras == null) { - mExtras = mSuggestion.getExtras(); - } - return mExtras; - } - } - -} diff --git a/src/com/android/quicksearchbox/ListSuggestionCursor.kt b/src/com/android/quicksearchbox/ListSuggestionCursor.kt new file mode 100644 index 0000000..83f12a9 --- /dev/null +++ b/src/com/android/quicksearchbox/ListSuggestionCursor.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.database.DataSetObservable +import android.database.DataSetObserver +import com.google.common.annotations.VisibleForTesting +import kotlin.collections.ArrayList +import kotlin.collections.HashSet + +/** A SuggestionCursor that is backed by a list of Suggestions. */ +open class ListSuggestionCursor(userQuery: String?, capacity: Int) : + AbstractSuggestionCursorWrapper(userQuery!!) { + private val mDataSetObservable: DataSetObservable = DataSetObservable() + + private val mSuggestions: ArrayList<Entry> + + private var mExtraColumns: HashSet<String>? = null + + override var position = 0 + + constructor(userQuery: String?) : this(userQuery, DEFAULT_CAPACITY) + + @VisibleForTesting + constructor( + userQuery: String?, + vararg suggestions: Suggestion? + ) : this(userQuery, suggestions.size) { + for (suggestion in suggestions) { + add(suggestion!!) + } + } + + /** + * Adds a suggestion from another suggestion cursor. + * + * @return `true` if the suggestion was added. + */ + open fun add(suggestion: Suggestion): Boolean { + mSuggestions.add(Entry(suggestion)) + return true + } + + override fun close() { + mSuggestions.clear() + } + + override fun moveTo(pos: Int) { + position = pos + } + + override fun moveToNext(): Boolean { + val size: Int = mSuggestions.size + if (position >= size) { + // Already past the end + return false + } + position++ + return position < size + } + + fun removeRow() { + mSuggestions.removeAt(position) + } + + fun replaceRow(suggestion: Suggestion) { + mSuggestions.set(position, Entry(suggestion)) + } + + override val count: Int + get() = mSuggestions.size + + @Override + override fun current(): Suggestion { + return mSuggestions.get(position).get() + } + + @Override + override fun toString(): String { + return this::class.simpleName.toString() + "{[" + userQuery + "] " + mSuggestions + "}" + } + + /** + * Register an observer that is called when changes happen to this data set. + * + * @param observer gets notified when the data set changes. + */ + override fun registerDataSetObserver(observer: DataSetObserver?) { + mDataSetObservable.registerObserver(observer) + } + + /** + * Unregister an observer that has previously been registered with [.registerDataSetObserver] + * + * @param observer the observer to unregister. + */ + override fun unregisterDataSetObserver(observer: DataSetObserver?) { + mDataSetObservable.unregisterObserver(observer) + } + + protected fun notifyDataSetChanged() { + mDataSetObservable.notifyChanged() + } + + // override with caching to avoid re-parsing the extras + @get:Override + override val extras: SuggestionExtras? + // override with caching to avoid re-parsing the extras + get() = mSuggestions.get(position).getExtras() + + override val extraColumns: Collection<String>? + get() { + if (mExtraColumns == null) { + mExtraColumns = HashSet<String>() + for (e in mSuggestions) { + val extras: SuggestionExtras? = e.getExtras() + val extraColumns: Collection<String>? = + if (extras == null) null else extras.extraColumnNames + if (extraColumns != null) { + for (column in extras!!.extraColumnNames) { + mExtraColumns?.add(column) + } + } + } + } + return if (mExtraColumns!!.isEmpty()) null else mExtraColumns + } + + /** This class exists purely to cache the suggestion extras. */ + private class Entry(private val mSuggestion: Suggestion) { + private var mExtras: SuggestionExtras? = null + fun get(): Suggestion { + return mSuggestion + } + + fun getExtras(): SuggestionExtras? { + if (mExtras == null) { + mExtras = mSuggestion.extras + } + return mExtras + } + } + + companion object { + private const val DEFAULT_CAPACITY = 16 + } + + init { + mSuggestions = ArrayList<Entry>(capacity) + } +} diff --git a/src/com/android/quicksearchbox/ListSuggestionCursorNoDuplicates.java b/src/com/android/quicksearchbox/ListSuggestionCursorNoDuplicates.java deleted file mode 100644 index 48c302c..0000000 --- a/src/com/android/quicksearchbox/ListSuggestionCursorNoDuplicates.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import android.util.Log; - -import java.util.HashSet; - -/** - * A SuggestionCursor that is backed by a list of SuggestionPosition objects - * and doesn't allow duplicate suggestions. - */ -public class ListSuggestionCursorNoDuplicates extends ListSuggestionCursor { - - private static final boolean DBG = false; - private static final String TAG = "QSB.ListSuggestionCursorNoDuplicates"; - - private final HashSet<String> mSuggestionKeys; - - public ListSuggestionCursorNoDuplicates(String userQuery) { - super(userQuery); - mSuggestionKeys = new HashSet<String>(); - } - - @Override - public boolean add(Suggestion suggestion) { - String key = SuggestionUtils.getSuggestionKey(suggestion); - if (mSuggestionKeys.add(key)) { - return super.add(suggestion); - } else { - if (DBG) Log.d(TAG, "Rejecting duplicate " + key); - return false; - } - } - -} diff --git a/src/com/android/quicksearchbox/ListSuggestionCursorNoDuplicates.kt b/src/com/android/quicksearchbox/ListSuggestionCursorNoDuplicates.kt new file mode 100644 index 0000000..2d2ca6c --- /dev/null +++ b/src/com/android/quicksearchbox/ListSuggestionCursorNoDuplicates.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.util.Log + +/** + * A SuggestionCursor that is backed by a list of SuggestionPosition objects and doesn't allow + * duplicate suggestions. + */ +class ListSuggestionCursorNoDuplicates(userQuery: String?) : ListSuggestionCursor(userQuery) { + private val mSuggestionKeys: HashSet<String> + + @Override + override fun add(suggestion: Suggestion): Boolean { + val key = SuggestionUtils.getSuggestionKey(suggestion) + return if (mSuggestionKeys.add(key)) { + super.add(suggestion) + } else { + if (DBG) Log.d(TAG, "Rejecting duplicate $key") + false + } + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.ListSuggestionCursorNoDuplicates" + } + + init { + mSuggestionKeys = HashSet<String>() + } +} diff --git a/src/com/android/quicksearchbox/Logger.java b/src/com/android/quicksearchbox/Logger.java deleted file mode 100644 index 40ff606..0000000 --- a/src/com/android/quicksearchbox/Logger.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - - - -/** - * Interface for logging implementations. - */ -public interface Logger { - - public static final int SEARCH_METHOD_BUTTON = 0; - public static final int SEARCH_METHOD_KEYBOARD = 1; - - public static final int SUGGESTION_CLICK_TYPE_LAUNCH = 0; - public static final int SUGGESTION_CLICK_TYPE_REFINE = 1; - public static final int SUGGESTION_CLICK_TYPE_QUICK_CONTACT = 2; - - /** - * Called when QSB has started. - * - * @param latency User-visible start-up latency in milliseconds. - */ - void logStart(int onCreateLatency, int latency, String intentSource); - - /** - * Called when a suggestion is clicked. - * - * @param suggestionId Suggestion ID; 0-based position of the suggestion in the UI if the list - * is flat. - * @param suggestionCursor all the suggestions shown in the UI. - * @param clickType One of the SUGGESTION_CLICK_TYPE constants. - */ - void logSuggestionClick(long suggestionId, SuggestionCursor suggestionCursor, int clickType); - - /** - * The user launched a search. - * - * @param startMethod One of {@link #SEARCH_METHOD_BUTTON} or {@link #SEARCH_METHOD_KEYBOARD}. - * @param numChars The number of characters in the query. - */ - void logSearch(int startMethod, int numChars); - - /** - * The user launched a voice search. - */ - void logVoiceSearch(); - - /** - * The user left QSB without performing any action (click suggestions, search or voice search). - * - * @param suggestionCursor all the suggestions shown in the UI when the user left - * @param numChars The number of characters in the query typed when the user left. - */ - void logExit(SuggestionCursor suggestionCursor, int numChars); - - /** - * Logs the latency of a suggestion query to a specific source. - * - * @param result The result of the query. - */ - void logLatency(SourceResult result); - -} diff --git a/src/com/android/quicksearchbox/Logger.kt b/src/com/android/quicksearchbox/Logger.kt new file mode 100644 index 0000000..8de5ce7 --- /dev/null +++ b/src/com/android/quicksearchbox/Logger.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +/** Interface for logging implementations. */ +interface Logger { + /** + * Called when QSB has started. + * + * @param latency User-visible start-up latency in milliseconds. + */ + fun logStart(onCreateLatency: Int, latency: Int, intentSource: String?) + + /** + * Called when a suggestion is clicked. + * + * @param suggestionId Suggestion ID; 0-based position of the suggestion in the UI if the list is + * flat. + * @param suggestionCursor all the suggestions shown in the UI. + * @param clickType One of the SUGGESTION_CLICK_TYPE constants. + */ + fun logSuggestionClick(suggestionId: Long, suggestionCursor: SuggestionCursor?, clickType: Int) + + /** + * The user launched a search. + * + * @param startMethod One of [.SEARCH_METHOD_BUTTON] or [.SEARCH_METHOD_KEYBOARD]. + * @param numChars The number of characters in the query. + */ + fun logSearch(startMethod: Int, numChars: Int) + + /** The user launched a voice search. */ + fun logVoiceSearch() + + /** + * The user left QSB without performing any action (click suggestions, search or voice search). + * + * @param suggestionCursor all the suggestions shown in the UI when the user left + * @param numChars The number of characters in the query typed when the user left. + */ + fun logExit(suggestionCursor: SuggestionCursor?, numChars: Int) + + /** + * Logs the latency of a suggestion query to a specific source. + * + * @param result The result of the query. + */ + fun logLatency(result: SourceResult?) + + companion object { + const val SEARCH_METHOD_BUTTON = 0 + const val SEARCH_METHOD_KEYBOARD = 1 + const val SUGGESTION_CLICK_TYPE_LAUNCH = 0 + const val SUGGESTION_CLICK_TYPE_REFINE = 1 + const val SUGGESTION_CLICK_TYPE_QUICK_CONTACT = 2 + } +} diff --git a/src/com/android/quicksearchbox/PackageIconLoader.java b/src/com/android/quicksearchbox/PackageIconLoader.java deleted file mode 100644 index a572d3c..0000000 --- a/src/com/android/quicksearchbox/PackageIconLoader.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import com.android.quicksearchbox.util.CachedLater; -import com.android.quicksearchbox.util.NamedTask; -import com.android.quicksearchbox.util.NamedTaskExecutor; -import com.android.quicksearchbox.util.Now; -import com.android.quicksearchbox.util.NowOrLater; -import com.android.quicksearchbox.util.Util; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Handler; -import android.text.TextUtils; -import android.util.Log; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -/** - * Loads icons from other packages. - * - * Code partly stolen from {@link ContentResolver} and android.app.SuggestionsAdapter. - */ -public class PackageIconLoader implements IconLoader { - - private static final boolean DBG = false; - private static final String TAG = "QSB.PackageIconLoader"; - - private final Context mContext; - - private final String mPackageName; - - private Context mPackageContext; - - private final Handler mUiThread; - - private final NamedTaskExecutor mIconLoaderExecutor; - - /** - * Creates a new icon loader. - * - * @param context The QSB application context. - * @param packageName The name of the package from which the icons will be loaded. - * Resource IDs without an explicit package will be resolved against the package - * of this context. - */ - public PackageIconLoader(Context context, String packageName, Handler uiThread, - NamedTaskExecutor iconLoaderExecutor) { - mContext = context; - mPackageName = packageName; - mUiThread = uiThread; - mIconLoaderExecutor = iconLoaderExecutor; - } - - private boolean ensurePackageContext() { - if (mPackageContext == null) { - try { - mPackageContext = mContext.createPackageContext(mPackageName, - Context.CONTEXT_RESTRICTED); - } catch (PackageManager.NameNotFoundException ex) { - // This should only happen if the app has just be uninstalled - Log.e(TAG, "Application not found " + mPackageName); - return false; - } - } - return true; - } - - public NowOrLater<Drawable> getIcon(final String drawableId) { - if (DBG) Log.d(TAG, "getIcon(" + drawableId + ")"); - if (TextUtils.isEmpty(drawableId) || "0".equals(drawableId)) { - return new Now<Drawable>(null); - } - if (!ensurePackageContext()) { - return new Now<Drawable>(null); - } - NowOrLater<Drawable> drawable; - try { - // First, see if it's just an integer - int resourceId = Integer.parseInt(drawableId); - // If so, find it by resource ID - Drawable icon = mPackageContext.getResources().getDrawable(resourceId); - drawable = new Now<Drawable>(icon); - } catch (NumberFormatException nfe) { - // It's not an integer, use it as a URI - Uri uri = Uri.parse(drawableId); - if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) { - // load all resources synchronously, to reduce UI flickering - drawable = new Now<Drawable>(getDrawable(uri)); - } else { - drawable = new IconLaterTask(uri); - } - } catch (Resources.NotFoundException nfe) { - // It was an integer, but it couldn't be found, bail out - Log.w(TAG, "Icon resource not found: " + drawableId); - drawable = new Now<Drawable>(null); - } - return drawable; - } - - public Uri getIconUri(String drawableId) { - if (TextUtils.isEmpty(drawableId) || "0".equals(drawableId)) { - return null; - } - if (!ensurePackageContext()) return null; - try { - int resourceId = Integer.parseInt(drawableId); - return Util.getResourceUri(mPackageContext, resourceId); - } catch (NumberFormatException nfe) { - return Uri.parse(drawableId); - } - } - - /** - * Gets a drawable by URI. - * - * @return A drawable, or {@code null} if the drawable could not be loaded. - */ - private Drawable getDrawable(Uri uri) { - try { - String scheme = uri.getScheme(); - if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { - // Load drawables through Resources, to get the source density information - OpenResourceIdResult r = getResourceId(uri); - try { - return r.r.getDrawable(r.id); - } catch (Resources.NotFoundException ex) { - throw new FileNotFoundException("Resource does not exist: " + uri); - } - } else { - // Let the ContentResolver handle content and file URIs. - InputStream stream = mPackageContext.getContentResolver().openInputStream(uri); - if (stream == null) { - throw new FileNotFoundException("Failed to open " + uri); - } - try { - return Drawable.createFromStream(stream, null); - } finally { - try { - stream.close(); - } catch (IOException ex) { - Log.e(TAG, "Error closing icon stream for " + uri, ex); - } - } - } - } catch (FileNotFoundException fnfe) { - Log.w(TAG, "Icon not found: " + uri + ", " + fnfe.getMessage()); - return null; - } - } - - /** - * A resource identified by the {@link Resources} that contains it, and a resource id. - */ - private class OpenResourceIdResult { - public Resources r; - public int id; - } - - /** - * Resolves an android.resource URI to a {@link Resources} and a resource id. - */ - private OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException { - String authority = uri.getAuthority(); - Resources r; - if (TextUtils.isEmpty(authority)) { - throw new FileNotFoundException("No authority: " + uri); - } else { - try { - r = mPackageContext.getPackageManager().getResourcesForApplication(authority); - } catch (NameNotFoundException ex) { - throw new FileNotFoundException("Failed to get resources: " + ex); - } - } - List<String> path = uri.getPathSegments(); - if (path == null) { - throw new FileNotFoundException("No path: " + uri); - } - int len = path.size(); - int id; - if (len == 1) { - try { - id = Integer.parseInt(path.get(0)); - } catch (NumberFormatException e) { - throw new FileNotFoundException("Single path segment is not a resource ID: " + uri); - } - } else if (len == 2) { - id = r.getIdentifier(path.get(1), path.get(0), authority); - } else { - throw new FileNotFoundException("More than two path segments: " + uri); - } - if (id == 0) { - throw new FileNotFoundException("No resource found for: " + uri); - } - OpenResourceIdResult res = new OpenResourceIdResult(); - res.r = r; - res.id = id; - return res; - } - - private class IconLaterTask extends CachedLater<Drawable> implements NamedTask { - private final Uri mUri; - - public IconLaterTask(Uri iconUri) { - mUri = iconUri; - } - - @Override - protected void create() { - mIconLoaderExecutor.execute(this); - } - - @Override - public void run() { - final Drawable icon = getIcon(); - mUiThread.post(new Runnable(){ - public void run() { - store(icon); - }}); - } - - @Override - public String getName() { - return mPackageName; - } - - private Drawable getIcon() { - try { - return getDrawable(mUri); - } catch (Throwable t) { - // we're making a call into another package, which could throw any exception. - // Make sure it doesn't crash QSB - Log.e(TAG, "Failed to load icon " + mUri, t); - return null; - } - } - } -} diff --git a/src/com/android/quicksearchbox/PackageIconLoader.kt b/src/com/android/quicksearchbox/PackageIconLoader.kt new file mode 100644 index 0000000..530d1b1 --- /dev/null +++ b/src/com/android/quicksearchbox/PackageIconLoader.kt @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.ContentResolver +import android.content.Context +import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException +import android.content.res.Resources +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Handler +import android.text.TextUtils +import android.util.Log +import androidx.core.content.ContextCompat +import com.android.quicksearchbox.util.* +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream + +/** + * Loads icons from other packages. + * + * Code partly stolen from [ContentResolver] and android.app.SuggestionsAdapter. + */ +class PackageIconLoader( + context: Context?, + packageName: String?, + uiThread: Handler?, + iconLoaderExecutor: NamedTaskExecutor +) : IconLoader { + + private val mContext: Context? + + private val mPackageName: String? + + private var mPackageContext: Context? = null + + private val mUiThread: Handler? + + private val mIconLoaderExecutor: NamedTaskExecutor + + private fun ensurePackageContext(): Boolean { + if (mPackageContext == null) { + mPackageContext = + try { + mContext?.createPackageContext(mPackageName, Context.CONTEXT_RESTRICTED) + } catch (ex: PackageManager.NameNotFoundException) { + // This should only happen if the app has just be uninstalled + Log.e(TAG, "Application not found " + mPackageName) + return false + } + } + return true + } + + override fun getIcon(drawableId: String?): NowOrLater<Drawable?>? { + if (DBG) Log.d(TAG, "getIcon($drawableId)") + if (TextUtils.isEmpty(drawableId) || "0" == drawableId) { + return Now<Drawable>(null) + } + if (!ensurePackageContext()) { + return Now<Drawable>(null) + } + var drawable: NowOrLater<Drawable?>? + try { + // First, see if it's just an integer + val resourceId: Int = drawableId!!.toInt() + // If so, find it by resource ID + val icon: Drawable? = ContextCompat.getDrawable(mPackageContext!!, resourceId) + drawable = Now(icon) + } catch (nfe: NumberFormatException) { + // It's not an integer, use it as a URI + val uri: Uri = Uri.parse(drawableId) + if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) { + // load all resources synchronously, to reduce UI flickering + drawable = Now(getDrawable(uri)) + } else { + drawable = IconLaterTask(uri) + } + } catch (nfe: Resources.NotFoundException) { + // It was an integer, but it couldn't be found, bail out + Log.w(TAG, "Icon resource not found: $drawableId") + drawable = Now(null) + } + return drawable + } + + override fun getIconUri(drawableId: String?): Uri? { + if (TextUtils.isEmpty(drawableId) || "0" == drawableId) { + return null + } + return if (!ensurePackageContext()) null + else + try { + val resourceId: Int = drawableId!!.toInt() + Util.getResourceUri(mPackageContext, resourceId) + } catch (nfe: NumberFormatException) { + Uri.parse(drawableId) + } + } + + /** + * Gets a drawable by URI. + * + * @return A drawable, or `null` if the drawable could not be loaded. + */ + private fun getDrawable(uri: Uri): Drawable? { + return try { + val scheme: String? = uri.getScheme() + if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { + // Load drawables through Resources, to get the source density information + val r: OpenResourceIdResult = getResourceId(uri) + try { + ContextCompat.getDrawable(mPackageContext!!, r.id) + } catch (ex: Resources.NotFoundException) { + throw FileNotFoundException("Resource does not exist: $uri") + } + } else { + // Let the ContentResolver handle content and file URIs. + val stream: InputStream = + mPackageContext!!.getContentResolver().openInputStream(uri) + ?: throw FileNotFoundException("Failed to open $uri") + try { + Drawable.createFromStream(stream, null) + } finally { + try { + stream.close() + } catch (ex: IOException) { + Log.e(TAG, "Error closing icon stream for $uri", ex) + } + } + } + } catch (fnfe: FileNotFoundException) { + Log.w(TAG, "Icon not found: " + uri + ", " + fnfe.message) + null + } + } + + /** A resource identified by the [Resources] that contains it, and a resource id. */ + private inner class OpenResourceIdResult { + @JvmField var r: Resources? = null + + @JvmField var id = 0 + } + + /** Resolves an android.resource URI to a [Resources] and a resource id. */ + @Throws(FileNotFoundException::class) + private fun getResourceId(uri: Uri): OpenResourceIdResult { + val authority: String? = uri.getAuthority() + val r: Resources? = + if (TextUtils.isEmpty(authority)) { + throw FileNotFoundException("No authority: $uri") + } else { + try { + mPackageContext?.getPackageManager()?.getResourcesForApplication(authority!!) + } catch (ex: NameNotFoundException) { + throw FileNotFoundException("Failed to get resources: $ex") + } + } + val path: List<String> = uri.getPathSegments() ?: throw FileNotFoundException("No path: $uri") + val id: Int = + when (path.size) { + 1 -> { + try { + Integer.parseInt(path[0]) + } catch (e: NumberFormatException) { + throw FileNotFoundException("Single path segment is not a resource ID: $uri") + } + } + 2 -> { + r!!.getIdentifier(path[1], path[0], authority) + } + else -> { + throw FileNotFoundException("More than two path segments: $uri") + } + } + if (id == 0) { + throw FileNotFoundException("No resource found for: $uri") + } + val res = OpenResourceIdResult() + res.r = r + res.id = id + return res + } + + private inner class IconLaterTask(iconUri: Uri) : CachedLater<Drawable?>(), NamedTask { + private val mUri: Uri + + @Override + override fun create() { + mIconLoaderExecutor.execute(this) + } + + @Override + override fun run() { + val icon: Drawable? = icon + mUiThread?.post( + object : Runnable { + override fun run() { + store(icon) + } + } + ) + } + + @get:Override + override val name: String? + get() = mPackageName + + // we're making a call into another package, which could throw any exception. + // Make sure it doesn't crash QSB + private val icon: Drawable? + get() = + try { + getDrawable(mUri) + } catch (t: Throwable) { + // we're making a call into another package, which could throw any exception. + // Make sure it doesn't crash QSB + Log.e(TAG, "Failed to load icon $mUri", t) + null + } + + init { + mUri = iconUri + } + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.PackageIconLoader" + } + + /** + * Creates a new icon loader. + * + * @param context The QSB application context. + * @param packageName The name of the package from which the icons will be loaded. + * ``` + * Resource IDs without an explicit package will be resolved against the package + * of this context. + * ``` + */ + init { + mContext = context + mPackageName = packageName + mUiThread = uiThread + mIconLoaderExecutor = iconLoaderExecutor + } +} diff --git a/src/com/android/quicksearchbox/QsbApplication.java b/src/com/android/quicksearchbox/QsbApplication.java deleted file mode 100644 index b3bccd3..0000000 --- a/src/com/android/quicksearchbox/QsbApplication.java +++ /dev/null @@ -1,364 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.os.Process; -import android.view.ContextThemeWrapper; - -import com.android.quicksearchbox.google.GoogleSource; -import com.android.quicksearchbox.google.GoogleSuggestClient; -import com.android.quicksearchbox.google.SearchBaseUrlHelper; -import com.android.quicksearchbox.ui.DefaultSuggestionViewFactory; -import com.android.quicksearchbox.ui.SuggestionViewFactory; -import com.android.quicksearchbox.util.Factory; -import com.android.quicksearchbox.util.HttpHelper; -import com.android.quicksearchbox.util.JavaNetHttpHelper; -import com.android.quicksearchbox.util.NamedTaskExecutor; -import com.android.quicksearchbox.util.PerNameExecutor; -import com.android.quicksearchbox.util.PriorityThreadFactory; -import com.android.quicksearchbox.util.SingleThreadNamedTaskExecutor; -import com.google.common.util.concurrent.ThreadFactoryBuilder; - -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; - -public class QsbApplication { - private final Context mContext; - - private int mVersionCode; - private Handler mUiThreadHandler; - private Config mConfig; - private SearchSettings mSettings; - private NamedTaskExecutor mSourceTaskExecutor; - private ThreadFactory mQueryThreadFactory; - private SuggestionsProvider mSuggestionsProvider; - private SuggestionViewFactory mSuggestionViewFactory; - private GoogleSource mGoogleSource; - private VoiceSearch mVoiceSearch; - private Logger mLogger; - private SuggestionFormatter mSuggestionFormatter; - private TextAppearanceFactory mTextAppearanceFactory; - private NamedTaskExecutor mIconLoaderExecutor; - private HttpHelper mHttpHelper; - private SearchBaseUrlHelper mSearchBaseUrlHelper; - - public QsbApplication(Context context) { - // the application context does not use the theme from the <application> tag - mContext = new ContextThemeWrapper(context, R.style.Theme_QuickSearchBox); - } - - public static boolean isFroyoOrLater() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO; - } - - public static boolean isHoneycombOrLater() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; - } - - public static QsbApplication get(Context context) { - return ((QsbApplicationWrapper) context.getApplicationContext()).getApp(); - } - - protected Context getContext() { - return mContext; - } - - public int getVersionCode() { - if (mVersionCode == 0) { - try { - PackageManager pm = getContext().getPackageManager(); - PackageInfo pkgInfo = pm.getPackageInfo(getContext().getPackageName(), 0); - mVersionCode = pkgInfo.versionCode; - } catch (PackageManager.NameNotFoundException ex) { - // The current package should always exist, how else could we - // run code from it? - throw new RuntimeException(ex); - } - } - return mVersionCode; - } - - protected void checkThread() { - if (Looper.myLooper() != Looper.getMainLooper()) { - throw new IllegalStateException("Accessed Application object from thread " - + Thread.currentThread().getName()); - } - } - - protected void close() { - checkThread(); - if (mConfig != null) { - mConfig.close(); - mConfig = null; - } - if (mSuggestionsProvider != null) { - mSuggestionsProvider.close(); - mSuggestionsProvider = null; - } - } - - public synchronized Handler getMainThreadHandler() { - if (mUiThreadHandler == null) { - mUiThreadHandler = new Handler(Looper.getMainLooper()); - } - return mUiThreadHandler; - } - - public void runOnUiThread(Runnable action) { - getMainThreadHandler().post(action); - } - - public synchronized NamedTaskExecutor getIconLoaderExecutor() { - if (mIconLoaderExecutor == null) { - mIconLoaderExecutor = createIconLoaderExecutor(); - } - return mIconLoaderExecutor; - } - - protected NamedTaskExecutor createIconLoaderExecutor() { - ThreadFactory iconThreadFactory = new PriorityThreadFactory( - Process.THREAD_PRIORITY_BACKGROUND); - return new PerNameExecutor(SingleThreadNamedTaskExecutor.factory(iconThreadFactory)); - } - - /** - * Indicates that construction of the QSB UI is now complete. - */ - public void onStartupComplete() { - } - - /** - * Gets the QSB configuration object. - * May be called from any thread. - */ - public synchronized Config getConfig() { - if (mConfig == null) { - mConfig = createConfig(); - } - return mConfig; - } - - protected Config createConfig() { - return new Config(getContext()); - } - - public synchronized SearchSettings getSettings() { - if (mSettings == null) { - mSettings = createSettings(); - mSettings.upgradeSettingsIfNeeded(); - } - return mSettings; - } - - protected SearchSettings createSettings() { - return new SearchSettingsImpl(getContext(), getConfig()); - } - - protected Factory<Executor> createExecutorFactory(final int numThreads) { - final ThreadFactory threadFactory = getQueryThreadFactory(); - return new Factory<Executor>() { - @Override - public Executor create() { - return Executors.newFixedThreadPool(numThreads, threadFactory); - } - }; - } - - /** - /** - * Gets the source task executor. - * May only be called from the main thread. - */ - public NamedTaskExecutor getSourceTaskExecutor() { - checkThread(); - if (mSourceTaskExecutor == null) { - mSourceTaskExecutor = createSourceTaskExecutor(); - } - return mSourceTaskExecutor; - } - - protected NamedTaskExecutor createSourceTaskExecutor() { - ThreadFactory queryThreadFactory = getQueryThreadFactory(); - return new PerNameExecutor(SingleThreadNamedTaskExecutor.factory(queryThreadFactory)); - } - - /** - * Gets the query thread factory. - * May only be called from the main thread. - */ - protected ThreadFactory getQueryThreadFactory() { - checkThread(); - if (mQueryThreadFactory == null) { - mQueryThreadFactory = createQueryThreadFactory(); - } - return mQueryThreadFactory; - } - - protected ThreadFactory createQueryThreadFactory() { - String nameFormat = "QSB #%d"; - int priority = getConfig().getQueryThreadPriority(); - return new ThreadFactoryBuilder() - .setNameFormat(nameFormat) - .setThreadFactory(new PriorityThreadFactory(priority)) - .build(); - } - - /** - * Gets the suggestion provider. - * - * May only be called from the main thread. - */ - protected SuggestionsProvider getSuggestionsProvider() { - checkThread(); - if (mSuggestionsProvider == null) { - mSuggestionsProvider = createSuggestionsProvider(); - } - return mSuggestionsProvider; - } - - protected SuggestionsProvider createSuggestionsProvider() { - return new SuggestionsProviderImpl(getConfig(), - getSourceTaskExecutor(), - getMainThreadHandler(), - getLogger()); - } - - /** - * Gets the default suggestion view factory. - * May only be called from the main thread. - */ - public SuggestionViewFactory getSuggestionViewFactory() { - checkThread(); - if (mSuggestionViewFactory == null) { - mSuggestionViewFactory = createSuggestionViewFactory(); - } - return mSuggestionViewFactory; - } - - protected SuggestionViewFactory createSuggestionViewFactory() { - return new DefaultSuggestionViewFactory(getContext()); - } - - /** - * Gets the Google source. - * May only be called from the main thread. - */ - public GoogleSource getGoogleSource() { - checkThread(); - if (mGoogleSource == null) { - mGoogleSource = createGoogleSource(); - } - return mGoogleSource; - } - - protected GoogleSource createGoogleSource() { - return new GoogleSuggestClient(getContext(), getMainThreadHandler(), - getIconLoaderExecutor(), getConfig()); - } - - /** - * Gets Voice Search utilities. - */ - public VoiceSearch getVoiceSearch() { - checkThread(); - if (mVoiceSearch == null) { - mVoiceSearch = createVoiceSearch(); - } - return mVoiceSearch; - } - - protected VoiceSearch createVoiceSearch() { - return new VoiceSearch(getContext()); - } - - /** - * Gets the event logger. - * May only be called from the main thread. - */ - public Logger getLogger() { - checkThread(); - if (mLogger == null) { - mLogger = createLogger(); - } - return mLogger; - } - - protected Logger createLogger() { - return new EventLogLogger(getContext(), getConfig()); - } - - public SuggestionFormatter getSuggestionFormatter() { - if (mSuggestionFormatter == null) { - mSuggestionFormatter = createSuggestionFormatter(); - } - return mSuggestionFormatter; - } - - protected SuggestionFormatter createSuggestionFormatter() { - return new LevenshteinSuggestionFormatter(getTextAppearanceFactory()); - } - - public TextAppearanceFactory getTextAppearanceFactory() { - if (mTextAppearanceFactory == null) { - mTextAppearanceFactory = createTextAppearanceFactory(); - } - return mTextAppearanceFactory; - } - - protected TextAppearanceFactory createTextAppearanceFactory() { - return new TextAppearanceFactory(getContext()); - } - - public synchronized HttpHelper getHttpHelper() { - if (mHttpHelper == null) { - mHttpHelper = createHttpHelper(); - } - return mHttpHelper; - } - - protected HttpHelper createHttpHelper() { - return new JavaNetHttpHelper( - new JavaNetHttpHelper.PassThroughRewriter(), - getConfig().getUserAgent()); - } - - public synchronized SearchBaseUrlHelper getSearchBaseUrlHelper() { - if (mSearchBaseUrlHelper == null) { - mSearchBaseUrlHelper = createSearchBaseUrlHelper(); - } - - return mSearchBaseUrlHelper; - } - - protected SearchBaseUrlHelper createSearchBaseUrlHelper() { - // This cast to "SearchSettingsImpl" is somewhat ugly. - return new SearchBaseUrlHelper(getContext(), getHttpHelper(), - getSettings(), ((SearchSettingsImpl)getSettings()).getSearchPreferences()); - } - - public Help getHelp() { - // No point caching this, it's super cheap. - return new Help(getContext(), getConfig()); - } -} diff --git a/src/com/android/quicksearchbox/QsbApplication.kt b/src/com/android/quicksearchbox/QsbApplication.kt new file mode 100644 index 0000000..f53b481 --- /dev/null +++ b/src/com/android/quicksearchbox/QsbApplication.kt @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.Process +import android.view.ContextThemeWrapper +import com.android.quicksearchbox.google.GoogleSource +import com.android.quicksearchbox.google.GoogleSuggestClient +import com.android.quicksearchbox.google.SearchBaseUrlHelper +import com.android.quicksearchbox.ui.DefaultSuggestionViewFactory +import com.android.quicksearchbox.ui.SuggestionViewFactory +import com.android.quicksearchbox.util.* +import com.google.common.util.concurrent.ThreadFactoryBuilder +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory + +class QsbApplication(context: Context?) { + private val mContext: Context? + + private var mVersionCode: Long = 0 + private var mUiThreadHandler: Handler? = null + private var mConfig: Config? = null + private var mSettings: SearchSettings? = null + private var mSourceTaskExecutor: NamedTaskExecutor? = null + private var mQueryThreadFactory: ThreadFactory? = null + private var mSuggestionsProvider: SuggestionsProvider? = null + private var mSuggestionViewFactory: SuggestionViewFactory? = null + private var mGoogleSource: GoogleSource? = null + private var mVoiceSearch: VoiceSearch? = null + private var mLogger: Logger? = null + private var mSuggestionFormatter: SuggestionFormatter? = null + private var mTextAppearanceFactory: TextAppearanceFactory? = null + private var mIconLoaderExecutor: NamedTaskExecutor? = null + private var mHttpHelper: HttpHelper? = null + private var mSearchBaseUrlHelper: SearchBaseUrlHelper? = null + protected val context: Context? + get() = mContext + + // The current package should always exist, how else could we + // run code from it? + val versionCode: Long + @Suppress("DEPRECATION") + get() { + if (mVersionCode == 0L) { + mVersionCode = + try { + val pm: PackageManager? = context?.getPackageManager() + val pkgInfo: PackageInfo? = pm?.getPackageInfo(context!!.getPackageName(), 0) + pkgInfo!!.getLongVersionCode() + } catch (ex: PackageManager.NameNotFoundException) { + // The current package should always exist, how else could we + // run code from it? + throw RuntimeException(ex) + } + } + return mVersionCode + } + + protected fun checkThread() { + if (Looper.myLooper() !== Looper.getMainLooper()) { + throw IllegalStateException( + "Accessed Application object from thread " + Thread.currentThread().getName() + ) + } + } + + fun close() { + checkThread() + if (mConfig != null) { + mConfig!!.close() + mConfig = null + } + if (mSuggestionsProvider != null) { + mSuggestionsProvider!!.close() + mSuggestionsProvider = null + } + } + + @get:Synchronized + val mainThreadHandler: Handler? + get() { + if (mUiThreadHandler == null) { + mUiThreadHandler = Handler(Looper.getMainLooper()) + } + return mUiThreadHandler + } + + fun runOnUiThread(action: Runnable?) { + mainThreadHandler?.post(action!!) + } + + @get:Synchronized + val iconLoaderExecutor: NamedTaskExecutor? + get() { + if (mIconLoaderExecutor == null) { + mIconLoaderExecutor = createIconLoaderExecutor() + } + return mIconLoaderExecutor + } + + protected fun createIconLoaderExecutor(): NamedTaskExecutor { + val iconThreadFactory: ThreadFactory = PriorityThreadFactory(Process.THREAD_PRIORITY_BACKGROUND) + return PerNameExecutor(SingleThreadNamedTaskExecutor.factory(iconThreadFactory)) + } + + /** Indicates that construction of the QSB UI is now complete. */ + fun onStartupComplete() {} + + /** Gets the QSB configuration object. May be called from any thread. */ + @get:Synchronized + val config: Config? + get() { + if (mConfig == null) { + mConfig = createConfig() + } + return mConfig + } + + protected fun createConfig(): Config { + return Config(context) + } + + @get:Synchronized + val settings: SearchSettings? + get() { + if (mSettings == null) { + mSettings = createSettings() + mSettings!!.upgradeSettingsIfNeeded() + } + return mSettings + } + + protected fun createSettings(): SearchSettings { + return SearchSettingsImpl(context, config) + } + + protected fun createExecutorFactory(numThreads: Int): Factory<Executor?> { + val threadFactory: ThreadFactory? = queryThreadFactory + return object : Factory<Executor?> { + @Override + override fun create(): Executor { + return Executors.newFixedThreadPool(numThreads, threadFactory) + } + } + } + + /** Gets the source task executor. May only be called from the main thread. */ + val sourceTaskExecutor: NamedTaskExecutor? + get() { + checkThread() + if (mSourceTaskExecutor == null) { + mSourceTaskExecutor = createSourceTaskExecutor() + } + return mSourceTaskExecutor + } + + protected fun createSourceTaskExecutor(): NamedTaskExecutor { + val queryThreadFactory: ThreadFactory? = queryThreadFactory + return PerNameExecutor(SingleThreadNamedTaskExecutor.factory(queryThreadFactory)) + } + + /** Gets the query thread factory. May only be called from the main thread. */ + protected val queryThreadFactory: ThreadFactory? + get() { + checkThread() + if (mQueryThreadFactory == null) { + mQueryThreadFactory = createQueryThreadFactory() + } + return mQueryThreadFactory + } + + protected fun createQueryThreadFactory(): ThreadFactory { + val nameFormat = "QSB #%d" + val priority: Int = config!!.queryThreadPriority + return ThreadFactoryBuilder() + .setNameFormat(nameFormat) + .setThreadFactory(PriorityThreadFactory(priority)) + .build() + } + + /** + * Gets the suggestion provider. + * + * May only be called from the main thread. + */ + val suggestionsProvider: SuggestionsProvider? + get() { + checkThread() + if (mSuggestionsProvider == null) { + mSuggestionsProvider = createSuggestionsProvider() + } + return mSuggestionsProvider + } + + protected fun createSuggestionsProvider(): SuggestionsProvider { + return SuggestionsProviderImpl(config!!, sourceTaskExecutor!!, mainThreadHandler, logger) + } + + /** Gets the default suggestion view factory. May only be called from the main thread. */ + val suggestionViewFactory: SuggestionViewFactory? + get() { + checkThread() + if (mSuggestionViewFactory == null) { + mSuggestionViewFactory = createSuggestionViewFactory() + } + return mSuggestionViewFactory + } + + protected fun createSuggestionViewFactory(): SuggestionViewFactory { + return DefaultSuggestionViewFactory(context) + } + + /** Gets the Google source. May only be called from the main thread. */ + val googleSource: GoogleSource? + get() { + checkThread() + if (mGoogleSource == null) { + mGoogleSource = createGoogleSource() + } + return mGoogleSource + } + + protected fun createGoogleSource(): GoogleSource { + return GoogleSuggestClient(context, mainThreadHandler, iconLoaderExecutor!!, config!!) + } + + /** Gets Voice Search utilities. */ + val voiceSearch: VoiceSearch? + get() { + checkThread() + if (mVoiceSearch == null) { + mVoiceSearch = createVoiceSearch() + } + return mVoiceSearch + } + + protected fun createVoiceSearch(): VoiceSearch { + return VoiceSearch(context) + } + + /** Gets the event logger. May only be called from the main thread. */ + val logger: Logger? + get() { + checkThread() + if (mLogger == null) { + mLogger = createLogger() + } + return mLogger + } + + protected fun createLogger(): Logger { + return EventLogLogger(context, config!!) + } + + val suggestionFormatter: SuggestionFormatter? + get() { + if (mSuggestionFormatter == null) { + mSuggestionFormatter = createSuggestionFormatter() + } + return mSuggestionFormatter + } + + protected fun createSuggestionFormatter(): SuggestionFormatter { + return LevenshteinSuggestionFormatter(textAppearanceFactory) + } + + val textAppearanceFactory: TextAppearanceFactory? + get() { + if (mTextAppearanceFactory == null) { + mTextAppearanceFactory = createTextAppearanceFactory() + } + return mTextAppearanceFactory + } + + protected fun createTextAppearanceFactory(): TextAppearanceFactory { + return TextAppearanceFactory(context) + } + + @get:Synchronized + val httpHelper: HttpHelper? + get() { + if (mHttpHelper == null) { + mHttpHelper = createHttpHelper() + } + return mHttpHelper + } + + protected fun createHttpHelper(): HttpHelper { + return JavaNetHttpHelper(JavaNetHttpHelper.PassThroughRewriter(), config!!.userAgent) + } + + @get:Synchronized + val searchBaseUrlHelper: SearchBaseUrlHelper? + get() { + if (mSearchBaseUrlHelper == null) { + mSearchBaseUrlHelper = createSearchBaseUrlHelper() + } + return mSearchBaseUrlHelper + } + + protected fun createSearchBaseUrlHelper(): SearchBaseUrlHelper { + // This cast to "SearchSettingsImpl" is somewhat ugly. + return SearchBaseUrlHelper( + context, + httpHelper!!, + settings!!, + (settings as SearchSettingsImpl?)!!.searchPreferences + ) + } + + // No point caching this, it's super cheap. + val help: Help + get() = // No point caching this, it's super cheap. + Help(context, config!!) + + companion object { + val isFroyoOrLater: Boolean + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO + val isHoneycombOrLater: Boolean + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB + + @JvmStatic + operator fun get(context: Context?): QsbApplication { + return (context?.getApplicationContext() as QsbApplicationWrapper).app + } + } + + init { + // the application context does not use the theme from the <application> tag + mContext = ContextThemeWrapper(context, R.style.Theme_QuickSearchBox) + } +} diff --git a/src/com/android/quicksearchbox/QsbApplicationWrapper.java b/src/com/android/quicksearchbox/QsbApplicationWrapper.java deleted file mode 100644 index 7329cdf..0000000 --- a/src/com/android/quicksearchbox/QsbApplicationWrapper.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.app.Application; - -public class QsbApplicationWrapper extends Application { - - private QsbApplication mApp; - - @Override - public void onTerminate() { - synchronized (this) { - if (mApp != null) { - mApp.close(); - } - } - super.onTerminate(); - } - - public synchronized QsbApplication getApp() { - if (mApp == null) { - mApp = createQsbApplication(); - } - return mApp; - } - - protected QsbApplication createQsbApplication() { - return new QsbApplication(this); - } - -} diff --git a/src/com/android/quicksearchbox/QsbApplicationWrapper.kt b/src/com/android/quicksearchbox/QsbApplicationWrapper.kt new file mode 100644 index 0000000..fedae95 --- /dev/null +++ b/src/com/android/quicksearchbox/QsbApplicationWrapper.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.Application + +class QsbApplicationWrapper : Application() { + + private var mApp: QsbApplication? = null + + @Override + override fun onTerminate() { + synchronized(this) { + if (mApp != null) { + mApp!!.close() + } + } + super.onTerminate() + } + + @get:Synchronized + val app: QsbApplication + get() { + if (mApp == null) { + mApp = createQsbApplication() + } + return mApp!! + } + + protected fun createQsbApplication(): QsbApplication { + return QsbApplication(this) + } +} diff --git a/src/com/android/quicksearchbox/QueryTask.java b/src/com/android/quicksearchbox/QueryTask.java deleted file mode 100644 index 8ea5be9..0000000 --- a/src/com/android/quicksearchbox/QueryTask.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import com.android.quicksearchbox.util.Consumer; -import com.android.quicksearchbox.util.Consumers; -import com.android.quicksearchbox.util.NamedTask; -import com.android.quicksearchbox.util.NamedTaskExecutor; - -import android.os.Handler; -import android.util.Log; - -/** - * A task that gets suggestions from a corpus. - */ -public class QueryTask<C extends SuggestionCursor> implements NamedTask { - private static final String TAG = "QSB.QueryTask"; - private static final boolean DBG = false; - - private final String mQuery; - private final int mQueryLimit; - private final SuggestionCursorProvider<C> mProvider; - private final Handler mHandler; - private final Consumer<C> mConsumer; - - /** - * Creates a new query task. - * - * @param query Query to run. - * @param queryLimit The number of suggestions to ask each provider for. - * @param provider The provider to ask for suggestions. - * @param handler Handler that {@link Consumer#consume} will - * get called on. If null, the method is called on the query thread. - * @param consumer Consumer to notify when the suggestions have been returned. - */ - public QueryTask(String query, int queryLimit, SuggestionCursorProvider<C> provider, - Handler handler, Consumer<C> consumer) { - mQuery = query; - mQueryLimit = queryLimit; - mProvider = provider; - mHandler = handler; - mConsumer = consumer; - } - - @Override - public String getName() { - return mProvider.getName(); - } - - @Override - public void run() { - final C cursor = mProvider.getSuggestions(mQuery, mQueryLimit); - if (DBG) Log.d(TAG, "Suggestions from " + mProvider + " = " + cursor); - Consumers.consumeCloseableAsync(mHandler, mConsumer, cursor); - } - - @Override - public String toString() { - return mProvider + "[" + mQuery + "]"; - } - - public static <C extends SuggestionCursor> void startQuery(String query, - int maxResults, - SuggestionCursorProvider<C> provider, - NamedTaskExecutor executor, Handler handler, - Consumer<C> consumer) { - - QueryTask<C> task = new QueryTask<C>(query, maxResults, provider, handler, - consumer); - executor.execute(task); - } -} diff --git a/src/com/android/quicksearchbox/QueryTask.kt b/src/com/android/quicksearchbox/QueryTask.kt new file mode 100644 index 0000000..1b5d847 --- /dev/null +++ b/src/com/android/quicksearchbox/QueryTask.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.os.Handler +import android.util.Log +import com.android.quicksearchbox.util.Consumer +import com.android.quicksearchbox.util.Consumers +import com.android.quicksearchbox.util.NamedTask +import com.android.quicksearchbox.util.NamedTaskExecutor + +/** A task that gets suggestions from a corpus. */ +class QueryTask<C : SuggestionCursor?>( + private val mQuery: String?, + private val mQueryLimit: Int, + private val mProvider: SuggestionCursorProvider<C>?, + handler: Handler?, + consumer: Consumer<C>? +) : NamedTask { + + private val mHandler: Handler? + + private val mConsumer: Consumer<C>? + + @get:Override + override val name: String? + get() = mProvider?.name + + @Override + override fun run() { + val cursor = mProvider?.getSuggestions(mQuery, mQueryLimit) + if (DBG) Log.d(TAG, "Suggestions from $mProvider = $cursor") + Consumers.consumeCloseableAsync(mHandler, mConsumer, cursor) + } + + @Override + override fun toString(): String { + return "$mProvider[$mQuery]" + } + + companion object { + private const val TAG = "QSB.QueryTask" + private const val DBG = false + + @JvmStatic + fun <C : SuggestionCursor?> startQuery( + query: String?, + maxResults: Int, + provider: SuggestionCursorProvider<C>?, + executor: NamedTaskExecutor, + handler: Handler?, + consumer: Consumer<C>? + ) { + val task = QueryTask(query, maxResults, provider, handler, consumer) + executor.execute(task) + } + } + + /** + * Creates a new query task. + * + * @param query Query to run. + * @param queryLimit The number of suggestions to ask each provider for. + * @param provider The provider to ask for suggestions. + * @param handler Handler that [Consumer.consume] will get called on. If null, the method is + * called on the query thread. + * @param consumer Consumer to notify when the suggestions have been returned. + */ + init { + mHandler = handler + mConsumer = consumer + } +} diff --git a/src/com/android/quicksearchbox/ResultFilter.kt b/src/com/android/quicksearchbox/ResultFilter.kt new file mode 100644 index 0000000..27b03dd --- /dev/null +++ b/src/com/android/quicksearchbox/ResultFilter.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +/** [SuggestionFilter] that accepts only results (not web suggestions). */ +class ResultFilter : SuggestionFilter { + override fun accept(s: Suggestion?): Boolean { + return !s!!.isWebSearchSuggestion + } +} diff --git a/src/com/android/quicksearchbox/SearchActivity.java b/src/com/android/quicksearchbox/SearchActivity.java deleted file mode 100644 index ff21a17..0000000 --- a/src/com/android/quicksearchbox/SearchActivity.java +++ /dev/null @@ -1,510 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import android.app.Activity; -import android.app.SearchManager; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.os.Debug; -import android.os.Handler; -import android.text.TextUtils; -import android.util.Log; -import android.view.Menu; -import android.view.View; - -import com.android.common.Search; -import com.android.quicksearchbox.ui.SearchActivityView; -import com.android.quicksearchbox.ui.SuggestionClickListener; -import com.android.quicksearchbox.ui.SuggestionsAdapter; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.CharMatcher; - -import java.io.File; - -/** - * The main activity for Quick Search Box. Shows the search UI. - * - */ -public class SearchActivity extends Activity { - - private static final boolean DBG = false; - private static final String TAG = "QSB.SearchActivity"; - - private static final String SCHEME_CORPUS = "qsb.corpus"; - - private static final String INTENT_EXTRA_TRACE_START_UP = "trace_start_up"; - - // Keys for the saved instance state. - private static final String INSTANCE_KEY_QUERY = "query"; - - private static final String ACTIVITY_HELP_CONTEXT = "search"; - - private boolean mTraceStartUp; - // Measures time from for last onCreate()/onNewIntent() call. - private LatencyTracker mStartLatencyTracker; - // Measures time spent inside onCreate() - private LatencyTracker mOnCreateTracker; - private int mOnCreateLatency; - // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). - private boolean mStarting; - // True if the user has taken some action, e.g. launching a search, voice search, - // or suggestions, since QSB was last started. - private boolean mTookAction; - - private SearchActivityView mSearchActivityView; - - private Source mSource; - - private Bundle mAppSearchData; - - private final Handler mHandler = new Handler(); - private final Runnable mUpdateSuggestionsTask = new Runnable() { - @Override - public void run() { - updateSuggestions(); - } - }; - - private final Runnable mShowInputMethodTask = new Runnable() { - @Override - public void run() { - mSearchActivityView.showInputMethodForQuery(); - } - }; - - private OnDestroyListener mDestroyListener; - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - mTraceStartUp = getIntent().hasExtra(INTENT_EXTRA_TRACE_START_UP); - if (mTraceStartUp) { - String traceFile = new File(getDir("traces", 0), "qsb-start.trace").getAbsolutePath(); - Log.i(TAG, "Writing start-up trace to " + traceFile); - Debug.startMethodTracing(traceFile); - } - recordStartTime(); - if (DBG) Log.d(TAG, "onCreate()"); - super.onCreate(savedInstanceState); - - // This forces the HTTP request to check the users domain to be - // sent as early as possible. - QsbApplication.get(this).getSearchBaseUrlHelper(); - - mSource = QsbApplication.get(this).getGoogleSource(); - - mSearchActivityView = setupContentView(); - - if (getConfig().showScrollingResults()) { - mSearchActivityView.setMaxPromotedResults(getConfig().getMaxPromotedResults()); - } else { - mSearchActivityView.limitResultsToViewHeight(); - } - - mSearchActivityView.setSearchClickListener(new SearchActivityView.SearchClickListener() { - @Override - public boolean onSearchClicked(int method) { - return SearchActivity.this.onSearchClicked(method); - } - }); - - mSearchActivityView.setQueryListener(new SearchActivityView.QueryListener() { - @Override - public void onQueryChanged() { - updateSuggestionsBuffered(); - } - }); - - mSearchActivityView.setSuggestionClickListener(new ClickHandler()); - - mSearchActivityView.setVoiceSearchButtonClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - onVoiceSearchClicked(); - } - }); - - View.OnClickListener finishOnClick = new View.OnClickListener() { - @Override - public void onClick(View v) { - finish(); - } - }; - mSearchActivityView.setExitClickListener(finishOnClick); - - // First get setup from intent - Intent intent = getIntent(); - setupFromIntent(intent); - // Then restore any saved instance state - restoreInstanceState(savedInstanceState); - - // Do this at the end, to avoid updating the list view when setSource() - // is called. - mSearchActivityView.start(); - - recordOnCreateDone(); - } - - protected SearchActivityView setupContentView() { - setContentView(R.layout.search_activity); - return (SearchActivityView) findViewById(R.id.search_activity_view); - } - - protected SearchActivityView getSearchActivityView() { - return mSearchActivityView; - } - - @Override - protected void onNewIntent(Intent intent) { - if (DBG) Log.d(TAG, "onNewIntent()"); - recordStartTime(); - setIntent(intent); - setupFromIntent(intent); - } - - private void recordStartTime() { - mStartLatencyTracker = new LatencyTracker(); - mOnCreateTracker = new LatencyTracker(); - mStarting = true; - mTookAction = false; - } - - private void recordOnCreateDone() { - mOnCreateLatency = mOnCreateTracker.getLatency(); - } - - protected void restoreInstanceState(Bundle savedInstanceState) { - if (savedInstanceState == null) return; - String query = savedInstanceState.getString(INSTANCE_KEY_QUERY); - setQuery(query, false); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - // We don't save appSearchData, since we always get the value - // from the intent and the user can't change it. - - outState.putString(INSTANCE_KEY_QUERY, getQuery()); - } - - private void setupFromIntent(Intent intent) { - if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); - String corpusName = getCorpusNameFromUri(intent.getData()); - String query = intent.getStringExtra(SearchManager.QUERY); - Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); - boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); - - setQuery(query, selectAll); - mAppSearchData = appSearchData; - - } - - private String getCorpusNameFromUri(Uri uri) { - if (uri == null) return null; - if (!SCHEME_CORPUS.equals(uri.getScheme())) return null; - return uri.getAuthority(); - } - - private QsbApplication getQsbApplication() { - return QsbApplication.get(this); - } - - private Config getConfig() { - return getQsbApplication().getConfig(); - } - - protected SearchSettings getSettings() { - return getQsbApplication().getSettings(); - } - - private SuggestionsProvider getSuggestionsProvider() { - return getQsbApplication().getSuggestionsProvider(); - } - - private Logger getLogger() { - return getQsbApplication().getLogger(); - } - - @VisibleForTesting - public void setOnDestroyListener(OnDestroyListener l) { - mDestroyListener = l; - } - - @Override - protected void onDestroy() { - if (DBG) Log.d(TAG, "onDestroy()"); - mSearchActivityView.destroy(); - super.onDestroy(); - if (mDestroyListener != null) { - mDestroyListener.onDestroyed(); - } - } - - @Override - protected void onStop() { - if (DBG) Log.d(TAG, "onStop()"); - if (!mTookAction) { - // TODO: This gets logged when starting other activities, e.g. by opening the search - // settings, or clicking a notification in the status bar. - // TODO we should log both sets of suggestions in 2-pane mode - getLogger().logExit(getCurrentSuggestions(), getQuery().length()); - } - // Close all open suggestion cursors. The query will be redone in onResume() - // if we come back to this activity. - mSearchActivityView.clearSuggestions(); - mSearchActivityView.onStop(); - super.onStop(); - } - - @Override - protected void onPause() { - if (DBG) Log.d(TAG, "onPause()"); - mSearchActivityView.onPause(); - super.onPause(); - } - - @Override - protected void onRestart() { - if (DBG) Log.d(TAG, "onRestart()"); - super.onRestart(); - } - - @Override - protected void onResume() { - if (DBG) Log.d(TAG, "onResume()"); - super.onResume(); - updateSuggestionsBuffered(); - mSearchActivityView.onResume(); - if (mTraceStartUp) Debug.stopMethodTracing(); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - // Since the menu items are dynamic, we recreate the menu every time. - menu.clear(); - createMenuItems(menu, true); - return true; - } - - public void createMenuItems(Menu menu, boolean showDisabled) { - getQsbApplication().getHelp().addHelpMenuItem(menu, ACTIVITY_HELP_CONTEXT); - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - if (hasFocus) { - // Launch the IME after a bit - mHandler.postDelayed(mShowInputMethodTask, 0); - } - } - - protected String getQuery() { - return mSearchActivityView.getQuery(); - } - - protected void setQuery(String query, boolean selectAll) { - mSearchActivityView.setQuery(query, selectAll); - } - - /** - * @return true if a search was performed as a result of this click, false otherwise. - */ - protected boolean onSearchClicked(int method) { - String query = CharMatcher.whitespace().trimAndCollapseFrom(getQuery(), ' '); - if (DBG) Log.d(TAG, "Search clicked, query=" + query); - - // Don't do empty queries - if (TextUtils.getTrimmedLength(query) == 0) return false; - - mTookAction = true; - - // Log search start - getLogger().logSearch(method, query.length()); - - // Start search - startSearch(mSource, query); - return true; - } - - protected void startSearch(Source searchSource, String query) { - Intent intent = searchSource.createSearchIntent(query, mAppSearchData); - launchIntent(intent); - } - - protected void onVoiceSearchClicked() { - if (DBG) Log.d(TAG, "Voice Search clicked"); - - mTookAction = true; - - // Log voice search start - getLogger().logVoiceSearch(); - - // Start voice search - Intent intent = mSource.createVoiceSearchIntent(mAppSearchData); - launchIntent(intent); - } - - protected Source getSearchSource() { - return mSource; - } - - protected SuggestionCursor getCurrentSuggestions() { - Suggestions suggestions = mSearchActivityView.getSuggestions(); - if (suggestions == null) { - return null; - } - return suggestions.getResult(); - } - - protected SuggestionPosition getCurrentSuggestions(SuggestionsAdapter<?> adapter, long id) { - SuggestionPosition pos = adapter.getSuggestion(id); - if (pos == null) { - return null; - } - SuggestionCursor suggestions = pos.getCursor(); - int position = pos.getPosition(); - if (suggestions == null) { - return null; - } - int count = suggestions.getCount(); - if (position < 0 || position >= count) { - Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count); - return null; - } - suggestions.moveTo(position); - return pos; - } - - protected void launchIntent(Intent intent) { - if (DBG) Log.d(TAG, "launchIntent " + intent); - if (intent == null) { - return; - } - try { - startActivity(intent); - } catch (RuntimeException ex) { - // Since the intents for suggestions specified by suggestion providers, - // guard against them not being handled, not allowed, etc. - Log.e(TAG, "Failed to start " + intent.toUri(0), ex); - } - } - - private boolean launchSuggestion(SuggestionsAdapter<?> adapter, long id) { - SuggestionPosition suggestion = getCurrentSuggestions(adapter, id); - if (suggestion == null) return false; - - if (DBG) Log.d(TAG, "Launching suggestion " + id); - mTookAction = true; - - // Log suggestion click - getLogger().logSuggestionClick(id, suggestion.getCursor(), - Logger.SUGGESTION_CLICK_TYPE_LAUNCH); - - // Launch intent - launchSuggestion(suggestion.getCursor(), suggestion.getPosition()); - - return true; - } - - protected void launchSuggestion(SuggestionCursor suggestions, int position) { - suggestions.moveTo(position); - Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData); - launchIntent(intent); - } - - protected void refineSuggestion(SuggestionsAdapter<?> adapter, long id) { - if (DBG) Log.d(TAG, "query refine clicked, pos " + id); - SuggestionPosition suggestion = getCurrentSuggestions(adapter, id); - if (suggestion == null) { - return; - } - String query = suggestion.getSuggestionQuery(); - if (TextUtils.isEmpty(query)) { - return; - } - - // Log refine click - getLogger().logSuggestionClick(id, suggestion.getCursor(), - Logger.SUGGESTION_CLICK_TYPE_REFINE); - - // Put query + space in query text view - String queryWithSpace = query + ' '; - setQuery(queryWithSpace, false); - updateSuggestions(); - mSearchActivityView.focusQueryTextView(); - } - - private void updateSuggestionsBuffered() { - if (DBG) Log.d(TAG, "updateSuggestionsBuffered()"); - mHandler.removeCallbacks(mUpdateSuggestionsTask); - long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); - mHandler.postDelayed(mUpdateSuggestionsTask, delay); - } - - private void gotSuggestions(Suggestions suggestions) { - if (mStarting) { - mStarting = false; - String source = getIntent().getStringExtra(Search.SOURCE); - int latency = mStartLatencyTracker.getLatency(); - getLogger().logStart(mOnCreateLatency, latency, source); - getQsbApplication().onStartupComplete(); - } - } - - public void updateSuggestions() { - if (DBG) Log.d(TAG, "updateSuggestions()"); - final String query = CharMatcher.whitespace().trimLeadingFrom(getQuery()); - updateSuggestions(query, mSource); - } - - protected void updateSuggestions(String query, Source source) { - if (DBG) Log.d(TAG, "updateSuggestions(\"" + query+"\"," + source + ")"); - Suggestions suggestions = getSuggestionsProvider().getSuggestions( - query, source); - - // Log start latency if this is the first suggestions update - gotSuggestions(suggestions); - - showSuggestions(suggestions); - } - - protected void showSuggestions(Suggestions suggestions) { - mSearchActivityView.setSuggestions(suggestions); - } - - private class ClickHandler implements SuggestionClickListener { - - @Override - public void onSuggestionClicked(SuggestionsAdapter<?> adapter, long id) { - launchSuggestion(adapter, id); - } - - @Override - public void onSuggestionQueryRefineClicked(SuggestionsAdapter<?> adapter, long id) { - refineSuggestion(adapter, id); - } - } - - public interface OnDestroyListener { - void onDestroyed(); - } - -} diff --git a/src/com/android/quicksearchbox/SearchActivity.kt b/src/com/android/quicksearchbox/SearchActivity.kt new file mode 100644 index 0000000..0620b97 --- /dev/null +++ b/src/com/android/quicksearchbox/SearchActivity.kt @@ -0,0 +1,475 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.Activity +import android.app.SearchManager +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Debug +import android.os.Handler +import android.os.Looper +import android.text.TextUtils +import android.util.Log +import android.view.Menu +import android.view.View +import com.android.common.Search +import com.android.quicksearchbox.ui.SearchActivityView +import com.android.quicksearchbox.ui.SuggestionClickListener +import com.android.quicksearchbox.ui.SuggestionsAdapter +import com.google.common.annotations.VisibleForTesting +import com.google.common.base.CharMatcher +import java.io.File + +/** The main activity for Quick Search Box. Shows the search UI. */ +class SearchActivity : Activity() { + private var mTraceStartUp = false + + // Measures time from for last onCreate()/onNewIntent() call. + private var mStartLatencyTracker: LatencyTracker? = null + + // Measures time spent inside onCreate() + private var mOnCreateTracker: LatencyTracker? = null + private var mOnCreateLatency = 0 + + // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). + private var mStarting = false + + // True if the user has taken some action, e.g. launching a search, voice search, + // or suggestions, since QSB was last started. + private var mTookAction = false + private var mSearchActivityView: SearchActivityView? = null + protected var searchSource: Source? = null + private set + private var mAppSearchData: Bundle? = null + private val mHandler: Handler = Handler(Looper.getMainLooper()) + private val mUpdateSuggestionsTask: Runnable = + object : Runnable { + @Override + override fun run() { + updateSuggestions() + } + } + private val mShowInputMethodTask: Runnable = + object : Runnable { + @Override + override fun run() { + mSearchActivityView?.showInputMethodForQuery() + } + } + private var mDestroyListener: OnDestroyListener? = null + + /** Called when the activity is first created. */ + @Override + override fun onCreate(savedInstanceState: Bundle?) { + mTraceStartUp = getIntent().hasExtra(INTENT_EXTRA_TRACE_START_UP) + if (mTraceStartUp) { + val traceFile: String = File(getDir("traces", 0), "qsb-start.trace").getAbsolutePath() + Log.i(TAG, "Writing start-up trace to $traceFile") + Debug.startMethodTracing(traceFile) + } + recordStartTime() + if (DBG) Log.d(TAG, "onCreate()") + super.onCreate(savedInstanceState) + + // This forces the HTTP request to check the users domain to be + // sent as early as possible. + QsbApplication[this].searchBaseUrlHelper + searchSource = QsbApplication[this].googleSource + mSearchActivityView = setupContentView() + if (config?.showScrollingResults() == true) { + mSearchActivityView?.setMaxPromotedResults(config!!.maxPromotedResults) + } else { + mSearchActivityView?.limitResultsToViewHeight() + } + mSearchActivityView?.setSearchClickListener( + object : SearchActivityView.SearchClickListener { + @Override + override fun onSearchClicked(method: Int): Boolean { + return this@SearchActivity.onSearchClicked(method) + } + } + ) + mSearchActivityView?.setQueryListener( + object : SearchActivityView.QueryListener { + @Override + override fun onQueryChanged() { + updateSuggestionsBuffered() + } + } + ) + mSearchActivityView?.setSuggestionClickListener(ClickHandler()) + mSearchActivityView?.setVoiceSearchButtonClickListener( + object : View.OnClickListener { + @Override + override fun onClick(view: View?) { + onVoiceSearchClicked() + } + } + ) + val finishOnClick: View.OnClickListener = + object : View.OnClickListener { + @Override + override fun onClick(v: View?) { + finish() + } + } + mSearchActivityView?.setExitClickListener(finishOnClick) + + // First get setup from intent + val intent: Intent = getIntent() + setupFromIntent(intent) + // Then restore any saved instance state + restoreInstanceState(savedInstanceState) + + // Do this at the end, to avoid updating the list view when setSource() + // is called. + mSearchActivityView?.start() + recordOnCreateDone() + } + + protected fun setupContentView(): SearchActivityView { + setContentView(R.layout.search_activity) + return findViewById(R.id.search_activity_view) as SearchActivityView + } + + protected val searchActivityView: SearchActivityView? + get() = mSearchActivityView + + @Override + protected override fun onNewIntent(intent: Intent) { + if (DBG) Log.d(TAG, "onNewIntent()") + recordStartTime() + setIntent(intent) + setupFromIntent(intent) + } + + private fun recordStartTime() { + mStartLatencyTracker = LatencyTracker() + mOnCreateTracker = LatencyTracker() + mStarting = true + mTookAction = false + } + + private fun recordOnCreateDone() { + mOnCreateLatency = mOnCreateTracker!!.latency + } + + protected fun restoreInstanceState(savedInstanceState: Bundle?) { + if (savedInstanceState == null) return + val query: String? = savedInstanceState.getString(INSTANCE_KEY_QUERY) + setQuery(query, false) + } + + @Override + protected override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + // We don't save appSearchData, since we always get the value + // from the intent and the user can't change it. + outState.putString(INSTANCE_KEY_QUERY, query) + } + + private fun setupFromIntent(intent: Intent) { + if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0).toString() + ")") + @Suppress("UNUSED_VARIABLE") val corpusName = getCorpusNameFromUri(intent.getData()) + val query: String? = intent.getStringExtra(SearchManager.QUERY) + val appSearchData: Bundle? = intent.getBundleExtra(SearchManager.APP_DATA) + val selectAll: Boolean = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false) + setQuery(query, selectAll) + mAppSearchData = appSearchData + } + + private fun getCorpusNameFromUri(uri: Uri?): String? { + if (uri == null) return null + return if (SCHEME_CORPUS != uri.getScheme()) null else uri.getAuthority() + } + + private val qsbApplication: QsbApplication + get() = QsbApplication[this] + + private val config: Config? + get() = qsbApplication.config + + protected val settings: SearchSettings? + get() = qsbApplication.settings + + private val suggestionsProvider: SuggestionsProvider? + get() = qsbApplication.suggestionsProvider + + private val logger: Logger? + get() = qsbApplication.logger + + @VisibleForTesting + fun setOnDestroyListener(l: OnDestroyListener?) { + mDestroyListener = l + } + + @Override + protected override fun onDestroy() { + if (DBG) Log.d(TAG, "onDestroy()") + mSearchActivityView?.destroy() + super.onDestroy() + if (mDestroyListener != null) { + mDestroyListener?.onDestroyed() + } + } + + @Override + protected override fun onStop() { + if (DBG) Log.d(TAG, "onStop()") + if (!mTookAction) { + // TODO: This gets logged when starting other activities, e.g. by opening the search + // settings, or clicking a notification in the status bar. + // TODO we should log both sets of suggestions in 2-pane mode + logger?.logExit(currentSuggestions, query!!.length) + } + // Close all open suggestion cursors. The query will be redone in onResume() + // if we come back to this activity. + mSearchActivityView?.clearSuggestions() + mSearchActivityView?.onStop() + super.onStop() + } + + @Override + protected override fun onPause() { + if (DBG) Log.d(TAG, "onPause()") + mSearchActivityView?.onPause() + super.onPause() + } + + @Override + protected override fun onRestart() { + if (DBG) Log.d(TAG, "onRestart()") + super.onRestart() + } + + @Override + protected override fun onResume() { + if (DBG) Log.d(TAG, "onResume()") + super.onResume() + updateSuggestionsBuffered() + mSearchActivityView?.onResume() + if (mTraceStartUp) Debug.stopMethodTracing() + } + + @Override + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + // Since the menu items are dynamic, we recreate the menu every time. + menu.clear() + createMenuItems(menu, true) + return true + } + + @Suppress("UNUSED_PARAMETER") + fun createMenuItems(menu: Menu, showDisabled: Boolean) { + qsbApplication.help.addHelpMenuItem(menu, ACTIVITY_HELP_CONTEXT) + } + + @Override + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (hasFocus) { + // Launch the IME after a bit + mHandler.postDelayed(mShowInputMethodTask, 0) + } + } + + protected val query: String? + get() = mSearchActivityView?.query + + protected fun setQuery(query: String?, selectAll: Boolean) { + mSearchActivityView?.setQuery(query, selectAll) + } + + /** @return true if a search was performed as a result of this click, false otherwise. */ + protected fun onSearchClicked(method: Int): Boolean { + val query: String = CharMatcher.whitespace().trimAndCollapseFrom(query as CharSequence, ' ') + if (DBG) Log.d(TAG, "Search clicked, query=$query") + + // Don't do empty queries + if (TextUtils.getTrimmedLength(query) == 0) return false + mTookAction = true + + // Log search start + logger?.logSearch(method, query.length) + + // Start search + startSearch(searchSource, query) + return true + } + + protected fun startSearch(searchSource: Source?, query: String?) { + val intent: Intent? = searchSource!!.createSearchIntent(query, mAppSearchData) + launchIntent(intent) + } + + protected fun onVoiceSearchClicked() { + if (DBG) Log.d(TAG, "Voice Search clicked") + mTookAction = true + + // Log voice search start + logger?.logVoiceSearch() + + // Start voice search + val intent: Intent? = searchSource!!.createVoiceSearchIntent(mAppSearchData) + launchIntent(intent) + } + + protected val currentSuggestions: SuggestionCursor? + get() { + val suggestions: Suggestions = mSearchActivityView?.suggestions ?: return null + return suggestions.getResult() + } + + protected fun getCurrentSuggestions( + adapter: SuggestionsAdapter<*>?, + id: Long + ): SuggestionPosition? { + val pos: SuggestionPosition = adapter?.getSuggestion(id) ?: return null + val suggestions: SuggestionCursor? = pos.cursor + val position: Int = pos.position + if (suggestions == null) { + return null + } + val count: Int = suggestions.count + if (position < 0 || position >= count) { + Log.w(TAG, "Invalid suggestion position $position, count = $count") + return null + } + suggestions.moveTo(position) + return pos + } + + protected fun launchIntent(intent: Intent?) { + if (DBG) Log.d(TAG, "launchIntent $intent") + if (intent == null) { + return + } + try { + startActivity(intent) + } catch (ex: RuntimeException) { + // Since the intents for suggestions specified by suggestion providers, + // guard against them not being handled, not allowed, etc. + Log.e(TAG, "Failed to start " + intent.toUri(0), ex) + } + } + + private fun launchSuggestion(adapter: SuggestionsAdapter<*>?, id: Long): Boolean { + val suggestion = getCurrentSuggestions(adapter, id) ?: return false + if (DBG) Log.d(TAG, "Launching suggestion $id") + mTookAction = true + + // Log suggestion click + logger?.logSuggestionClick(id, suggestion.cursor, Logger.SUGGESTION_CLICK_TYPE_LAUNCH) + + // Launch intent + launchSuggestion(suggestion.cursor, suggestion.position) + return true + } + + protected fun launchSuggestion(suggestions: SuggestionCursor?, position: Int) { + suggestions?.moveTo(position) + val intent: Intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData) + launchIntent(intent) + } + + protected fun refineSuggestion(adapter: SuggestionsAdapter<*>?, id: Long) { + if (DBG) Log.d(TAG, "query refine clicked, pos $id") + val suggestion = getCurrentSuggestions(adapter, id) ?: return + val query: String? = suggestion.suggestionQuery + if (TextUtils.isEmpty(query)) { + return + } + + // Log refine click + logger?.logSuggestionClick(id, suggestion.cursor, Logger.SUGGESTION_CLICK_TYPE_REFINE) + + // Put query + space in query text view + val queryWithSpace = "$query " + setQuery(queryWithSpace, false) + updateSuggestions() + mSearchActivityView?.focusQueryTextView() + } + + private fun updateSuggestionsBuffered() { + if (DBG) Log.d(TAG, "updateSuggestionsBuffered()") + mHandler.removeCallbacks(mUpdateSuggestionsTask) + val delay: Long = config!!.typingUpdateSuggestionsDelayMillis + mHandler.postDelayed(mUpdateSuggestionsTask, delay) + } + + @Suppress("UNUSED_PARAMETER") + private fun gotSuggestions(suggestions: Suggestions?) { + if (mStarting) { + mStarting = false + val source: String? = getIntent().getStringExtra(Search.SOURCE) + val latency: Int = mStartLatencyTracker!!.latency + logger?.logStart(mOnCreateLatency, latency, source) + qsbApplication.onStartupComplete() + } + } + + fun updateSuggestions() { + if (DBG) Log.d(TAG, "updateSuggestions()") + val query: String = CharMatcher.whitespace().trimLeadingFrom(query as CharSequence) + updateSuggestions(query, searchSource) + } + + protected fun updateSuggestions(query: String, source: Source?) { + if (DBG) Log.d(TAG, "updateSuggestions(\"$query\",$source)") + val suggestions = suggestionsProvider?.getSuggestions(query, source!!) + + // Log start latency if this is the first suggestions update + gotSuggestions(suggestions) + showSuggestions(suggestions) + } + + protected fun showSuggestions(suggestions: Suggestions?) { + mSearchActivityView?.suggestions = suggestions + } + + private inner class ClickHandler : SuggestionClickListener { + @Override + override fun onSuggestionClicked(adapter: SuggestionsAdapter<*>?, suggestionId: Long) { + launchSuggestion(adapter, suggestionId) + } + + @Override + override fun onSuggestionQueryRefineClicked( + adapter: SuggestionsAdapter<*>?, + suggestionId: Long + ) { + refineSuggestion(adapter, suggestionId) + } + } + + interface OnDestroyListener { + fun onDestroyed() + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.SearchActivity" + private const val SCHEME_CORPUS = "qsb.corpus" + private const val INTENT_EXTRA_TRACE_START_UP = "trace_start_up" + + // Keys for the saved instance state. + private const val INSTANCE_KEY_QUERY = "query" + private const val ACTIVITY_HELP_CONTEXT = "search" + } +} diff --git a/src/com/android/quicksearchbox/SearchSettings.java b/src/com/android/quicksearchbox/SearchSettings.java deleted file mode 100644 index 7b1a8a9..0000000 --- a/src/com/android/quicksearchbox/SearchSettings.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - - -/** - * Interface for search settings. - * - * NOTE: Currently, this is not used very widely, in most instances - * implementers of this interface are passed around by class name. - * Should this be deprecated ? - */ -public interface SearchSettings { - - public void upgradeSettingsIfNeeded(); - - /** - * Informs our listeners about the updated settings data. - */ - public void broadcastSettingsChanged(); - - public int getNextVoiceSearchHintIndex(int size); - - public void resetVoiceSearchHintFirstSeenTime(); - - public boolean haveVoiceSearchHintsExpired(int currentVoiceSearchVersion); - - /** - * Determines whether google.com should be used as the base path - * for all searches (as opposed to using its country specific variants). - */ - public boolean shouldUseGoogleCom(); - - public void setUseGoogleCom(boolean useGoogleCom); - - public long getSearchBaseDomainApplyTime(); - - public String getSearchBaseDomain(); - - public void setSearchBaseDomain(String searchBaseUrl); -} diff --git a/src/com/android/quicksearchbox/SearchSettings.kt b/src/com/android/quicksearchbox/SearchSettings.kt new file mode 100644 index 0000000..b3d2755 --- /dev/null +++ b/src/com/android/quicksearchbox/SearchSettings.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +/** + * Interface for search settings. + * + * NOTE: Currently, this is not used very widely, in most instances implementers of this interface + * are passed around by class name. Should this be deprecated ? + */ +interface SearchSettings { + fun upgradeSettingsIfNeeded() + + /** Informs our listeners about the updated settings data. */ + fun broadcastSettingsChanged() + fun getNextVoiceSearchHintIndex(size: Int): Int + fun resetVoiceSearchHintFirstSeenTime() + fun haveVoiceSearchHintsExpired(currentVoiceSearchVersion: Int): Boolean + + /** + * Determines whether google.com should be used as the base path for all searches (as opposed to + * using its country specific variants). + */ + fun shouldUseGoogleCom(): Boolean + fun setUseGoogleCom(useGoogleCom: Boolean) + val searchBaseDomainApplyTime: Long + var searchBaseDomain: String? +} diff --git a/src/com/android/quicksearchbox/SearchSettingsImpl.java b/src/com/android/quicksearchbox/SearchSettingsImpl.java deleted file mode 100644 index 1fc74ea..0000000 --- a/src/com/android/quicksearchbox/SearchSettingsImpl.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import android.app.SearchManager; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.util.Log; - -import com.android.common.SharedPreferencesCompat; - -/** - * Manages user settings. - */ -public class SearchSettingsImpl implements SearchSettings { - - private static final boolean DBG = false; - private static final String TAG = "QSB.SearchSettingsImpl"; - - // Name of the preferences file used to store search preference - public static final String PREFERENCES_NAME = "SearchSettings"; - - /** - * Preference key used for storing the index of the next voice search hint to show. - */ - private static final String NEXT_VOICE_SEARCH_HINT_INDEX_PREF = "next_voice_search_hint"; - - /** - * Preference key used to store the time at which the first voice search hint was displayed. - */ - private static final String FIRST_VOICE_HINT_DISPLAY_TIME = "first_voice_search_hint_time"; - - /** - * Preference key for the version of voice search we last got hints from. - */ - private static final String LAST_SEEN_VOICE_SEARCH_VERSION = "voice_search_version"; - - /** - * Preference key for storing whether searches always go to google.com. Public - * so that it can be used by PreferenceControllers. - */ - public static final String USE_GOOGLE_COM_PREF = "use_google_com"; - - /** - * Preference key for the base search URL. This value is normally set by - * a SearchBaseUrlHelper instance. Public so classes can listen to changes - * on this key. - */ - public static final String SEARCH_BASE_DOMAIN_PREF = "search_base_domain"; - - /** - * This is the time at which the base URL was stored, and is set using - * @link{System.currentTimeMillis()}. - */ - private static final String SEARCH_BASE_DOMAIN_APPLY_TIME = "search_base_domain_apply_time"; - - private final Context mContext; - - private final Config mConfig; - - public SearchSettingsImpl(Context context, Config config) { - mContext = context; - mConfig = config; - } - - protected Context getContext() { - return mContext; - } - - protected Config getConfig() { - return mConfig; - } - - @Override - public void upgradeSettingsIfNeeded() { - } - - public SharedPreferences getSearchPreferences() { - return getContext().getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); - } - - protected void storeBoolean(String name, boolean value) { - SharedPreferencesCompat.apply(getSearchPreferences().edit().putBoolean(name, value)); - } - - protected void storeInt(String name, int value) { - SharedPreferencesCompat.apply(getSearchPreferences().edit().putInt(name, value)); - } - - protected void storeLong(String name, long value) { - SharedPreferencesCompat.apply(getSearchPreferences().edit().putLong(name, value)); - } - - protected void storeString(String name, String value) { - SharedPreferencesCompat.apply(getSearchPreferences().edit().putString(name, value)); - } - - protected void removePref(String name) { - SharedPreferencesCompat.apply(getSearchPreferences().edit().remove(name)); - } - - /** - * Informs our listeners about the updated settings data. - */ - @Override - public void broadcastSettingsChanged() { - // We use a message broadcast since the listeners could be in multiple processes. - Intent intent = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED); - Log.i(TAG, "Broadcasting: " + intent); - getContext().sendBroadcast(intent); - } - - @Override - public int getNextVoiceSearchHintIndex(int size) { - int i = getAndIncrementIntPreference(getSearchPreferences(), - NEXT_VOICE_SEARCH_HINT_INDEX_PREF); - return i % size; - } - - // TODO: Could this be made atomic to avoid races? - private int getAndIncrementIntPreference(SharedPreferences prefs, String name) { - int i = prefs.getInt(name, 0); - storeInt(name, i + 1); - return i; - } - - @Override - public void resetVoiceSearchHintFirstSeenTime() { - storeLong(FIRST_VOICE_HINT_DISPLAY_TIME, System.currentTimeMillis()); - } - - @Override - public boolean haveVoiceSearchHintsExpired(int currentVoiceSearchVersion) { - SharedPreferences prefs = getSearchPreferences(); - - if (currentVoiceSearchVersion != 0) { - long currentTime = System.currentTimeMillis(); - int lastVoiceSearchVersion = prefs.getInt(LAST_SEEN_VOICE_SEARCH_VERSION, 0); - long firstHintTime = prefs.getLong(FIRST_VOICE_HINT_DISPLAY_TIME, 0); - if (firstHintTime == 0 || currentVoiceSearchVersion != lastVoiceSearchVersion) { - SharedPreferencesCompat.apply(prefs.edit() - .putInt(LAST_SEEN_VOICE_SEARCH_VERSION, currentVoiceSearchVersion) - .putLong(FIRST_VOICE_HINT_DISPLAY_TIME, currentTime)); - firstHintTime = currentTime; - } - if (currentTime - firstHintTime > getConfig().getVoiceSearchHintActivePeriod()) { - if (DBG) Log.d(TAG, "Voice seach hint period expired; not showing hints."); - return true; - } else { - return false; - } - } else { - if (DBG) Log.d(TAG, "Could not determine voice search version; not showing hints."); - return true; - } - } - - /** - * @return true if user searches should always be based at google.com, false - * otherwise. - */ - @Override - public boolean shouldUseGoogleCom() { - // Note that this preserves the old behaviour of using google.com - // for searches, with the gl= parameter set. - return getSearchPreferences().getBoolean(USE_GOOGLE_COM_PREF, true); - } - - @Override - public void setUseGoogleCom(boolean useGoogleCom) { - storeBoolean(USE_GOOGLE_COM_PREF, useGoogleCom); - } - - @Override - public long getSearchBaseDomainApplyTime() { - return getSearchPreferences().getLong(SEARCH_BASE_DOMAIN_APPLY_TIME, -1); - } - - @Override - public String getSearchBaseDomain() { - // Note that the only time this will return null is on the first run - // of the app, or when settings have been cleared. Callers should - // ideally check that getSearchBaseDomainApplyTime() is not -1 before - // calling this function. - return getSearchPreferences().getString(SEARCH_BASE_DOMAIN_PREF, null); - } - - @Override - public void setSearchBaseDomain(String searchBaseUrl) { - Editor sharedPrefEditor = getSearchPreferences().edit(); - sharedPrefEditor.putString(SEARCH_BASE_DOMAIN_PREF, searchBaseUrl); - sharedPrefEditor.putLong(SEARCH_BASE_DOMAIN_APPLY_TIME, System.currentTimeMillis()); - - SharedPreferencesCompat.apply(sharedPrefEditor); - } -} diff --git a/src/com/android/quicksearchbox/SearchSettingsImpl.kt b/src/com/android/quicksearchbox/SearchSettingsImpl.kt new file mode 100644 index 0000000..8168f78 --- /dev/null +++ b/src/com/android/quicksearchbox/SearchSettingsImpl.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.SearchManager +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.SharedPreferences.Editor +import android.util.Log +import com.android.common.SharedPreferencesCompat + +/** Manages user settings. */ +class SearchSettingsImpl(context: Context?, config: Config?) : SearchSettings { + private val mContext: Context? + protected val config: Config? + protected val context: Context? + get() = mContext + + @Override override fun upgradeSettingsIfNeeded() {} + + val searchPreferences: SharedPreferences + get() = context!!.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + + protected fun storeBoolean(name: String?, value: Boolean) { + SharedPreferencesCompat.apply(searchPreferences.edit().putBoolean(name, value)) + } + + protected fun storeInt(name: String?, value: Int) { + SharedPreferencesCompat.apply(searchPreferences.edit().putInt(name, value)) + } + + protected fun storeLong(name: String?, value: Long) { + SharedPreferencesCompat.apply(searchPreferences.edit().putLong(name, value)) + } + + protected fun storeString(name: String?, value: String?) { + SharedPreferencesCompat.apply(searchPreferences.edit().putString(name, value)) + } + + protected fun removePref(name: String?) { + SharedPreferencesCompat.apply(searchPreferences.edit().remove(name)) + } + + /** Informs our listeners about the updated settings data. */ + @Override + override fun broadcastSettingsChanged() { + // We use a message broadcast since the listeners could be in multiple processes. + val intent = Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED) + Log.i(TAG, "Broadcasting: $intent") + context?.sendBroadcast(intent) + } + + @Override + override fun getNextVoiceSearchHintIndex(size: Int): Int { + val i = getAndIncrementIntPreference(searchPreferences, NEXT_VOICE_SEARCH_HINT_INDEX_PREF) + return i % size + } + + // TODO: Could this be made atomic to avoid races? + private fun getAndIncrementIntPreference(prefs: SharedPreferences, name: String): Int { + val i: Int = prefs.getInt(name, 0) + storeInt(name, i + 1) + return i + } + + @Override + override fun resetVoiceSearchHintFirstSeenTime() { + storeLong(FIRST_VOICE_HINT_DISPLAY_TIME, System.currentTimeMillis()) + } + + @Override + override fun haveVoiceSearchHintsExpired(currentVoiceSearchVersion: Int): Boolean { + val prefs: SharedPreferences = searchPreferences + return if (currentVoiceSearchVersion != 0) { + val currentTime: Long = System.currentTimeMillis() + val lastVoiceSearchVersion: Int = prefs.getInt(LAST_SEEN_VOICE_SEARCH_VERSION, 0) + var firstHintTime: Long = prefs.getLong(FIRST_VOICE_HINT_DISPLAY_TIME, 0) + if (firstHintTime == 0L || currentVoiceSearchVersion != lastVoiceSearchVersion) { + SharedPreferencesCompat.apply( + prefs + .edit() + .putInt(LAST_SEEN_VOICE_SEARCH_VERSION, currentVoiceSearchVersion) + .putLong(FIRST_VOICE_HINT_DISPLAY_TIME, currentTime) + ) + firstHintTime = currentTime + } + if (currentTime - firstHintTime > config!!.voiceSearchHintActivePeriod) { + if (DBG) Log.d(TAG, "Voice search hint period expired; not showing hints.") + return true + } else { + false + } + } else { + if (DBG) Log.d(TAG, "Could not determine voice search version; not showing hints.") + true + } + } + + /** @return true if user searches should always be based at google.com, false otherwise. */ + @Override + override fun shouldUseGoogleCom(): Boolean { + // Note that this preserves the old behaviour of using google.com + // for searches, with the gl= parameter set. + return searchPreferences.getBoolean(USE_GOOGLE_COM_PREF, true) + } + + @Override + override fun setUseGoogleCom(useGoogleCom: Boolean) { + storeBoolean(USE_GOOGLE_COM_PREF, useGoogleCom) + } + + @get:Override + override val searchBaseDomainApplyTime: Long + get() = searchPreferences.getLong(SEARCH_BASE_DOMAIN_APPLY_TIME, -1) + + // Note that the only time this will return null is on the first run + // of the app, or when settings have been cleared. Callers should + // ideally check that getSearchBaseDomainApplyTime() is not -1 before + // calling this function. + @get:Override + @set:Override + override var searchBaseDomain: String? + get() = searchPreferences.getString(SEARCH_BASE_DOMAIN_PREF, null) + set(searchBaseUrl) { + val sharedPrefEditor: Editor = searchPreferences.edit() + sharedPrefEditor.putString(SEARCH_BASE_DOMAIN_PREF, searchBaseUrl) + sharedPrefEditor.putLong(SEARCH_BASE_DOMAIN_APPLY_TIME, System.currentTimeMillis()) + SharedPreferencesCompat.apply(sharedPrefEditor) + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.SearchSettingsImpl" + + // Name of the preferences file used to store search preference + const val PREFERENCES_NAME = "SearchSettings" + + /** Preference key used for storing the index of the next voice search hint to show. */ + private const val NEXT_VOICE_SEARCH_HINT_INDEX_PREF = "next_voice_search_hint" + + /** Preference key used to store the time at which the first voice search hint was displayed. */ + private const val FIRST_VOICE_HINT_DISPLAY_TIME = "first_voice_search_hint_time" + + /** Preference key for the version of voice search we last got hints from. */ + private const val LAST_SEEN_VOICE_SEARCH_VERSION = "voice_search_version" + + /** + * Preference key for storing whether searches always go to google.com. Public so that it can be + * used by PreferenceControllers. + */ + const val USE_GOOGLE_COM_PREF = "use_google_com" + + /** + * Preference key for the base search URL. This value is normally set by a SearchBaseUrlHelper + * instance. Public so classes can listen to changes on this key. + */ + const val SEARCH_BASE_DOMAIN_PREF = "search_base_domain" + + /** + * This is the time at which the base URL was stored, and is set using + * @link{System.currentTimeMillis()}. + */ + private const val SEARCH_BASE_DOMAIN_APPLY_TIME = "search_base_domain_apply_time" + } + + init { + mContext = context + this.config = config + } +} diff --git a/src/com/android/quicksearchbox/SearchWidgetProvider.java b/src/com/android/quicksearchbox/SearchWidgetProvider.java deleted file mode 100644 index fdd8cf3..0000000 --- a/src/com/android/quicksearchbox/SearchWidgetProvider.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import com.android.common.Search; -import com.android.common.speech.Recognition; -import com.android.quicksearchbox.util.Util; - -import android.app.Activity; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.app.SearchManager; -import android.appwidget.AppWidgetManager; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.Typeface; -import android.net.Uri; -import android.os.Bundle; -import android.os.SystemClock; -import android.speech.RecognizerIntent; -import android.text.Annotation; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.style.StyleSpan; -import android.util.Log; -import android.view.View; -import android.widget.RemoteViews; - -import java.util.ArrayList; -import java.util.Random; - -/** - * Search widget provider. - * - */ -public class SearchWidgetProvider extends BroadcastReceiver { - - private static final boolean DBG = false; - private static final String TAG = "QSB.SearchWidgetProvider"; - - /** - * The {@link Search#SOURCE} value used when starting searches from the search widget. - */ - private static final String WIDGET_SEARCH_SOURCE = "launcher-widget"; - - @Override - public void onReceive(Context context, Intent intent) { - if (DBG) Log.d(TAG, "onReceive(" + intent.toUri(0) + ")"); - String action = intent.getAction(); - if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) { - // nothing needs doing - } else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) { - updateSearchWidgets(context); - } else { - if (DBG) Log.d(TAG, "Unhandled intent action=" + action); - } - } - - private static SearchWidgetState[] getSearchWidgetStates(Context context) { - AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); - int[] appWidgetIds = appWidgetManager.getAppWidgetIds(myComponentName(context)); - SearchWidgetState[] states = new SearchWidgetState[appWidgetIds.length]; - for (int i = 0; i<appWidgetIds.length; ++i) { - states[i] = getSearchWidgetState(context, appWidgetIds[i]); - } - return states; - } - - - /** - * Updates all search widgets. - */ - public static void updateSearchWidgets(Context context) { - if (DBG) Log.d(TAG, "updateSearchWidgets"); - SearchWidgetState[] states = getSearchWidgetStates(context); - - for (SearchWidgetState state : states) { - state.updateWidget(context, AppWidgetManager.getInstance(context)); - } - } - - /** - * Gets the component name of this search widget provider. - */ - private static ComponentName myComponentName(Context context) { - String pkg = context.getPackageName(); - String cls = pkg + ".SearchWidgetProvider"; - return new ComponentName(pkg, cls); - } - - private static Intent createQsbActivityIntent(Context context, String action, - Bundle widgetAppData) { - Intent qsbIntent = new Intent(action); - qsbIntent.setPackage(context.getPackageName()); - qsbIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_CLEAR_TOP - | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - qsbIntent.putExtra(SearchManager.APP_DATA, widgetAppData); - return qsbIntent; - } - - private static SearchWidgetState getSearchWidgetState(Context context, int appWidgetId) { - if (DBG) Log.d(TAG, "Creating appwidget state " + appWidgetId); - SearchWidgetState state = new SearchWidgetState(appWidgetId); - - Bundle widgetAppData = new Bundle(); - widgetAppData.putString(Search.SOURCE, WIDGET_SEARCH_SOURCE); - - // Text field click - Intent qsbIntent = createQsbActivityIntent( - context, - SearchManager.INTENT_ACTION_GLOBAL_SEARCH, - widgetAppData); - state.setQueryTextViewIntent(qsbIntent); - - // Voice search button - Intent voiceSearchIntent = getVoiceSearchIntent(context, widgetAppData); - state.setVoiceSearchIntent(voiceSearchIntent); - - return state; - } - - private static Intent getVoiceSearchIntent(Context context, Bundle widgetAppData) { - VoiceSearch voiceSearch = QsbApplication.get(context).getVoiceSearch(); - return voiceSearch.createVoiceWebSearchIntent(widgetAppData); - } - - private static class SearchWidgetState { - private final int mAppWidgetId; - private Intent mQueryTextViewIntent; - private Intent mVoiceSearchIntent; - - public SearchWidgetState(int appWidgetId) { - mAppWidgetId = appWidgetId; - } - - public void setQueryTextViewIntent(Intent queryTextViewIntent) { - mQueryTextViewIntent = queryTextViewIntent; - } - - public void setVoiceSearchIntent(Intent voiceSearchIntent) { - mVoiceSearchIntent = voiceSearchIntent; - } - - public void updateWidget(Context context,AppWidgetManager appWidgetMgr) { - if (DBG) Log.d(TAG, "Updating appwidget " + mAppWidgetId); - RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.search_widget); - - setOnClickActivityIntent(context, views, R.id.search_widget_text, - mQueryTextViewIntent); - // Voice Search button - if (mVoiceSearchIntent != null) { - setOnClickActivityIntent(context, views, R.id.search_widget_voice_btn, - mVoiceSearchIntent); - views.setViewVisibility(R.id.search_widget_voice_btn, View.VISIBLE); - } else { - views.setViewVisibility(R.id.search_widget_voice_btn, View.GONE); - } - - appWidgetMgr.updateAppWidget(mAppWidgetId, views); - } - - private void setOnClickActivityIntent(Context context, RemoteViews views, int viewId, - Intent intent) { - intent.setPackage(context.getPackageName()); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); - views.setOnClickPendingIntent(viewId, pendingIntent); - } - } - -} diff --git a/src/com/android/quicksearchbox/SearchWidgetProvider.kt b/src/com/android/quicksearchbox/SearchWidgetProvider.kt new file mode 100644 index 0000000..54588ec --- /dev/null +++ b/src/com/android/quicksearchbox/SearchWidgetProvider.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.PendingIntent +import android.app.SearchManager +import android.appwidget.AppWidgetManager +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.RemoteViews +import com.android.common.Search + +/** Search widget provider. */ +class SearchWidgetProvider : BroadcastReceiver() { + @Override + override fun onReceive(context: Context?, intent: Intent) { + if (DBG) Log.d(TAG, "onReceive(" + intent.toUri(0).toString() + ")") + val action: String? = intent.getAction() + if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) { + // nothing needs doing + } else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) { + updateSearchWidgets(context) + } else { + if (DBG) Log.d(TAG, "Unhandled intent action=$action") + } + } + + private class SearchWidgetState(private val mAppWidgetId: Int) { + private var mQueryTextViewIntent: Intent? = null + private var mVoiceSearchIntent: Intent? = null + fun setQueryTextViewIntent(queryTextViewIntent: Intent?) { + mQueryTextViewIntent = queryTextViewIntent + } + + fun setVoiceSearchIntent(voiceSearchIntent: Intent?) { + mVoiceSearchIntent = voiceSearchIntent + } + + fun updateWidget(context: Context?, appWidgetMgr: AppWidgetManager) { + if (DBG) Log.d(TAG, "Updating appwidget $mAppWidgetId") + val views = RemoteViews(context!!.getPackageName(), R.layout.search_widget) + setOnClickActivityIntent(context, views, R.id.search_widget_text, mQueryTextViewIntent) + // Voice Search button + if (mVoiceSearchIntent != null) { + setOnClickActivityIntent(context, views, R.id.search_widget_voice_btn, mVoiceSearchIntent) + views.setViewVisibility(R.id.search_widget_voice_btn, View.VISIBLE) + } else { + views.setViewVisibility(R.id.search_widget_voice_btn, View.GONE) + } + appWidgetMgr.updateAppWidget(mAppWidgetId, views) + } + + private fun setOnClickActivityIntent( + context: Context?, + views: RemoteViews, + viewId: Int, + intent: Intent? + ) { + intent?.setPackage(context?.getPackageName()) + val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0) + views.setOnClickPendingIntent(viewId, pendingIntent) + } + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.SearchWidgetProvider" + + /** The [Search.SOURCE] value used when starting searches from the search widget. */ + private const val WIDGET_SEARCH_SOURCE = "launcher-widget" + private fun getSearchWidgetStates(context: Context?): Array<SearchWidgetState?> { + val appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context) + val appWidgetIds: IntArray = appWidgetManager.getAppWidgetIds(myComponentName(context)) + val states: Array<SearchWidgetState?> = arrayOfNulls(appWidgetIds.size) + for (i in appWidgetIds.indices) { + states[i] = getSearchWidgetState(context, appWidgetIds[i]) + } + return states + } + + /** Updates all search widgets. */ + @JvmStatic + fun updateSearchWidgets(context: Context?) { + if (DBG) Log.d(TAG, "updateSearchWidgets") + val states: Array<SearchWidgetState?> = getSearchWidgetStates(context) + for (state in states) { + state?.updateWidget(context, AppWidgetManager.getInstance(context)) + } + } + + /** Gets the component name of this search widget provider. */ + private fun myComponentName(context: Context?): ComponentName { + val pkg: String = context!!.getPackageName() + val cls = "$pkg.SearchWidgetProvider" + return ComponentName(pkg, cls) + } + + private fun createQsbActivityIntent( + context: Context?, + action: String, + widgetAppData: Bundle + ): Intent { + val qsbIntent = Intent(action) + qsbIntent.setPackage(context?.getPackageName()) + qsbIntent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + ) + qsbIntent.putExtra(SearchManager.APP_DATA, widgetAppData) + return qsbIntent + } + + private fun getSearchWidgetState(context: Context?, appWidgetId: Int): SearchWidgetState { + if (DBG) Log.d(TAG, "Creating appwidget state $appWidgetId") + val state: SearchWidgetState = SearchWidgetState(appWidgetId) + val widgetAppData = Bundle() + widgetAppData.putString(Search.SOURCE, WIDGET_SEARCH_SOURCE) + + // Text field click + val qsbIntent: Intent = + createQsbActivityIntent(context, SearchManager.INTENT_ACTION_GLOBAL_SEARCH, widgetAppData) + state.setQueryTextViewIntent(qsbIntent) + + // Voice search button + val voiceSearchIntent: Intent? = getVoiceSearchIntent(context, widgetAppData) + state.setVoiceSearchIntent(voiceSearchIntent) + return state + } + + private fun getVoiceSearchIntent(context: Context?, widgetAppData: Bundle): Intent? { + val voiceSearch: VoiceSearch? = QsbApplication[context].voiceSearch + return voiceSearch?.createVoiceWebSearchIntent(widgetAppData) + } + } +} diff --git a/src/com/android/quicksearchbox/Source.java b/src/com/android/quicksearchbox/Source.java deleted file mode 100644 index 680f415..0000000 --- a/src/com/android/quicksearchbox/Source.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import com.android.quicksearchbox.util.NowOrLater; - -import android.content.ComponentName; -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; - -/** - * Interface for suggestion sources. - * - */ -public interface Source extends SuggestionCursorProvider<SourceResult> { - - /** - * Gets the name activity that intents from this source are sent to. - */ - ComponentName getIntentComponent(); - - /** - * Gets the suggestion URI for getting suggestions from this Source. - */ - String getSuggestUri(); - - /** - * Gets the localized, human-readable label for this source. - */ - CharSequence getLabel(); - - /** - * Gets the icon for this suggestion source. - */ - Drawable getSourceIcon(); - - /** - * Gets the icon URI for this suggestion source. - */ - Uri getSourceIconUri(); - - /** - * Gets an icon from this suggestion source. - * - * @param drawableId Resource ID or URI. - */ - NowOrLater<Drawable> getIcon(String drawableId); - - /** - * Gets the URI for an icon form this suggestion source. - * - * @param drawableId Resource ID or URI. - */ - Uri getIconUri(String drawableId); - - /** - * Gets the search hint text for this suggestion source. - */ - CharSequence getHint(); - - /** - * Gets the description to use for this source in system search settings. - */ - CharSequence getSettingsDescription(); - - /** - * - * Note: this does not guarantee that this source will be queried for queries of - * this length or longer, only that it will not be queried for anything shorter. - * - * @return The minimum number of characters needed to trigger this source. - */ - int getQueryThreshold(); - - /** - * Indicates whether a source should be invoked for supersets of queries it has returned zero - * results for in the past. For example, if a source returned zero results for "bo", it would - * be ignored for "bob". - * - * If set to <code>false</code>, this source will only be ignored for a single session; the next - * time the search dialog is brought up, all sources will be queried. - * - * @return <code>true</code> if this source should be queried after returning no results. - */ - boolean queryAfterZeroResults(); - - boolean voiceSearchEnabled(); - - /** - * Whether this source should be included in the blended All mode. The source must - * also be enabled to be included in All. - */ - boolean includeInAll(); - - Intent createSearchIntent(String query, Bundle appData); - - Intent createVoiceSearchIntent(Bundle appData); - - /** - * Checks if the current process can read the suggestions from this source. - */ - boolean canRead(); - - /** - * Gets suggestions from this source. - * - * @param query The user query. - * @return The suggestion results. - */ - @Override - SourceResult getSuggestions(String query, int queryLimit); - - /** - * Gets the default intent action for suggestions from this source. - * - * @return The default intent action, or {@code null}. - */ - String getDefaultIntentAction(); - - /** - * Gets the default intent data for suggestions from this source. - * - * @return The default intent data, or {@code null}. - */ - String getDefaultIntentData(); - - /** - * Gets the root source, if this source is a wrapper around another. Otherwise, returns this - * source. - */ - Source getRoot(); - -} diff --git a/src/com/android/quicksearchbox/Source.kt b/src/com/android/quicksearchbox/Source.kt new file mode 100644 index 0000000..cdc0a79 --- /dev/null +++ b/src/com/android/quicksearchbox/Source.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.ComponentName +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import com.android.quicksearchbox.util.NowOrLater + +/** Interface for suggestion sources. */ +interface Source : SuggestionCursorProvider<com.android.quicksearchbox.SourceResult?> { + /** Gets the name activity that intents from this source are sent to. */ + val intentComponent: ComponentName? + + /** Gets the suggestion URI for getting suggestions from this Source. */ + val suggestUri: String? + + /** Gets the localized, human-readable label for this source. */ + val label: CharSequence? + + /** Gets the icon for this suggestion source. */ + val sourceIcon: Drawable? + + /** Gets the icon URI for this suggestion source. */ + val sourceIconUri: Uri? + + /** + * Gets an icon from this suggestion source. + * + * @param drawableId Resource ID or URI. + */ + fun getIcon(drawableId: String?): NowOrLater<Drawable?>? + + /** + * Gets the URI for an icon form this suggestion source. + * + * @param drawableId Resource ID or URI. + */ + fun getIconUri(drawableId: String?): Uri? + + /** Gets the search hint text for this suggestion source. */ + val hint: CharSequence? + + /** Gets the description to use for this source in system search settings. */ + val settingsDescription: CharSequence? + + /** + * + * Note: this does not guarantee that this source will be queried for queries of this length or + * longer, only that it will not be queried for anything shorter. + * + * @return The minimum number of characters needed to trigger this source. + */ + val queryThreshold: Int + + /** + * Indicates whether a source should be invoked for supersets of queries it has returned zero + * results for in the past. For example, if a source returned zero results for "bo", it would be + * ignored for "bob". + * + * If set to `false`, this source will only be ignored for a single session; the next time the + * search dialog is brought up, all sources will be queried. + * + * @return `true` if this source should be queried after returning no results. + */ + fun queryAfterZeroResults(): Boolean + fun voiceSearchEnabled(): Boolean + + /** + * Whether this source should be included in the blended All mode. The source must also be enabled + * to be included in All. + */ + fun includeInAll(): Boolean + fun createSearchIntent(query: String?, appData: Bundle?): Intent? + fun createVoiceSearchIntent(appData: Bundle?): Intent? + + /** Checks if the current process can read the suggestions from this source. */ + fun canRead(): Boolean + + /** + * Gets suggestions from this source. + * + * @param query The user query. + * @return The suggestion results. + */ + @Override override fun getSuggestions(query: String?, queryLimit: Int): SourceResult? + + /** + * Gets the default intent action for suggestions from this source. + * + * @return The default intent action, or `null`. + */ + val defaultIntentAction: String? + + /** + * Gets the default intent data for suggestions from this source. + * + * @return The default intent data, or `null`. + */ + val defaultIntentData: String? + + /** + * Gets the root source, if this source is a wrapper around another. Otherwise, returns this + * source. + */ + fun getRoot(): Source +} diff --git a/src/com/android/quicksearchbox/SourceResult.java b/src/com/android/quicksearchbox/SourceResult.kt index 20ea48f..b5baae3 100644 --- a/src/com/android/quicksearchbox/SourceResult.java +++ b/src/com/android/quicksearchbox/SourceResult.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * Copyright (C) 2022 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. @@ -13,14 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.quicksearchbox -package com.android.quicksearchbox; - -/** - * The result of getting suggestions from a single source. - */ -public interface SourceResult extends SuggestionCursor { - - Source getSource(); - +/** The result of getting suggestions from a single source. */ +interface SourceResult : SuggestionCursor { + val source: Source? } diff --git a/src/com/android/quicksearchbox/Suggestion.java b/src/com/android/quicksearchbox/Suggestion.java deleted file mode 100644 index 81f5578..0000000 --- a/src/com/android/quicksearchbox/Suggestion.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.content.ComponentName; - -/** - * Interface for individual suggestions. - */ -public interface Suggestion { - - /** - * Gets the source that produced the current suggestion. - */ - Source getSuggestionSource(); - - /** - * Gets the shortcut ID of the current suggestion. - */ - String getShortcutId(); - - /** - * Whether to show a spinner while refreshing this shortcut. - */ - boolean isSpinnerWhileRefreshing(); - - /** - * Gets the format of the text returned by {@link #getSuggestionText1()} - * and {@link #getSuggestionText2()}. - * - * @return {@code null} or "html" - */ - String getSuggestionFormat(); - - /** - * Gets the first text line for the current suggestion. - */ - String getSuggestionText1(); - - /** - * Gets the second text line for the current suggestion. - */ - String getSuggestionText2(); - - /** - * Gets the second text line URL for the current suggestion. - */ - String getSuggestionText2Url(); - - /** - * Gets the left-hand-side icon for the current suggestion. - * - * @return A string that can be passed to {@link Source#getIcon(String)}. - */ - String getSuggestionIcon1(); - - /** - * Gets the right-hand-side icon for the current suggestion. - * - * @return A string that can be passed to {@link Source#getIcon(String)}. - */ - String getSuggestionIcon2(); - - /** - * Gets the intent action for the current suggestion. - */ - String getSuggestionIntentAction(); - - /** - * Gets the name of the activity that the intent for the current suggestion will be sent to. - */ - ComponentName getSuggestionIntentComponent(); - - /** - * Gets the extra data associated with this suggestion's intent. - */ - String getSuggestionIntentExtraData(); - - /** - * Gets the data associated with this suggestion's intent. - */ - String getSuggestionIntentDataString(); - - /** - * Gets the query associated with this suggestion's intent. - */ - String getSuggestionQuery(); - - /** - * Gets the suggestion log type for the current suggestion. This is logged together - * with the value returned from {@link Source#getName()}. - * The value is source-specific. Most sources return {@code null}. - */ - String getSuggestionLogType(); - - /** - * Checks if this suggestion is a shortcut. - */ - boolean isSuggestionShortcut(); - - /** - * Checks if this is a web search suggestion. - */ - boolean isWebSearchSuggestion(); - - /** - * Checks whether this suggestion comes from the user's search history. - */ - boolean isHistorySuggestion(); - - /** - * Returns any extras associated with this suggestion, or {@code null} if there are none. - */ - SuggestionExtras getExtras(); - -} diff --git a/src/com/android/quicksearchbox/Suggestion.kt b/src/com/android/quicksearchbox/Suggestion.kt new file mode 100644 index 0000000..2f82a15 --- /dev/null +++ b/src/com/android/quicksearchbox/Suggestion.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.ComponentName + +/** Interface for individual suggestions. */ +interface Suggestion { + /** Gets the source that produced the current suggestion. */ + val suggestionSource: com.android.quicksearchbox.Source? + + /** Gets the shortcut ID of the current suggestion. */ + val shortcutId: String? + + /** Whether to show a spinner while refreshing this shortcut. */ + val isSpinnerWhileRefreshing: Boolean + + /** + * Gets the format of the text returned by [.getSuggestionText1] and [.getSuggestionText2]. + * + * @return `null` or "html" + */ + val suggestionFormat: String? + + /** Gets the first text line for the current suggestion. */ + val suggestionText1: String? + + /** Gets the second text line for the current suggestion. */ + val suggestionText2: String? + + /** Gets the second text line URL for the current suggestion. */ + val suggestionText2Url: String? + + /** + * Gets the left-hand-side icon for the current suggestion. + * + * @return A string that can be passed to [Source.getIcon]. + */ + val suggestionIcon1: String? + + /** + * Gets the right-hand-side icon for the current suggestion. + * + * @return A string that can be passed to [Source.getIcon]. + */ + val suggestionIcon2: String? + + /** Gets the intent action for the current suggestion. */ + val suggestionIntentAction: String? + + /** Gets the name of the activity that the intent for the current suggestion will be sent to. */ + val suggestionIntentComponent: ComponentName? + + /** Gets the extra data associated with this suggestion's intent. */ + val suggestionIntentExtraData: String? + + /** Gets the data associated with this suggestion's intent. */ + val suggestionIntentDataString: String? + + /** Gets the query associated with this suggestion's intent. */ + val suggestionQuery: String? + + /** + * Gets the suggestion log type for the current suggestion. This is logged together with the value + * returned from [Source.getName]. The value is source-specific. Most sources return `null`. + */ + val suggestionLogType: String? + + /** Checks if this suggestion is a shortcut. */ + val isSuggestionShortcut: Boolean + + /** Checks if this is a web search suggestion. */ + val isWebSearchSuggestion: Boolean + + /** Checks whether this suggestion comes from the user's search history. */ + val isHistorySuggestion: Boolean + + /** Returns any extras associated with this suggestion, or `null` if there are none. */ + val extras: com.android.quicksearchbox.SuggestionExtras? +} diff --git a/src/com/android/quicksearchbox/SuggestionCursor.java b/src/com/android/quicksearchbox/SuggestionCursor.java deleted file mode 100644 index 04d53c8..0000000 --- a/src/com/android/quicksearchbox/SuggestionCursor.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import com.android.quicksearchbox.util.QuietlyCloseable; - -import android.database.DataSetObserver; - -import java.util.Collection; - -/** - * A sequence of suggestions, with a current position. - */ -public interface SuggestionCursor extends Suggestion, QuietlyCloseable { - - /** - * Gets the query that the user typed to get this suggestion. - */ - String getUserQuery(); - - /** - * Gets the number of suggestions in this result. - * - * @return The number of suggestions, or {@code 0} if this result represents a failed query. - */ - int getCount(); - - /** - * Moves to a given suggestion. - * - * @param pos The position to move to. - * @throws IndexOutOfBoundsException if {@code pos < 0} or {@code pos >= getCount()}. - */ - void moveTo(int pos); - - /** - * Moves to the next suggestion, if there is one. - * - * @return {@code false} if there is no next suggestion. - */ - boolean moveToNext(); - - /** - * Gets the current position within the cursor. - */ - int getPosition(); - - /** - * Frees any resources used by this cursor. - */ - @Override - void close(); - - /** - * Register an observer that is called when changes happen to this data set. - * - * @param observer gets notified when the data set changes. - */ - void registerDataSetObserver(DataSetObserver observer); - - /** - * Unregister an observer that has previously been registered with - * {@link #registerDataSetObserver(DataSetObserver)} - * - * @param observer the observer to unregister. - */ - void unregisterDataSetObserver(DataSetObserver observer); - - /** - * Return the extra columns present in this cursor, or null if none exist. - */ - Collection<String> getExtraColumns(); -} diff --git a/src/com/android/quicksearchbox/SuggestionCursor.kt b/src/com/android/quicksearchbox/SuggestionCursor.kt new file mode 100644 index 0000000..40c704b --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionCursor.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.database.DataSetObserver +import com.android.quicksearchbox.util.QuietlyCloseable +import kotlin.collections.Collection + +/** A sequence of suggestions, with a current position. */ +interface SuggestionCursor : Suggestion, QuietlyCloseable { + /** Gets the query that the user typed to get this suggestion. */ + val userQuery: String? + + /** + * Gets the number of suggestions in this result. + * + * @return The number of suggestions, or `0` if this result represents a failed query. + */ + val count: Int + + /** + * Moves to a given suggestion. + * + * @param pos The position to move to. + * @throws IndexOutOfBoundsException if `pos < 0` or `pos >= getCount()`. + */ + fun moveTo(pos: Int) + + /** + * Moves to the next suggestion, if there is one. + * + * @return `false` if there is no next suggestion. + */ + fun moveToNext(): Boolean + + /** Gets the current position within the cursor. */ + val position: Int + + /** Frees any resources used by this cursor. */ + @Override override fun close() + + /** + * Register an observer that is called when changes happen to this data set. + * + * @param observer gets notified when the data set changes. + */ + fun registerDataSetObserver(observer: DataSetObserver?) + + /** + * Unregister an observer that has previously been registered with [.registerDataSetObserver] + * + * @param observer the observer to unregister. + */ + fun unregisterDataSetObserver(observer: DataSetObserver?) + + /** Return the extra columns present in this cursor, or null if none exist. */ + val extraColumns: Collection<String>? +} diff --git a/src/com/android/quicksearchbox/SuggestionCursorBackedCursor.java b/src/com/android/quicksearchbox/SuggestionCursorBackedCursor.java deleted file mode 100644 index 7e929c5..0000000 --- a/src/com/android/quicksearchbox/SuggestionCursorBackedCursor.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.app.SearchManager; -import android.database.AbstractCursor; -import android.database.CursorIndexOutOfBoundsException; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; - -public class SuggestionCursorBackedCursor extends AbstractCursor { - - // This array also used in CursorBackedSuggestionExtras to avoid duplication. - public static final String[] COLUMNS = { - "_id", // 0, This will contain the row number. CursorAdapter, used by SuggestionsAdapter, - // used by SearchDialog, expects an _id column. - SearchManager.SUGGEST_COLUMN_TEXT_1, // 1 - SearchManager.SUGGEST_COLUMN_TEXT_2, // 2 - SearchManager.SUGGEST_COLUMN_TEXT_2_URL, // 3 - SearchManager.SUGGEST_COLUMN_ICON_1, // 4 - SearchManager.SUGGEST_COLUMN_ICON_2, // 5 - SearchManager.SUGGEST_COLUMN_INTENT_ACTION, // 6 - SearchManager.SUGGEST_COLUMN_INTENT_DATA, // 7 - SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, // 8 - SearchManager.SUGGEST_COLUMN_QUERY, // 9 - SearchManager.SUGGEST_COLUMN_FORMAT, // 10 - SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, // 11 - SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING, // 12 - }; - - private static final int COLUMN_INDEX_ID = 0; - private static final int COLUMN_INDEX_TEXT1 = 1; - private static final int COLUMN_INDEX_TEXT2 = 2; - private static final int COLUMN_INDEX_TEXT2_URL = 3; - private static final int COLUMN_INDEX_ICON1 = 4; - private static final int COLUMN_INDEX_ICON2 = 5; - private static final int COLUMN_INDEX_INTENT_ACTION = 6; - private static final int COLUMN_INDEX_INTENT_DATA = 7; - private static final int COLUMN_INDEX_INTENT_EXTRA_DATA = 8; - private static final int COLUMN_INDEX_QUERY = 9; - private static final int COLUMN_INDEX_FORMAT = 10; - private static final int COLUMN_INDEX_SHORTCUT_ID = 11; - private static final int COLUMN_INDEX_SPINNER_WHILE_REFRESHING = 12; - - private final SuggestionCursor mCursor; - private ArrayList<String> mExtraColumns; - - public SuggestionCursorBackedCursor(SuggestionCursor cursor) { - mCursor = cursor; - } - - @Override - public void close() { - super.close(); - mCursor.close(); - } - - @Override - public String[] getColumnNames() { - Collection<String> extraColumns = mCursor.getExtraColumns(); - if (extraColumns != null) { - ArrayList<String> allColumns = new ArrayList<String>(COLUMNS.length + - extraColumns.size()); - mExtraColumns = new ArrayList<String>(extraColumns); - allColumns.addAll(Arrays.asList(COLUMNS)); - allColumns.addAll(mExtraColumns); - return allColumns.toArray(new String[allColumns.size()]); - } else { - return COLUMNS; - } - } - - @Override - public int getCount() { - return mCursor.getCount(); - } - - private Suggestion get() { - mCursor.moveTo(getPosition()); - return mCursor; - } - - private String getExtra(int columnIdx) { - int extraColumn = columnIdx - COLUMNS.length; - SuggestionExtras extras = get().getExtras(); - if (extras != null) { - return extras.getExtra(mExtraColumns.get(extraColumn)); - } else { - return null; - } - } - - @Override - public int getInt(int column) { - if (column == COLUMN_INDEX_ID) { - return getPosition(); - } else { - try { - return Integer.valueOf(getString(column)); - } catch (NumberFormatException e) { - return 0; - } - } - } - - @Override - public String getString(int column) { - if (column < COLUMNS.length) { - switch (column) { - case COLUMN_INDEX_ID: - return String.valueOf(getPosition()); - case COLUMN_INDEX_TEXT1: - return get().getSuggestionText1(); - case COLUMN_INDEX_TEXT2: - return get().getSuggestionText2(); - case COLUMN_INDEX_TEXT2_URL: - return get().getSuggestionText2Url(); - case COLUMN_INDEX_ICON1: - return get().getSuggestionIcon1(); - case COLUMN_INDEX_ICON2: - return get().getSuggestionIcon2(); - case COLUMN_INDEX_INTENT_ACTION: - return get().getSuggestionIntentAction(); - case COLUMN_INDEX_INTENT_DATA: - return get().getSuggestionIntentDataString(); - case COLUMN_INDEX_INTENT_EXTRA_DATA: - return get().getSuggestionIntentExtraData(); - case COLUMN_INDEX_QUERY: - return get().getSuggestionQuery(); - case COLUMN_INDEX_FORMAT: - return get().getSuggestionFormat(); - case COLUMN_INDEX_SHORTCUT_ID: - return get().getShortcutId(); - case COLUMN_INDEX_SPINNER_WHILE_REFRESHING: - return String.valueOf(get().isSpinnerWhileRefreshing()); - default: - throw new CursorIndexOutOfBoundsException("Requested column " + column - + " of " + COLUMNS.length); - } - } else { - return getExtra(column); - } - } - - @Override - public long getLong(int column) { - try { - return Long.valueOf(getString(column)); - } catch (NumberFormatException e) { - return 0; - } - } - - @Override - public boolean isNull(int column) { - return getString(column) == null; - } - - @Override - public short getShort(int column) { - try { - return Short.valueOf(getString(column)); - } catch (NumberFormatException e) { - return 0; - } - } - - @Override - public double getDouble(int column) { - try { - return Double.valueOf(getString(column)); - } catch (NumberFormatException e) { - return 0; - } - } - - @Override - public float getFloat(int column) { - try { - return Float.valueOf(getString(column)); - } catch (NumberFormatException e) { - return 0; - } - } -} diff --git a/src/com/android/quicksearchbox/SuggestionCursorBackedCursor.kt b/src/com/android/quicksearchbox/SuggestionCursorBackedCursor.kt new file mode 100644 index 0000000..9ee2b06 --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionCursorBackedCursor.kt @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.SearchManager +import android.database.AbstractCursor +import android.database.CursorIndexOutOfBoundsException +import kotlin.collections.ArrayList + +class SuggestionCursorBackedCursor(private val mCursor: SuggestionCursor?) : AbstractCursor() { + private var mExtraColumns: ArrayList<String>? = null + + @Override + override fun close() { + super.close() + mCursor?.close() + } + + @Override + override fun getColumnNames(): Array<String> { + val extraColumns: Collection<String>? = mCursor?.extraColumns + return if (extraColumns != null) { + val allColumns: ArrayList<String> = ArrayList<String>(COLUMNS.size + extraColumns.size) + mExtraColumns = ArrayList<String>(extraColumns) + allColumns.addAll(COLUMNS.asList()) + mExtraColumns?.let { allColumns.addAll(it) } + allColumns.toArray(arrayOfNulls<String>(allColumns.size)) + } else { + COLUMNS + } + } + + @Override + override fun getCount(): Int { + return mCursor!!.count + } + + private fun get(): SuggestionCursor? { + mCursor?.moveTo(position) + return mCursor + } + + private fun getExtra(columnIdx: Int): String? { + val extraColumn = columnIdx - COLUMNS.size + val extras: SuggestionExtras? = get()?.extras + return extras?.getExtra(mExtraColumns!!.get(extraColumn)) + } + + @Override + override fun getInt(column: Int): Int { + return if (column == COLUMN_INDEX_ID) { + position + } else { + try { + getString(column)!!.toInt() + } catch (e: NumberFormatException) { + 0 + } + } + } + + @Override + override fun getString(column: Int): String? { + return if (column < COLUMNS.size) { + when (column) { + COLUMN_INDEX_ID -> position.toString() + COLUMN_INDEX_TEXT1 -> get()?.suggestionText1 + COLUMN_INDEX_TEXT2 -> get()?.suggestionText2 + COLUMN_INDEX_TEXT2_URL -> get()?.suggestionText2Url + COLUMN_INDEX_ICON1 -> get()?.suggestionIcon1 + COLUMN_INDEX_ICON2 -> get()?.suggestionIcon2 + COLUMN_INDEX_INTENT_ACTION -> get()?.suggestionIntentAction + COLUMN_INDEX_INTENT_DATA -> get()?.suggestionIntentDataString + COLUMN_INDEX_INTENT_EXTRA_DATA -> get()?.suggestionIntentExtraData + COLUMN_INDEX_QUERY -> get()?.suggestionQuery + COLUMN_INDEX_FORMAT -> get()?.suggestionFormat + COLUMN_INDEX_SHORTCUT_ID -> get()?.shortcutId + COLUMN_INDEX_SPINNER_WHILE_REFRESHING -> get()?.isSpinnerWhileRefreshing.toString() + else -> + throw CursorIndexOutOfBoundsException( + "Requested column " + column + " of " + COLUMNS.size + ) + } + } else { + getExtra(column) + } + } + + @Override + override fun getLong(column: Int): Long { + return try { + getString(column)!!.toLong() + } catch (e: NumberFormatException) { + 0 + } + } + + @Override + override fun isNull(column: Int): Boolean { + return getString(column) == null + } + + @Override + override fun getShort(column: Int): Short { + return try { + getString(column)!!.toShort() + } catch (e: NumberFormatException) { + 0 + } + } + + @Override + override fun getDouble(column: Int): Double { + return try { + getString(column)!!.toDouble() + } catch (e: NumberFormatException) { + 0.0 + } + } + + @Override + override fun getFloat(column: Int): Float { + return try { + getString(column)!!.toFloat() + } catch (e: NumberFormatException) { + 0.0F + } + } + + companion object { + // This array also used in CursorBackedSuggestionExtras to avoid duplication. + val COLUMNS = + arrayOf( + "_id", // 0, This will contain the row number. CursorAdapter, used by SuggestionsAdapter, + // used by SearchDialog, expects an _id column. + SearchManager.SUGGEST_COLUMN_TEXT_1, // 1 + SearchManager.SUGGEST_COLUMN_TEXT_2, // 2 + SearchManager.SUGGEST_COLUMN_TEXT_2_URL, // 3 + SearchManager.SUGGEST_COLUMN_ICON_1, // 4 + SearchManager.SUGGEST_COLUMN_ICON_2, // 5 + SearchManager.SUGGEST_COLUMN_INTENT_ACTION, // 6 + SearchManager.SUGGEST_COLUMN_INTENT_DATA, // 7 + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, // 8 + SearchManager.SUGGEST_COLUMN_QUERY, // 9 + SearchManager.SUGGEST_COLUMN_FORMAT, // 10 + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, // 11 + SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING + ) + private const val COLUMN_INDEX_ID = 0 + private const val COLUMN_INDEX_TEXT1 = 1 + private const val COLUMN_INDEX_TEXT2 = 2 + private const val COLUMN_INDEX_TEXT2_URL = 3 + private const val COLUMN_INDEX_ICON1 = 4 + private const val COLUMN_INDEX_ICON2 = 5 + private const val COLUMN_INDEX_INTENT_ACTION = 6 + private const val COLUMN_INDEX_INTENT_DATA = 7 + private const val COLUMN_INDEX_INTENT_EXTRA_DATA = 8 + private const val COLUMN_INDEX_QUERY = 9 + private const val COLUMN_INDEX_FORMAT = 10 + private const val COLUMN_INDEX_SHORTCUT_ID = 11 + private const val COLUMN_INDEX_SPINNER_WHILE_REFRESHING = 12 + } +} diff --git a/src/com/android/quicksearchbox/SuggestionCursorProvider.java b/src/com/android/quicksearchbox/SuggestionCursorProvider.java deleted file mode 100644 index 23109cd..0000000 --- a/src/com/android/quicksearchbox/SuggestionCursorProvider.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - - -/** - * Interface for objects that can produce a SuggestionCursor given a query. - */ -public interface SuggestionCursorProvider<C extends SuggestionCursor> { - - /** - * Gets the name of the provider. This is used for logging and - * to execute tasks on the queue for the provider. - */ - String getName(); - - /** - * Gets suggestions from the provider. - * - * @param query The user query. - * @param queryLimit An advisory maximum number of results that the source should return. - * @return The suggestion results. Must not be {@code null}. - */ - C getSuggestions(String query, int queryLimit); -} diff --git a/src/com/android/quicksearchbox/SuggestionCursorProvider.kt b/src/com/android/quicksearchbox/SuggestionCursorProvider.kt new file mode 100644 index 0000000..3ea7b93 --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionCursorProvider.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +/** Interface for objects that can produce a SuggestionCursor given a query. */ +interface SuggestionCursorProvider<C : SuggestionCursor?> { + /** + * Gets the name of the provider. This is used for logging and to execute tasks on the queue for + * the provider. + */ + val name: String? + + /** + * Gets suggestions from the provider. + * + * @param query The user query. + * @param queryLimit An advisory maximum number of results that the source should return. + * @return The suggestion results. Must not be `null`. + */ + fun getSuggestions(query: String?, queryLimit: Int): C? +} diff --git a/src/com/android/quicksearchbox/SuggestionCursorWrapper.java b/src/com/android/quicksearchbox/SuggestionCursorWrapper.java deleted file mode 100644 index 83e74f4..0000000 --- a/src/com/android/quicksearchbox/SuggestionCursorWrapper.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.database.DataSetObserver; - -import java.util.Collection; - -/** - * A suggestion cursor that delegates all methods to another SuggestionCursor. - */ -public class SuggestionCursorWrapper extends AbstractSuggestionCursorWrapper { - - private final SuggestionCursor mCursor; - - public SuggestionCursorWrapper(String userQuery, SuggestionCursor cursor) { - super(userQuery); - mCursor = cursor; - } - - public void close() { - if (mCursor != null) { - mCursor.close(); - } - } - - public int getCount() { - return mCursor == null ? 0 : mCursor.getCount(); - } - - public int getPosition() { - return mCursor == null ? 0 : mCursor.getPosition(); - } - - public void moveTo(int pos) { - if (mCursor != null) { - mCursor.moveTo(pos); - } - } - - public boolean moveToNext() { - if (mCursor != null) { - return mCursor.moveToNext(); - } else { - return false; - } - } - - public void registerDataSetObserver(DataSetObserver observer) { - if (mCursor != null) { - mCursor.registerDataSetObserver(observer); - } - } - - public void unregisterDataSetObserver(DataSetObserver observer) { - if (mCursor != null) { - mCursor.unregisterDataSetObserver(observer); - } - } - - @Override - protected SuggestionCursor current() { - return mCursor; - } - - public Collection<String> getExtraColumns() { - return mCursor.getExtraColumns(); - } - -} diff --git a/src/com/android/quicksearchbox/SuggestionCursorWrapper.kt b/src/com/android/quicksearchbox/SuggestionCursorWrapper.kt new file mode 100644 index 0000000..c09ea1a --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionCursorWrapper.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.database.DataSetObserver + +/** A suggestion cursor that delegates all methods to another SuggestionCursor. */ +open class SuggestionCursorWrapper(userQuery: String?, private val mCursor: SuggestionCursor?) : + AbstractSuggestionCursorWrapper(userQuery!!) { + override fun close() { + if (mCursor != null) { + mCursor.close() + } + } + + override val count: Int + get() = if (mCursor == null) 0 else mCursor.count + override val position: Int + get() = if (mCursor == null) 0 else mCursor.position + + override fun moveTo(pos: Int) { + if (mCursor != null) { + mCursor.moveTo(pos) + } + } + + override fun moveToNext(): Boolean { + return mCursor?.moveToNext() ?: false + } + + override fun registerDataSetObserver(observer: DataSetObserver?) { + if (mCursor != null) { + mCursor.registerDataSetObserver(observer) + } + } + + override fun unregisterDataSetObserver(observer: DataSetObserver?) { + if (mCursor != null) { + mCursor.unregisterDataSetObserver(observer) + } + } + + @Override + override fun current(): SuggestionCursor { + return mCursor!! + } + + override val extraColumns: Collection<String>? + get() = mCursor?.extraColumns +} diff --git a/src/com/android/quicksearchbox/SuggestionData.java b/src/com/android/quicksearchbox/SuggestionData.java deleted file mode 100644 index 3cc835d..0000000 --- a/src/com/android/quicksearchbox/SuggestionData.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import com.google.common.annotations.VisibleForTesting; - -import android.content.ComponentName; -import android.content.Intent; - - -/** - * Holds data for each suggest item including the display data and how to launch the result. - * Used for passing from the provider to the suggest cursor. - */ -public class SuggestionData implements Suggestion { - - private final Source mSource; - private String mFormat; - private String mText1; - private String mText2; - private String mText2Url; - private String mIcon1; - private String mIcon2; - private String mShortcutId; - private boolean mSpinnerWhileRefreshing; - private String mIntentAction; - private String mIntentData; - private String mIntentExtraData; - private String mSuggestionQuery; - private String mLogType; - private boolean mIsShortcut; - private boolean mIsHistory; - private SuggestionExtras mExtras; - - public SuggestionData(Source source) { - mSource = source; - } - - public Source getSuggestionSource() { - return mSource; - } - - public String getSuggestionFormat() { - return mFormat; - } - - public String getSuggestionText1() { - return mText1; - } - - public String getSuggestionText2() { - return mText2; - } - - public String getSuggestionText2Url() { - return mText2Url; - } - - public String getSuggestionIcon1() { - return mIcon1; - } - - public String getSuggestionIcon2() { - return mIcon2; - } - - public boolean isSpinnerWhileRefreshing() { - return mSpinnerWhileRefreshing; - } - - public String getIntentExtraData() { - return mIntentExtraData; - } - - public String getShortcutId() { - return mShortcutId; - } - - public String getSuggestionIntentAction() { - if (mIntentAction != null) return mIntentAction; - return mSource.getDefaultIntentAction(); - } - - public ComponentName getSuggestionIntentComponent() { - return mSource.getIntentComponent(); - } - - public String getSuggestionIntentDataString() { - return mIntentData; - } - - public String getSuggestionIntentExtraData() { - return mIntentExtraData; - } - - public String getSuggestionQuery() { - return mSuggestionQuery; - } - - public String getSuggestionLogType() { - return mLogType; - } - - public boolean isSuggestionShortcut() { - return mIsShortcut; - } - - public boolean isWebSearchSuggestion() { - return Intent.ACTION_WEB_SEARCH.equals(getSuggestionIntentAction()); - } - - public boolean isHistorySuggestion() { - return mIsHistory; - } - - @VisibleForTesting - public SuggestionData setFormat(String format) { - mFormat = format; - return this; - } - - @VisibleForTesting - public SuggestionData setText1(String text1) { - mText1 = text1; - return this; - } - - @VisibleForTesting - public SuggestionData setText2(String text2) { - mText2 = text2; - return this; - } - - @VisibleForTesting - public SuggestionData setText2Url(String text2Url) { - mText2Url = text2Url; - return this; - } - - @VisibleForTesting - public SuggestionData setIcon1(String icon1) { - mIcon1 = icon1; - return this; - } - - @VisibleForTesting - public SuggestionData setIcon2(String icon2) { - mIcon2 = icon2; - return this; - } - - @VisibleForTesting - public SuggestionData setIntentAction(String intentAction) { - mIntentAction = intentAction; - return this; - } - - @VisibleForTesting - public SuggestionData setIntentData(String intentData) { - mIntentData = intentData; - return this; - } - - @VisibleForTesting - public SuggestionData setIntentExtraData(String intentExtraData) { - mIntentExtraData = intentExtraData; - return this; - } - - @VisibleForTesting - public SuggestionData setSuggestionQuery(String suggestionQuery) { - mSuggestionQuery = suggestionQuery; - return this; - } - - @VisibleForTesting - public SuggestionData setShortcutId(String shortcutId) { - mShortcutId = shortcutId; - return this; - } - - @VisibleForTesting - public SuggestionData setSpinnerWhileRefreshing(boolean spinnerWhileRefreshing) { - mSpinnerWhileRefreshing = spinnerWhileRefreshing; - return this; - } - - @VisibleForTesting - public SuggestionData setSuggestionLogType(String logType) { - mLogType = logType; - return this; - } - - @VisibleForTesting - public SuggestionData setIsShortcut(boolean isShortcut) { - mIsShortcut = isShortcut; - return this; - } - - @VisibleForTesting - public SuggestionData setIsHistory(boolean isHistory) { - mIsHistory = isHistory; - return this; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mFormat == null) ? 0 : mFormat.hashCode()); - result = prime * result + ((mIcon1 == null) ? 0 : mIcon1.hashCode()); - result = prime * result + ((mIcon2 == null) ? 0 : mIcon2.hashCode()); - result = prime * result + ((mIntentAction == null) ? 0 : mIntentAction.hashCode()); - result = prime * result + ((mIntentData == null) ? 0 : mIntentData.hashCode()); - result = prime * result + ((mIntentExtraData == null) ? 0 : mIntentExtraData.hashCode()); - result = prime * result + ((mLogType == null) ? 0 : mLogType.hashCode()); - result = prime * result + ((mShortcutId == null) ? 0 : mShortcutId.hashCode()); - result = prime * result + ((mSource == null) ? 0 : mSource.hashCode()); - result = prime * result + (mSpinnerWhileRefreshing ? 1231 : 1237); - result = prime * result + ((mSuggestionQuery == null) ? 0 : mSuggestionQuery.hashCode()); - result = prime * result + ((mText1 == null) ? 0 : mText1.hashCode()); - result = prime * result + ((mText2 == null) ? 0 : mText2.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - SuggestionData other = (SuggestionData)obj; - if (mFormat == null) { - if (other.mFormat != null) - return false; - } else if (!mFormat.equals(other.mFormat)) - return false; - if (mIcon1 == null) { - if (other.mIcon1 != null) - return false; - } else if (!mIcon1.equals(other.mIcon1)) - return false; - if (mIcon2 == null) { - if (other.mIcon2 != null) - return false; - } else if (!mIcon2.equals(other.mIcon2)) - return false; - if (mIntentAction == null) { - if (other.mIntentAction != null) - return false; - } else if (!mIntentAction.equals(other.mIntentAction)) - return false; - if (mIntentData == null) { - if (other.mIntentData != null) - return false; - } else if (!mIntentData.equals(other.mIntentData)) - return false; - if (mIntentExtraData == null) { - if (other.mIntentExtraData != null) - return false; - } else if (!mIntentExtraData.equals(other.mIntentExtraData)) - return false; - if (mLogType == null) { - if (other.mLogType != null) - return false; - } else if (!mLogType.equals(other.mLogType)) - return false; - if (mShortcutId == null) { - if (other.mShortcutId != null) - return false; - } else if (!mShortcutId.equals(other.mShortcutId)) - return false; - if (mSource == null) { - if (other.mSource != null) - return false; - } else if (!mSource.equals(other.mSource)) - return false; - if (mSpinnerWhileRefreshing != other.mSpinnerWhileRefreshing) - return false; - if (mSuggestionQuery == null) { - if (other.mSuggestionQuery != null) - return false; - } else if (!mSuggestionQuery.equals(other.mSuggestionQuery)) - return false; - if (mText1 == null) { - if (other.mText1 != null) - return false; - } else if (!mText1.equals(other.mText1)) - return false; - if (mText2 == null) { - if (other.mText2 != null) - return false; - } else if (!mText2.equals(other.mText2)) - return false; - return true; - } - - /** - * Returns a string representation of the contents of this SuggestionData, - * for debugging purposes. - */ - @Override - public String toString() { - StringBuilder builder = new StringBuilder("SuggestionData("); - appendField(builder, "source", mSource.getName()); - appendField(builder, "text1", mText1); - appendField(builder, "intentAction", mIntentAction); - appendField(builder, "intentData", mIntentData); - appendField(builder, "query", mSuggestionQuery); - appendField(builder, "shortcutid", mShortcutId); - appendField(builder, "logtype", mLogType); - return builder.toString(); - } - - private void appendField(StringBuilder builder, String name, String value) { - if (value != null) { - builder.append(",").append(name).append("=").append(value); - } - } - - @VisibleForTesting - public void setExtras(SuggestionExtras extras) { - mExtras = extras; - } - - public SuggestionExtras getExtras() { - return mExtras; - } - -} diff --git a/src/com/android/quicksearchbox/SuggestionData.kt b/src/com/android/quicksearchbox/SuggestionData.kt new file mode 100644 index 0000000..a7bf924 --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionData.kt @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.ComponentName +import android.content.Intent +import com.google.common.annotations.VisibleForTesting + +/** + * Holds data for each suggest item including the display data and how to launch the result. Used + * for passing from the provider to the suggest cursor. + */ +class SuggestionData(override val suggestionSource: Source?) : Suggestion { + private var mFormat: String? = null + private var mText1: String? = null + private var mText2: String? = null + private var mText2Url: String? = null + private var mIcon1: String? = null + private var mIcon2: String? = null + private var mShortcutId: String? = null + override var isSpinnerWhileRefreshing = false + private set + private var mIntentAction: String? = null + private var mIntentData: String? = null + var intentExtraData: String? = null + private set + private var mSuggestionQuery: String? = null + private var mLogType: String? = null + override var isSuggestionShortcut = false + private set + override var isHistorySuggestion = false + private set + private var mExtras: SuggestionExtras? = null + override val suggestionFormat: String + get() = mFormat!! + override val suggestionText1: String + get() = mText1!! + override val suggestionText2: String + get() = mText2!! + override val suggestionText2Url: String + get() = mText2Url!! + override val suggestionIcon1: String + get() = mIcon1!! + override val suggestionIcon2: String + get() = mIcon2!! + override val shortcutId: String + get() = mShortcutId!! + override val suggestionIntentAction: String? + get() = mIntentAction ?: suggestionSource?.defaultIntentAction + override val suggestionIntentComponent: ComponentName? + get() = suggestionSource?.intentComponent + override val suggestionIntentDataString: String + get() = mIntentData!! + override val suggestionIntentExtraData: String + get() = intentExtraData!! + override val suggestionQuery: String + get() = mSuggestionQuery!! + override val suggestionLogType: String + get() = mLogType!! + override val isWebSearchSuggestion: Boolean + get() = Intent.ACTION_WEB_SEARCH.equals(suggestionIntentAction) + + @VisibleForTesting + fun setFormat(format: String?): SuggestionData { + mFormat = format + return this + } + + @VisibleForTesting + fun setText1(text1: String?): SuggestionData { + mText1 = text1 + return this + } + + @VisibleForTesting + fun setText2(text2: String?): SuggestionData { + mText2 = text2 + return this + } + + @VisibleForTesting + fun setText2Url(text2Url: String?): SuggestionData { + mText2Url = text2Url + return this + } + + @VisibleForTesting + fun setIcon1(icon1: String?): SuggestionData { + mIcon1 = icon1 + return this + } + + @VisibleForTesting + fun setIcon2(icon2: String?): SuggestionData { + mIcon2 = icon2 + return this + } + + @VisibleForTesting + fun setIntentAction(intentAction: String?): SuggestionData { + mIntentAction = intentAction + return this + } + + @VisibleForTesting + fun setIntentData(intentData: String?): SuggestionData { + mIntentData = intentData + return this + } + + @VisibleForTesting + fun setIntentExtraData(intentExtraData: String?): SuggestionData { + this.intentExtraData = intentExtraData + return this + } + + @VisibleForTesting + fun setSuggestionQuery(suggestionQuery: String?): SuggestionData { + mSuggestionQuery = suggestionQuery + return this + } + + @VisibleForTesting + fun setShortcutId(shortcutId: String?): SuggestionData { + mShortcutId = shortcutId + return this + } + + @VisibleForTesting + fun setSpinnerWhileRefreshing(spinnerWhileRefreshing: Boolean): SuggestionData { + isSpinnerWhileRefreshing = spinnerWhileRefreshing + return this + } + + @VisibleForTesting + fun setSuggestionLogType(logType: String?): SuggestionData { + mLogType = logType + return this + } + + @VisibleForTesting + fun setIsShortcut(isShortcut: Boolean): SuggestionData { + isSuggestionShortcut = isShortcut + return this + } + + @VisibleForTesting + fun setIsHistory(isHistory: Boolean): SuggestionData { + isHistorySuggestion = isHistory + return this + } + + @Override + override fun hashCode(): Int { + val prime = 31 + var result = 1 + result = prime * result + if (mFormat == null) 0 else mFormat.hashCode() + result = prime * result + if (mIcon1 == null) 0 else mIcon1.hashCode() + result = prime * result + if (mIcon2 == null) 0 else mIcon2.hashCode() + result = prime * result + if (mIntentAction == null) 0 else mIntentAction.hashCode() + result = prime * result + if (mIntentData == null) 0 else mIntentData.hashCode() + result = prime * result + if (intentExtraData == null) 0 else intentExtraData.hashCode() + result = prime * result + if (mLogType == null) 0 else mLogType.hashCode() + result = prime * result + if (mShortcutId == null) 0 else mShortcutId.hashCode() + result = prime * result + if (suggestionSource == null) 0 else suggestionSource.hashCode() + result = prime * result + if (isSpinnerWhileRefreshing) 1231 else 1237 + result = prime * result + if (mSuggestionQuery == null) 0 else mSuggestionQuery.hashCode() + result = prime * result + if (mText1 == null) 0 else mText1.hashCode() + result = prime * result + if (mText2 == null) 0 else mText2.hashCode() + return result + } + + @Override + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (this::class !== other::class) return false + val suggestionData = other as SuggestionData + if (mFormat == null) { + if (suggestionData.mFormat != null) return false + } else if (!mFormat.equals(suggestionData.mFormat)) return false + if (mIcon1 == null) { + if (suggestionData.mIcon1 != null) return false + } else if (!mIcon1.equals(suggestionData.mIcon1)) return false + if (mIcon2 == null) { + if (suggestionData.mIcon2 != null) return false + } else if (!mIcon2.equals(suggestionData.mIcon2)) return false + if (mIntentAction == null) { + if (suggestionData.mIntentAction != null) return false + } else if (!mIntentAction.equals(suggestionData.mIntentAction)) return false + if (mIntentData == null) { + if (suggestionData.mIntentData != null) return false + } else if (!mIntentData.equals(suggestionData.mIntentData)) return false + if (intentExtraData == null) { + if (suggestionData.intentExtraData != null) return false + } else if (!intentExtraData.equals(suggestionData.intentExtraData)) return false + if (mLogType == null) { + if (suggestionData.mLogType != null) return false + } else if (!mLogType.equals(suggestionData.mLogType)) return false + if (mShortcutId == null) { + if (suggestionData.mShortcutId != null) return false + } else if (!mShortcutId.equals(suggestionData.mShortcutId)) return false + if (suggestionSource == null) { + if (suggestionData.suggestionSource != null) return false + } else if (!suggestionSource.equals(suggestionData.suggestionSource)) return false + if (isSpinnerWhileRefreshing != suggestionData.isSpinnerWhileRefreshing) return false + if (mSuggestionQuery == null) { + if (suggestionData.mSuggestionQuery != null) return false + } else if (!mSuggestionQuery.equals(suggestionData.mSuggestionQuery)) return false + if (mText1 == null) { + if (suggestionData.mText1 != null) return false + } else if (!mText1.equals(suggestionData.mText1)) return false + if (mText2 == null) { + if (suggestionData.mText2 != null) return false + } else if (!mText2.equals(suggestionData.mText2)) return false + return true + } + + /** + * Returns a string representation of the contents of this SuggestionData, for debugging purposes. + */ + @Override + override fun toString(): String { + val builder: StringBuilder = StringBuilder("SuggestionData(") + appendField(builder, "source", suggestionSource!!.name) + appendField(builder, "text1", mText1) + appendField(builder, "intentAction", mIntentAction) + appendField(builder, "intentData", mIntentData) + appendField(builder, "query", mSuggestionQuery) + appendField(builder, "shortcutid", mShortcutId) + appendField(builder, "logtype", mLogType) + return builder.toString() + } + + private fun appendField(builder: StringBuilder, name: String, value: String?) { + if (value != null) { + builder.append(",").append(name).append("=").append(value) + } + } + + @set:VisibleForTesting + override var extras: SuggestionExtras? + get() = mExtras + set(extras) { + mExtras = extras + } +} diff --git a/src/com/android/quicksearchbox/SuggestionExtras.java b/src/com/android/quicksearchbox/SuggestionExtras.java deleted file mode 100644 index 263c808..0000000 --- a/src/com/android/quicksearchbox/SuggestionExtras.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import org.json.JSONException; - -import java.util.Collection; - -/** - * Extra data that can be attached to a suggestion. - */ -public interface SuggestionExtras { - - /** - * Return the names of custom columns present in these extras. - */ - Collection<String> getExtraColumnNames(); - - /** - * @param columnName The column to get a value from. - */ - String getExtra(String columnName); - - /** - * Flatten these extras as a JSON object. - */ - String toJsonString() throws JSONException; - -} diff --git a/src/com/android/quicksearchbox/SuggestionNonFormatter.java b/src/com/android/quicksearchbox/SuggestionExtras.kt index d7dc0bd..583b7b5 100644 --- a/src/com/android/quicksearchbox/SuggestionNonFormatter.java +++ b/src/com/android/quicksearchbox/SuggestionExtras.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2022 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. @@ -13,22 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.quicksearchbox -package com.android.quicksearchbox; +import org.json.JSONException +/** Extra data that can be attached to a suggestion. */ +interface SuggestionExtras { + /** Return the names of custom columns present in these extras. */ + val extraColumnNames: Collection<String> -/** - * Basic SuggestionFormatter that does no formatting. - */ -public class SuggestionNonFormatter extends SuggestionFormatter { - - public SuggestionNonFormatter(TextAppearanceFactory spanFactory) { - super(spanFactory); - } - - @Override - public CharSequence formatSuggestion(String query, String suggestion) { - return suggestion; - } + /** @param columnName The column to get a value from. */ + fun getExtra(columnName: String?): String? + /** Flatten these extras as a JSON object. */ + @Throws(JSONException::class) fun toJsonString(): String? } diff --git a/src/com/android/quicksearchbox/SuggestionFilter.java b/src/com/android/quicksearchbox/SuggestionFilter.java deleted file mode 100644 index aaf0c70..0000000 --- a/src/com/android/quicksearchbox/SuggestionFilter.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -/** - * Interface for choosing which suggestions to include in a promoted list. - */ -public interface SuggestionFilter { - /** - * Determines if a suggestion should be added to the promoted suggestion list. - * - * @param s The suggestion in question - * @return true to include it in the results - */ - boolean accept(Suggestion s); -} diff --git a/src/com/android/quicksearchbox/SuggestionFilter.kt b/src/com/android/quicksearchbox/SuggestionFilter.kt new file mode 100644 index 0000000..5cfb5bd --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionFilter.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +/** Interface for choosing which suggestions to include in a promoted list. */ +interface SuggestionFilter { + /** + * Determines if a suggestion should be added to the promoted suggestion list. + * + * @param s The suggestion in question + * @return true to include it in the results + */ + fun accept(s: Suggestion?): Boolean +} diff --git a/src/com/android/quicksearchbox/SuggestionFormatter.java b/src/com/android/quicksearchbox/SuggestionFormatter.java deleted file mode 100644 index a6eab9a..0000000 --- a/src/com/android/quicksearchbox/SuggestionFormatter.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.text.Spannable; - -/** - * Suggestion formatter interface. This is used to bold (or otherwise highlight) portions of a - * suggestion which were not a part of the query. - */ -public abstract class SuggestionFormatter { - - private final TextAppearanceFactory mSpanFactory; - - protected SuggestionFormatter(TextAppearanceFactory spanFactory) { - mSpanFactory = spanFactory; - } - - /** - * Formats a suggestion for display in the UI. - * - * @param query the query as entered by the user - * @param suggestion the suggestion - * @return Formatted suggestion text. - */ - public abstract CharSequence formatSuggestion(String query, String suggestion); - - protected void applyQueryTextStyle(Spannable text, int start, int end) { - if (start == end) return; - setSpans(text, start, end, mSpanFactory.createSuggestionQueryTextAppearance()); - } - - protected void applySuggestedTextStyle(Spannable text, int start, int end) { - if (start == end) return; - setSpans(text, start, end, mSpanFactory.createSuggestionSuggestedTextAppearance()); - } - - private void setSpans(Spannable text, int start, int end, Object[] spans) { - for (Object span : spans) { - text.setSpan(span, start, end, 0); - } - } - -} diff --git a/src/com/android/quicksearchbox/SuggestionFormatter.kt b/src/com/android/quicksearchbox/SuggestionFormatter.kt new file mode 100644 index 0000000..fea3ee9 --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionFormatter.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.text.Spannable + +/** + * Suggestion formatter interface. This is used to bold (or otherwise highlight) portions of a + * suggestion which were not a part of the query. + */ +abstract class SuggestionFormatter +protected constructor(private val mSpanFactory: TextAppearanceFactory) { + /** + * Formats a suggestion for display in the UI. + * + * @param query the query as entered by the user + * @param suggestion the suggestion + * @return Formatted suggestion text. + */ + abstract fun formatSuggestion(query: String?, suggestion: String?): CharSequence? + protected fun applyQueryTextStyle(text: Spannable, start: Int, end: Int) { + if (start == end) return + setSpans(text, start, end, mSpanFactory.createSuggestionQueryTextAppearance()) + } + + protected fun applySuggestedTextStyle(text: Spannable, start: Int, end: Int) { + if (start == end) return + setSpans(text, start, end, mSpanFactory.createSuggestionSuggestedTextAppearance()) + } + + private fun setSpans(text: Spannable, start: Int, end: Int, spans: Array<Any>) { + for (span in spans) { + text.setSpan(span, start, end, 0) + } + } +} diff --git a/src/com/android/quicksearchbox/ResultFilter.java b/src/com/android/quicksearchbox/SuggestionNonFormatter.kt index 76d88b6..d473d74 100644 --- a/src/com/android/quicksearchbox/ResultFilter.java +++ b/src/com/android/quicksearchbox/SuggestionNonFormatter.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2022 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. @@ -13,18 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.quicksearchbox; - -/** - * {@link SuggestionFilter} that accepts only results (not web suggestions). - */ -public class ResultFilter implements SuggestionFilter { - - public ResultFilter() { - } - - public boolean accept(Suggestion s) { - return !s.isWebSearchSuggestion(); - } +package com.android.quicksearchbox +/** Basic SuggestionFormatter that does no formatting. */ +class SuggestionNonFormatter(spanFactory: TextAppearanceFactory?) : + SuggestionFormatter(spanFactory!!) { + @Override + override fun formatSuggestion(query: String?, suggestion: String?): CharSequence? { + return suggestion + } } diff --git a/src/com/android/quicksearchbox/SuggestionPosition.java b/src/com/android/quicksearchbox/SuggestionPosition.java deleted file mode 100644 index 8311978..0000000 --- a/src/com/android/quicksearchbox/SuggestionPosition.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - - -/** - * A pointer to a suggestion in a {@link SuggestionCursor}. - * - */ -public class SuggestionPosition extends AbstractSuggestionWrapper { - - private final SuggestionCursor mCursor; - - private final int mPosition; - - public SuggestionPosition(SuggestionCursor cursor) { - this(cursor, cursor.getPosition()); - } - - public SuggestionPosition(SuggestionCursor cursor, int suggestionPos) { - mCursor = cursor; - mPosition = suggestionPos; - } - - public SuggestionCursor getCursor() { - return mCursor; - } - - /** - * Gets the suggestion cursor, moved to point to the right suggestion. - */ - @Override - protected Suggestion current() { - mCursor.moveTo(mPosition); - return mCursor; - } - - public int getPosition() { - return mPosition; - } - - @Override - public String toString() { - return mCursor + ":" + mPosition; - } - -} diff --git a/src/com/android/quicksearchbox/SuggestionPosition.kt b/src/com/android/quicksearchbox/SuggestionPosition.kt new file mode 100644 index 0000000..36be904 --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionPosition.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +/** A pointer to a suggestion in a [SuggestionCursor]. */ +class SuggestionPosition +@JvmOverloads +constructor(val cursor: SuggestionCursor?, val position: Int = cursor!!.position) : + AbstractSuggestionWrapper() { + + /** Gets the suggestion cursor, moved to point to the right suggestion. */ + @Override + override fun current(): Suggestion? { + cursor?.moveTo(position) + return cursor + } + + @Override + override fun toString(): String { + return cursor.toString() + ":" + position + } +} diff --git a/src/com/android/quicksearchbox/SuggestionUtils.java b/src/com/android/quicksearchbox/SuggestionUtils.java deleted file mode 100644 index bf15053..0000000 --- a/src/com/android/quicksearchbox/SuggestionUtils.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import com.google.common.annotations.VisibleForTesting; - -import android.app.SearchManager; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; - -/** - * Some utilities for suggestions. - */ -public class SuggestionUtils { - - private SuggestionUtils() { - } - - public static Intent getSuggestionIntent(SuggestionCursor suggestion, Bundle appSearchData) { - String action = suggestion.getSuggestionIntentAction(); - - String data = suggestion.getSuggestionIntentDataString(); - String query = suggestion.getSuggestionQuery(); - String userQuery = suggestion.getUserQuery(); - String extraData = suggestion.getSuggestionIntentExtraData(); - - // Now build the Intent - Intent intent = new Intent(action); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // We need CLEAR_TOP to avoid reusing an old task that has other activities - // on top of the one we want. - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - if (data != null) { - intent.setData(Uri.parse(data)); - } - intent.putExtra(SearchManager.USER_QUERY, userQuery); - if (query != null) { - intent.putExtra(SearchManager.QUERY, query); - } - if (extraData != null) { - intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); - } - if (appSearchData != null) { - intent.putExtra(SearchManager.APP_DATA, appSearchData); - } - - intent.setComponent(suggestion.getSuggestionIntentComponent()); - return intent; - } - - /** - * Gets a unique key that identifies a suggestion. This is used to avoid - * duplicate suggestions. - */ - public static String getSuggestionKey(Suggestion suggestion) { - String action = makeKeyComponent(suggestion.getSuggestionIntentAction()); - String data = makeKeyComponent(normalizeUrl(suggestion.getSuggestionIntentDataString())); - String query = makeKeyComponent(normalizeUrl(suggestion.getSuggestionQuery())); - // calculating accurate size of string builder avoids an allocation vs starting with - // the default size and having to expand. - int size = action.length() + 2 + data.length() + query.length(); - return new StringBuilder(size) - .append(action) - .append('#') - .append(data) - .append('#') - .append(query) - .toString(); - } - - private static String makeKeyComponent(String str) { - return str == null ? "" : str; - } - - private static final String SCHEME_SEPARATOR = "://"; - private static final String DEFAULT_SCHEME = "http"; - - /** - * Simple url normalization that adds http:// if no scheme exists, and - * strips empty paths, e.g., - * www.google.com/ -> http://www.google.com. Used to prevent obvious - * duplication of nav suggestions, bookmarks and urls entered by the user. - */ - @VisibleForTesting - static String normalizeUrl(String url) { - String normalized; - if (url != null) { - int start; - int schemePos = url.indexOf(SCHEME_SEPARATOR); - if (schemePos == -1) { - // no scheme - add the default - normalized = DEFAULT_SCHEME + SCHEME_SEPARATOR + url; - start = DEFAULT_SCHEME.length() + SCHEME_SEPARATOR.length(); - } else { - normalized = url; - start = schemePos + SCHEME_SEPARATOR.length(); - } - int end = normalized.length(); - if (normalized.indexOf('/', start) == end - 1) { - end--; - } - return normalized.substring(0, end); - } - return url; - } - -} diff --git a/src/com/android/quicksearchbox/SuggestionUtils.kt b/src/com/android/quicksearchbox/SuggestionUtils.kt new file mode 100644 index 0000000..cde4cd6 --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionUtils.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.SearchManager +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import com.google.common.annotations.VisibleForTesting +import kotlin.text.StringBuilder + +/** Some utilities for suggestions. */ +object SuggestionUtils { + @JvmStatic + fun getSuggestionIntent(suggestion: SuggestionCursor?, appSearchData: Bundle?): Intent { + val action: String? = suggestion?.suggestionIntentAction + val data: String? = suggestion?.suggestionIntentDataString + val query: String? = suggestion?.suggestionQuery + val userQuery: String? = suggestion?.userQuery + val extraData: String? = suggestion?.suggestionIntentExtraData + + // Now build the Intent + val intent = Intent(action) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // We need CLEAR_TOP to avoid reusing an old task that has other activities + // on top of the one we want. + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + if (data != null) { + intent.setData(Uri.parse(data)) + } + intent.putExtra(SearchManager.USER_QUERY, userQuery) + if (query != null) { + intent.putExtra(SearchManager.QUERY, query) + } + if (extraData != null) { + intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData) + } + if (appSearchData != null) { + intent.putExtra(SearchManager.APP_DATA, appSearchData) + } + intent.setComponent(suggestion?.suggestionIntentComponent) + return intent + } + + /** + * Gets a unique key that identifies a suggestion. This is used to avoid duplicate suggestions. + */ + @JvmStatic + fun getSuggestionKey(suggestion: Suggestion): String { + val action: String = makeKeyComponent(suggestion.suggestionIntentAction) + val data: String = makeKeyComponent(normalizeUrl(suggestion.suggestionIntentDataString)) + val query: String = makeKeyComponent(normalizeUrl(suggestion.suggestionQuery)) + // calculating accurate size of string builder avoids an allocation vs starting with + // the default size and having to expand. + val size: Int = action.length + 2 + data.length + query.length + return StringBuilder(size) + .append(action) + .append('#') + .append(data) + .append('#') + .append(query) + .toString() + } + + private fun makeKeyComponent(str: String?): String { + return str ?: "" + } + + private const val SCHEME_SEPARATOR = "://" + private const val DEFAULT_SCHEME = "http" + + /** + * Simple url normalization that adds http:// if no scheme exists, and strips empty paths, e.g., + * www.google.com/ -> http://www.google.com. Used to prevent obvious duplication of nav + * suggestions, bookmarks and urls entered by the user. + */ + @JvmStatic + @VisibleForTesting + fun normalizeUrl(url: String?): String? { + val normalized: String + if (url != null) { + val start: Int + val schemePos: Int = url.indexOf(SCHEME_SEPARATOR) + if (schemePos == -1) { + // no scheme - add the default + normalized = DEFAULT_SCHEME + SCHEME_SEPARATOR + url + start = DEFAULT_SCHEME.length + SCHEME_SEPARATOR.length + } else { + normalized = url + start = schemePos + SCHEME_SEPARATOR.length + } + var end: Int = normalized.length + if (normalized.indexOf('/', start) == end - 1) { + end-- + } + return normalized.substring(0, end) + } + return url + } +} diff --git a/src/com/android/quicksearchbox/Suggestions.java b/src/com/android/quicksearchbox/Suggestions.java deleted file mode 100644 index aca2a67..0000000 --- a/src/com/android/quicksearchbox/Suggestions.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.database.DataSetObservable; -import android.database.DataSetObserver; -import android.util.Log; - -/** - * Collects all corpus results for a single query. - */ -public class Suggestions { - private static final boolean DBG = false; - private static final String TAG = "QSB.Suggestions"; - - /** True if {@link Suggestions#close} has been called. */ - private boolean mClosed = false; - protected final String mQuery; - - /** - * The observers that want notifications of changes to the published suggestions. - * This object may be accessed on any thread. - */ - private final DataSetObservable mDataSetObservable = new DataSetObservable(); - - private Source mSource; - - private SourceResult mResult; - - private int mRefCount = 0; - - private boolean mDone = false; - - public Suggestions(String query, Source source) { - mQuery = query; - mSource = source; - } - - public void acquire() { - mRefCount++; - } - - public void release() { - mRefCount--; - if (mRefCount <= 0) { - close(); - } - } - - public Source getSource() { - return mSource; - } - - /** - * Marks the suggestions set as complete, regardless of whether all corpora have - * returned. - */ - public void done() { - mDone = true; - } - - /** - * Checks whether all sources have reported. - * Must be called on the UI thread, or before this object is seen by the UI thread. - */ - public boolean isDone() { - return mDone || mResult != null; - } - - /** - * Adds a list of corpus results. Must be called on the UI thread, or before this - * object is seen by the UI thread. - */ - public void addResults(SourceResult result) { - if (isClosed()) { - result.close(); - return; - } - - if (DBG) { - Log.d(TAG, "addResults["+ hashCode() + "] source:" + - result.getSource().getName() + " results:" + result.getCount()); - } - if (!mQuery.equals(result.getUserQuery())) { - throw new IllegalArgumentException("Got result for wrong query: " - + mQuery + " != " + result.getUserQuery()); - } - mResult = result; - notifyDataSetChanged(); - } - - /** - * Registers an observer that will be notified when the reported results or - * the done status changes. - */ - public void registerDataSetObserver(DataSetObserver observer) { - if (mClosed) { - throw new IllegalStateException("registerDataSetObserver() when closed"); - } - mDataSetObservable.registerObserver(observer); - } - - - /** - * Unregisters an observer. - */ - public void unregisterDataSetObserver(DataSetObserver observer) { - mDataSetObservable.unregisterObserver(observer); - } - - /** - * Calls {@link DataSetObserver#onChanged()} on all observers. - */ - protected void notifyDataSetChanged() { - if (DBG) Log.d(TAG, "notifyDataSetChanged()"); - mDataSetObservable.notifyChanged(); - } - - /** - * Closes all the source results and unregisters all observers. - */ - private void close() { - if (DBG) Log.d(TAG, "close() [" + hashCode() + "]"); - if (mClosed) { - throw new IllegalStateException("Double close()"); - } - mClosed = true; - mDataSetObservable.unregisterAll(); - if (mResult != null) { - mResult.close(); - } - mResult = null; - } - - public boolean isClosed() { - return mClosed; - } - - @Override - protected void finalize() { - if (!mClosed) { - Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[" + getQuery() + "]"); - } - } - - public String getQuery() { - return mQuery; - } - - /** - * Gets the list of corpus results reported so far. Do not modify or hang on to - * the returned iterator. - */ - public SourceResult getResult() { - return mResult; - } - - public SourceResult getWebResult() { - return mResult; - } - - /** - * Gets the number of source results. - * Must be called on the UI thread, or before this object is seen by the UI thread. - */ - public int getResultCount() { - if (isClosed()) { - throw new IllegalStateException("Called getSourceCount() when closed."); - } - return mResult == null ? 0 : mResult.getCount(); - } - - @Override - public String toString() { - return "Suggestions@" + hashCode() + "{source=" + mSource - + ",getResultCount()=" + getResultCount() + "}"; - } - -} diff --git a/src/com/android/quicksearchbox/Suggestions.kt b/src/com/android/quicksearchbox/Suggestions.kt new file mode 100644 index 0000000..2b4b620 --- /dev/null +++ b/src/com/android/quicksearchbox/Suggestions.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.database.DataSetObservable +import android.database.DataSetObserver +import android.util.Log + +/** Collects all corpus results for a single query. */ +class Suggestions(val query: String, val source: Source) { + + /** + * The observers that want notifications of changes to the published suggestions. This object may + * be accessed on any thread. + */ + private val mDataSetObservable: DataSetObservable = DataSetObservable() + + private var mResult: SourceResult? = null + + private var mRefCount = 0 + + private var mDone = false + + /** True if [Suggestions.close] has been called. */ + var isClosed = false + private set + + /** + * Gets the list of corpus results reported so far. Do not modify or hang on to the returned + * iterator. + */ + fun getResult(): SourceResult? { + return mResult + } + + fun getWebResult(): SourceResult? { + return mResult + } + + fun acquire() { + mRefCount++ + } + + fun release() { + mRefCount-- + if (mRefCount <= 0) { + close() + } + } + + /** Marks the suggestions set as complete, regardless of whether all corpora have returned. */ + fun done() { + mDone = true + } + + /** + * Checks whether all sources have reported. Must be called on the UI thread, or before this + * object is seen by the UI thread. + */ + val isDone: Boolean + get() = mDone || mResult != null + + /** + * Adds a list of corpus results. Must be called on the UI thread, or before this object is seen + * by the UI thread. + */ + fun addResults(result: SourceResult?) { + if (isClosed) { + result?.close() + return + } + if (DBG) { + Log.d( + TAG, + "addResults[" + + hashCode().toString() + + "] source:" + + result?.source?.name.toString() + + " results:" + + result?.count + ) + } + if (query != result?.userQuery) { + throw IllegalArgumentException( + "Got result for wrong query: " + query + " != " + result?.userQuery + ) + } + mResult = result + notifyDataSetChanged() + } + + /** + * Registers an observer that will be notified when the reported results or the done status + * changes. + */ + fun registerDataSetObserver(observer: DataSetObserver?) { + if (isClosed) { + throw IllegalStateException("registerDataSetObserver() when closed") + } + mDataSetObservable.registerObserver(observer) + } + + /** Unregisters an observer. */ + fun unregisterDataSetObserver(observer: DataSetObserver?) { + mDataSetObservable.unregisterObserver(observer) + } + + /** Calls [DataSetObserver.onChanged] on all observers. */ + protected fun notifyDataSetChanged() { + if (DBG) Log.d(TAG, "notifyDataSetChanged()") + mDataSetObservable.notifyChanged() + } + + /** Closes all the source results and unregisters all observers. */ + private fun close() { + if (DBG) Log.d(TAG, "close() [" + hashCode().toString() + "]") + if (isClosed) { + throw IllegalStateException("Double close()") + } + isClosed = true + mDataSetObservable.unregisterAll() + mResult?.close() + mResult = null + } + + @Override + protected fun finalize() { + if (!isClosed) { + Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[$query]") + } + } + + /** + * Gets the number of source results. Must be called on the UI thread, or before this object is + * seen by the UI thread. + */ + val resultCount: Int + get() { + if (isClosed) { + throw IllegalStateException("Called resultCount when closed.") + } + return mResult?.count ?: 0 + } + + @Override + override fun toString(): String { + return "Suggestions@" + + hashCode().toString() + + "{source=" + + source.toString() + + ",resultCount=" + + resultCount.toString() + + "}" + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.Suggestions" + } +} diff --git a/src/com/android/quicksearchbox/SuggestionsProvider.java b/src/com/android/quicksearchbox/SuggestionsProvider.java deleted file mode 100644 index 9196018..0000000 --- a/src/com/android/quicksearchbox/SuggestionsProvider.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -/** - * Provides a set of suggestion results for a query.. - * - */ -public interface SuggestionsProvider { - - /** - * Gets suggestions for a query. - * - * @param query The query. - * @param source The source to query. Must be non-null. - */ - Suggestions getSuggestions(String query, Source source); - - void close(); -} diff --git a/src/com/android/quicksearchbox/SuggestionsProvider.kt b/src/com/android/quicksearchbox/SuggestionsProvider.kt new file mode 100644 index 0000000..aaa7e21 --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionsProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +/** Provides a set of suggestion results for a query.. */ +interface SuggestionsProvider { + /** + * Gets suggestions for a query. + * + * @param query The query. + * @param source The source to query. Must be non-null. + */ + fun getSuggestions(query: String, source: Source): Suggestions + fun close() +} diff --git a/src/com/android/quicksearchbox/SuggestionsProviderImpl.java b/src/com/android/quicksearchbox/SuggestionsProviderImpl.java deleted file mode 100644 index 76a9071..0000000 --- a/src/com/android/quicksearchbox/SuggestionsProviderImpl.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox; - -import android.os.Handler; -import android.util.Log; - -import com.android.quicksearchbox.util.BatchingNamedTaskExecutor; -import com.android.quicksearchbox.util.Consumer; -import com.android.quicksearchbox.util.NamedTaskExecutor; -import com.android.quicksearchbox.util.NoOpConsumer; - -/** - * Suggestions provider implementation. - * - * The provider will only handle a single query at a time. If a new query comes - * in, the old one is cancelled. - */ -public class SuggestionsProviderImpl implements SuggestionsProvider { - - private static final boolean DBG = false; - private static final String TAG = "QSB.SuggestionsProviderImpl"; - - private final Config mConfig; - - private final NamedTaskExecutor mQueryExecutor; - - private final Handler mPublishThread; - - private final Logger mLogger; - - public SuggestionsProviderImpl(Config config, - NamedTaskExecutor queryExecutor, - Handler publishThread, - Logger logger) { - mConfig = config; - mQueryExecutor = queryExecutor; - mPublishThread = publishThread; - mLogger = logger; - } - - @Override - public void close() { - } - - @Override - public Suggestions getSuggestions(String query, Source sourceToQuery) { - if (DBG) Log.d(TAG, "getSuggestions(" + query + ")"); - final Suggestions suggestions = new Suggestions(query, sourceToQuery); - Log.i(TAG, "chars:" + query.length() + ",source:" + sourceToQuery); - - Consumer<SourceResult> receiver; - if (shouldDisplayResults(query)) { - receiver = new SuggestionCursorReceiver(suggestions); - } else { - receiver = new NoOpConsumer<SourceResult>(); - suggestions.done(); - } - - int maxResults = mConfig.getMaxResultsPerSource(); - QueryTask.startQuery(query, maxResults, sourceToQuery, mQueryExecutor, - mPublishThread, receiver); - - return suggestions; - } - - private boolean shouldDisplayResults(String query) { - if (query.length() == 0 && !mConfig.showSuggestionsForZeroQuery()) { - // Note that even though we don't display such results, it's - // useful to run the query itself because it warms up the network - // connection. - return false; - } - return true; - } - - - private class SuggestionCursorReceiver implements Consumer<SourceResult> { - private final Suggestions mSuggestions; - - public SuggestionCursorReceiver(Suggestions suggestions) { - mSuggestions = suggestions; - } - - @Override - public boolean consume(SourceResult cursor) { - if (DBG) { - Log.d(TAG, "SuggestionCursorReceiver.consume(" + cursor + ") corpus=" + - cursor.getSource() + " count = " + cursor.getCount()); - } - // publish immediately - if (DBG) Log.d(TAG, "Publishing results"); - mSuggestions.addResults(cursor); - if (cursor != null && mLogger != null) { - mLogger.logLatency(cursor); - } - return true; - } - - } -} diff --git a/src/com/android/quicksearchbox/SuggestionsProviderImpl.kt b/src/com/android/quicksearchbox/SuggestionsProviderImpl.kt new file mode 100644 index 0000000..9c02c07 --- /dev/null +++ b/src/com/android/quicksearchbox/SuggestionsProviderImpl.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.os.Handler +import android.util.Log +import com.android.quicksearchbox.util.Consumer +import com.android.quicksearchbox.util.NamedTaskExecutor +import com.android.quicksearchbox.util.NoOpConsumer + +/** + * Suggestions provider implementation. + * + * The provider will only handle a single query at a time. If a new query comes in, the old one is + * cancelled. + */ +class SuggestionsProviderImpl( + private val mConfig: Config, + private val mQueryExecutor: NamedTaskExecutor, + publishThread: Handler?, + logger: Logger? +) : SuggestionsProvider { + + private val mPublishThread: Handler? + + private val mLogger: Logger? + + @Override override fun close() {} + + @Override + override fun getSuggestions(query: String, source: Source): Suggestions { + if (DBG) Log.d(TAG, "getSuggestions($query)") + val suggestions = Suggestions(query, source) + Log.i(TAG, "chars:" + query.length.toString() + ",source:" + source) + val receiver: Consumer<SourceResult?> + if (shouldDisplayResults(query)) { + receiver = SuggestionCursorReceiver(suggestions) + } else { + receiver = NoOpConsumer() + suggestions.done() + } + val maxResults: Int = mConfig.maxResultsPerSource + QueryTask.startQuery(query, maxResults, source, mQueryExecutor, mPublishThread, receiver) + return suggestions + } + + private fun shouldDisplayResults(query: String): Boolean { + return !(query.isEmpty() && !mConfig.showSuggestionsForZeroQuery()) + } + + private inner class SuggestionCursorReceiver(private val mSuggestions: Suggestions) : + Consumer<SourceResult?> { + @Override + override fun consume(value: SourceResult?): Boolean { + if (DBG) { + Log.d( + TAG, + "SuggestionCursorReceiver.consume(" + + value + + ") corpus=" + + value?.source + + " count = " + + value?.count + ) + } + // publish immediately + if (DBG) Log.d(TAG, "Publishing results") + mSuggestions.addResults(value) + if (value != null && mLogger != null) { + mLogger.logLatency(value) + } + return true + } + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.SuggestionsProviderImpl" + } + + init { + mPublishThread = publishThread + mLogger = logger + } +} diff --git a/src/com/android/quicksearchbox/TextAppearanceFactory.java b/src/com/android/quicksearchbox/TextAppearanceFactory.java deleted file mode 100644 index af950d9..0000000 --- a/src/com/android/quicksearchbox/TextAppearanceFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.content.Context; -import android.text.style.TextAppearanceSpan; - -/** - * Factory class for text appearances. - */ -public class TextAppearanceFactory { - private final Context mContext; - - public TextAppearanceFactory(Context context) { - mContext = context; - } - - public Object[] createSuggestionQueryTextAppearance() { - return new Object[]{ - new TextAppearanceSpan(mContext, R.style.SuggestionText1_Query) - }; - } - - public Object[] createSuggestionSuggestedTextAppearance() { - return new Object[]{ - new TextAppearanceSpan(mContext, R.style.SuggestionText1_Suggested) - }; - } - -} diff --git a/src/com/android/quicksearchbox/TextAppearanceFactory.kt b/src/com/android/quicksearchbox/TextAppearanceFactory.kt new file mode 100644 index 0000000..0b1e0cc --- /dev/null +++ b/src/com/android/quicksearchbox/TextAppearanceFactory.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.content.Context +import android.text.style.TextAppearanceSpan + +/** Factory class for text appearances. */ +open class TextAppearanceFactory(context: Context?) { + private val mContext: Context? + open fun createSuggestionQueryTextAppearance(): Array<Any> { + return arrayOf(TextAppearanceSpan(mContext, R.style.SuggestionText1_Query)) + } + + open fun createSuggestionSuggestedTextAppearance(): Array<Any> { + return arrayOf(TextAppearanceSpan(mContext, R.style.SuggestionText1_Suggested)) + } + + init { + mContext = context + } +} diff --git a/src/com/android/quicksearchbox/VoiceSearch.java b/src/com/android/quicksearchbox/VoiceSearch.java deleted file mode 100644 index 674db96..0000000 --- a/src/com/android/quicksearchbox/VoiceSearch.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox; - -import android.app.SearchManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ComponentInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; -import android.os.Bundle; -import android.speech.RecognizerIntent; -import android.util.Log; - -/** - * Voice Search integration. - */ -public class VoiceSearch { - - private static final String TAG = "QSB.VoiceSearch"; - - private final Context mContext; - - public VoiceSearch(Context context) { - mContext = context; - } - - protected Context getContext() { - return mContext; - } - - public boolean shouldShowVoiceSearch() { - return isVoiceSearchAvailable(); - } - - protected Intent createVoiceSearchIntent() { - return new Intent(RecognizerIntent.ACTION_WEB_SEARCH); - } - - private ResolveInfo getResolveInfo() { - Intent intent = createVoiceSearchIntent(); - ResolveInfo ri = mContext.getPackageManager(). - resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); - return ri; - } - - public boolean isVoiceSearchAvailable() { - return getResolveInfo() != null; - } - - public Intent createVoiceWebSearchIntent(Bundle appData) { - if (!isVoiceSearchAvailable()) return null; - Intent intent = createVoiceSearchIntent(); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, - RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); - if (appData != null) { - intent.putExtra(SearchManager.APP_DATA, appData); - } - return intent; - } - - /** - * Create an intent to launch the voice search help screen, if any exists. - * @return The intent, or null. - */ - public Intent createVoiceSearchHelpIntent() { - return null; - } - - /** - * Gets the {@code versionCode} of the currently installed voice search package. - * - * @return The {@code versionCode} of voiceSearch, or 0 if none is installed. - */ - public int getVersion() { - ResolveInfo ri = getResolveInfo(); - if (ri == null) return 0; - ComponentInfo ci = ri.activityInfo != null ? ri.activityInfo : ri.serviceInfo; - try { - return getContext().getPackageManager().getPackageInfo(ci.packageName, 0).versionCode; - } catch (NameNotFoundException e) { - Log.e(TAG, "Cannot find voice search package " + ci.packageName, e); - return 0; - } - } - - public ComponentName getComponent() { - return createVoiceSearchIntent().resolveActivity(getContext().getPackageManager()); - } -} diff --git a/src/com/android/quicksearchbox/VoiceSearch.kt b/src/com/android/quicksearchbox/VoiceSearch.kt new file mode 100644 index 0000000..608e5d8 --- /dev/null +++ b/src/com/android/quicksearchbox/VoiceSearch.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022 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.quicksearchbox + +import android.app.SearchManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ComponentInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException +import android.content.pm.ResolveInfo +import android.os.Bundle +import android.speech.RecognizerIntent +import android.util.Log + +/** Voice Search integration. */ +class VoiceSearch(context: Context?) { + + private val mContext: Context? + + protected val context: Context? + get() = mContext + + fun shouldShowVoiceSearch(): Boolean { + return isVoiceSearchAvailable + } + + protected fun createVoiceSearchIntent(): Intent { + return Intent(RecognizerIntent.ACTION_WEB_SEARCH) + } + + private val resolveInfo: ResolveInfo? + @Suppress("DEPRECATION") + get() { + val intent: Intent = createVoiceSearchIntent() + return mContext + ?.getPackageManager() + ?.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) + } + val isVoiceSearchAvailable: Boolean + get() = resolveInfo != null + + fun createVoiceWebSearchIntent(appData: Bundle?): Intent? { + if (!isVoiceSearchAvailable) return null + val intent: Intent = createVoiceSearchIntent() + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra( + RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH + ) + if (appData != null) { + intent.putExtra(SearchManager.APP_DATA, appData) + } + return intent + } + + /** + * Create an intent to launch the voice search help screen, if any exists. + * @return The intent, or null. + */ + fun createVoiceSearchHelpIntent(): Intent? { + return null + } + + /** + * Gets the `versionCode` of the currently installed voice search package. + * + * @return The `versionCode` of voiceSearch, or 0 if none is installed. + */ + val version: Long + @Suppress("DEPRECATION") + get() { + val ri: ResolveInfo = resolveInfo ?: return 0 + val ci: ComponentInfo = if (ri.activityInfo != null) ri.activityInfo else ri.serviceInfo + return try { + context!!.getPackageManager().getPackageInfo(ci.packageName, 0).getLongVersionCode() + } catch (e: NameNotFoundException) { + Log.e(TAG, "Cannot find voice search package " + ci.packageName, e) + 0 + } + } + val component: ComponentName + get() = createVoiceSearchIntent().resolveActivity(context!!.getPackageManager()) + + companion object { + private const val TAG = "QSB.VoiceSearch" + } + + init { + mContext = context + } +} diff --git a/src/com/android/quicksearchbox/google/AbstractGoogleSource.java b/src/com/android/quicksearchbox/google/AbstractGoogleSource.java deleted file mode 100644 index 2077777..0000000 --- a/src/com/android/quicksearchbox/google/AbstractGoogleSource.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.google; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; - -import com.android.quicksearchbox.AbstractInternalSource; -import com.android.quicksearchbox.CursorBackedSourceResult; -import com.android.quicksearchbox.R; -import com.android.quicksearchbox.SourceResult; -import com.android.quicksearchbox.SuggestionCursor; -import com.android.quicksearchbox.util.NamedTaskExecutor; - -/** - * Special source implementation for Google suggestions. - */ -public abstract class AbstractGoogleSource extends AbstractInternalSource implements GoogleSource { - - /* - * This name corresponds to what was used in previous version of quick search box. We use the - * same name so that shortcuts continue to work after an upgrade. (It also makes logging more - * consistent). - */ - private static final String GOOGLE_SOURCE_NAME = - "com.android.quicksearchbox/.google.GoogleSearch"; - - public AbstractGoogleSource(Context context, Handler uiThread, NamedTaskExecutor iconLoader) { - super(context, uiThread, iconLoader); - } - - @Override - public abstract ComponentName getIntentComponent(); - - @Override - public abstract SuggestionCursor refreshShortcut(String shortcutId, String extraData); - - /** - * Called by QSB to get web suggestions for a query. - */ - @Override - public abstract SourceResult queryInternal(String query); - - /** - * Called by external apps to get web suggestions for a query. - */ - @Override - public abstract SourceResult queryExternal(String query); - - @Override - public Intent createVoiceSearchIntent(Bundle appData) { - return createVoiceWebSearchIntent(appData); - } - - @Override - public String getDefaultIntentAction() { - return Intent.ACTION_WEB_SEARCH; - } - - @Override - public CharSequence getHint() { - return getContext().getString(R.string.google_search_hint); - } - - @Override - public CharSequence getLabel() { - return getContext().getString(R.string.google_search_label); - } - - @Override - public String getName() { - return GOOGLE_SOURCE_NAME; - } - - @Override - public CharSequence getSettingsDescription() { - return getContext().getString(R.string.google_search_description); - } - - @Override - protected int getSourceIconResource() { - return R.mipmap.google_icon; - } - - @Override - public SourceResult getSuggestions(String query, int queryLimit) { - return emptyIfNull(queryInternal(query), query); - } - - public SourceResult getSuggestionsExternal(String query) { - return emptyIfNull(queryExternal(query), query); - } - - private SourceResult emptyIfNull(SourceResult result, String query) { - return result == null ? new CursorBackedSourceResult(this, query) : result; - } - - @Override - public boolean voiceSearchEnabled() { - return true; - } - - @Override - public boolean includeInAll() { - return true; - } - -} diff --git a/src/com/android/quicksearchbox/google/AbstractGoogleSource.kt b/src/com/android/quicksearchbox/google/AbstractGoogleSource.kt new file mode 100644 index 0000000..32e9367 --- /dev/null +++ b/src/com/android/quicksearchbox/google/AbstractGoogleSource.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.google + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import com.android.quicksearchbox.AbstractInternalSource +import com.android.quicksearchbox.CursorBackedSourceResult +import com.android.quicksearchbox.R +import com.android.quicksearchbox.SourceResult +import com.android.quicksearchbox.SuggestionCursor +import com.android.quicksearchbox.util.NamedTaskExecutor + +/** Special source implementation for Google suggestions. */ +abstract class AbstractGoogleSource( + context: Context?, + uiThread: Handler?, + iconLoader: NamedTaskExecutor +) : + AbstractInternalSource(context, uiThread, iconLoader), + com.android.quicksearchbox.google.GoogleSource { + @get:Override abstract override val intentComponent: ComponentName? + + @Override + abstract override fun refreshShortcut(shortcutId: String?, extraData: String?): SuggestionCursor? + + /** Called by QSB to get web suggestions for a query. */ + @Override abstract override fun queryInternal(query: String?): SourceResult? + + /** Called by external apps to get web suggestions for a query. */ + @Override abstract override fun queryExternal(query: String?): SourceResult? + + @Override + override fun createVoiceSearchIntent(appData: Bundle?): Intent? { + return createVoiceWebSearchIntent(appData) + } + + @get:Override + override val defaultIntentAction: String + get() = Intent.ACTION_WEB_SEARCH + + @get:Override + override val hint: CharSequence + get() = context!!.getString(R.string.google_search_hint) + + @get:Override + override val label: CharSequence + get() = context!!.getString(R.string.google_search_label) + + @get:Override + override val name: String + get() = AbstractGoogleSource.Companion.GOOGLE_SOURCE_NAME + + @get:Override + override val settingsDescription: CharSequence + get() = context!!.getString(R.string.google_search_description) + + @get:Override + override val sourceIconResource: Int + get() = R.mipmap.google_icon + + @Override + override fun getSuggestions(query: String?, queryLimit: Int): SourceResult? { + return emptyIfNull(queryInternal(query), query) + } + + fun getSuggestionsExternal(query: String?): SourceResult { + return emptyIfNull(queryExternal(query), query) + } + + private fun emptyIfNull(result: SourceResult?, query: String?): SourceResult { + return if (result == null) CursorBackedSourceResult(this, query) else result + } + + @Override + override fun voiceSearchEnabled(): Boolean { + return true + } + + @Override + override fun includeInAll(): Boolean { + return true + } + + companion object { + /* + * This name corresponds to what was used in previous version of quick search box. We use the + * same name so that shortcuts continue to work after an upgrade. (It also makes logging more + * consistent). + */ + private const val GOOGLE_SOURCE_NAME = "com.android.quicksearchbox/.google.GoogleSearch" + } +} diff --git a/src/com/android/quicksearchbox/google/AbstractGoogleSourceResult.java b/src/com/android/quicksearchbox/google/AbstractGoogleSourceResult.java deleted file mode 100644 index 6eb8f9d..0000000 --- a/src/com/android/quicksearchbox/google/AbstractGoogleSourceResult.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.google; - -import com.android.quicksearchbox.R; -import com.android.quicksearchbox.Source; -import com.android.quicksearchbox.SourceResult; -import com.android.quicksearchbox.SuggestionExtras; - -import android.content.ComponentName; -import android.database.DataSetObserver; - -import java.util.Collection; - -public abstract class AbstractGoogleSourceResult implements SourceResult { - - private final Source mSource; - private final String mUserQuery; - private int mPos = 0; - - public AbstractGoogleSourceResult(Source source, String userQuery) { - mSource = source; - mUserQuery = userQuery; - } - - public abstract int getCount(); - - public abstract String getSuggestionQuery(); - - public Source getSource() { - return mSource; - } - - public void close() { - } - - public int getPosition() { - return mPos; - } - - public String getUserQuery() { - return mUserQuery; - } - - public void moveTo(int pos) { - mPos = pos; - } - - public boolean moveToNext() { - int size = getCount(); - if (mPos >= size) { - // Already past the end - return false; - } - mPos++; - return mPos < size; - } - - public void registerDataSetObserver(DataSetObserver observer) { - } - - public void unregisterDataSetObserver(DataSetObserver observer) { - } - - public String getSuggestionText1() { - return getSuggestionQuery(); - } - - public Source getSuggestionSource() { - return mSource; - } - - public boolean isSuggestionShortcut() { - return false; - } - - public String getShortcutId() { - return null; - } - - public String getSuggestionFormat() { - return null; - } - - public String getSuggestionIcon1() { - return String.valueOf(R.drawable.magnifying_glass); - } - - public String getSuggestionIcon2() { - return null; - } - - public String getSuggestionIntentAction() { - return mSource.getDefaultIntentAction(); - } - - public ComponentName getSuggestionIntentComponent() { - return mSource.getIntentComponent(); - } - - public String getSuggestionIntentDataString() { - return null; - } - - public String getSuggestionIntentExtraData() { - return null; - } - - public String getSuggestionLogType() { - return null; - } - - public String getSuggestionText2() { - return null; - } - - public String getSuggestionText2Url() { - return null; - } - - public boolean isSpinnerWhileRefreshing() { - return false; - } - - public boolean isWebSearchSuggestion() { - return true; - } - - public boolean isHistorySuggestion() { - return false; - } - - public SuggestionExtras getExtras() { - return null; - } - - public Collection<String> getExtraColumns() { - return null; - } -} diff --git a/src/com/android/quicksearchbox/google/AbstractGoogleSourceResult.kt b/src/com/android/quicksearchbox/google/AbstractGoogleSourceResult.kt new file mode 100644 index 0000000..9ee4d58 --- /dev/null +++ b/src/com/android/quicksearchbox/google/AbstractGoogleSourceResult.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.google + +import android.content.ComponentName +import android.database.DataSetObserver +import com.android.quicksearchbox.R +import com.android.quicksearchbox.Source +import com.android.quicksearchbox.SourceResult +import com.android.quicksearchbox.SuggestionExtras + +abstract class AbstractGoogleSourceResult(source: Source, userQuery: String) : SourceResult { + private val mSource: Source + override val userQuery: String + override var position = 0 + abstract override val count: Int + abstract override val suggestionQuery: String? + override val source: Source + get() = mSource + + override fun close() {} + override fun moveTo(pos: Int) { + position = pos + } + + override fun moveToNext(): Boolean { + val size = count + if (position >= size) { + // Already past the end + return false + } + position++ + return position < size + } + + override fun registerDataSetObserver(observer: DataSetObserver?) {} + override fun unregisterDataSetObserver(observer: DataSetObserver?) {} + override val suggestionText1: String? + get() = suggestionQuery + override val suggestionSource: Source + get() = mSource + override val isSuggestionShortcut: Boolean + get() = false + override val shortcutId: String? + get() = null + override val suggestionFormat: String? + get() = null + override val suggestionIcon1: String + get() = R.drawable.magnifying_glass.toString() + override val suggestionIcon2: String? + get() = null + override val suggestionIntentAction: String? + get() = mSource.defaultIntentAction + override val suggestionIntentComponent: ComponentName? + get() = mSource.intentComponent + override val suggestionIntentDataString: String? + get() = null + override val suggestionIntentExtraData: String? + get() = null + override val suggestionLogType: String? + get() = null + override val suggestionText2: String? + get() = null + override val suggestionText2Url: String? + get() = null + override val isSpinnerWhileRefreshing: Boolean + get() = false + override val isWebSearchSuggestion: Boolean + get() = true + override val isHistorySuggestion: Boolean + get() = false + override val extras: SuggestionExtras? + get() = null + override val extraColumns: Collection<String>? + get() = null + + init { + mSource = source + this.userQuery = userQuery + } +} diff --git a/src/com/android/quicksearchbox/google/GoogleSearch.java b/src/com/android/quicksearchbox/google/GoogleSearch.java deleted file mode 100644 index 58755d8..0000000 --- a/src/com/android/quicksearchbox/google/GoogleSearch.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2008 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.quicksearchbox.google; - -import com.android.common.Search; -import com.android.quicksearchbox.QsbApplication; - -import android.app.Activity; -import android.app.PendingIntent; -import android.app.SearchManager; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.location.Location; -import android.net.Uri; -import android.os.Bundle; -import android.provider.Browser; -import android.text.TextUtils; -import android.util.Log; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Locale; - -/** - * This class is purely here to get search queries and route them to - * the global {@link Intent#ACTION_WEB_SEARCH}. - */ -public class GoogleSearch extends Activity { - private static final String TAG = "GoogleSearch"; - private static final boolean DBG = false; - - // Used to figure out which domain to base search requests - // on. - private SearchBaseUrlHelper mSearchDomainHelper; - - // "source" parameter for Google search requests from unknown sources (e.g. apps). This will get - // prefixed with the string 'android-' before being sent on the wire. - final static String GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown"; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Intent intent = getIntent(); - String action = intent != null ? intent.getAction() : null; - - // This should probably be moved so as to - // send out the request to /checksearchdomain as early as possible. - mSearchDomainHelper = QsbApplication.get(this).getSearchBaseUrlHelper(); - - if (Intent.ACTION_WEB_SEARCH.equals(action) || Intent.ACTION_SEARCH.equals(action)) { - handleWebSearchIntent(intent); - } - - finish(); - } - - /** - * Construct the language code (hl= paramater) for the given locale. - */ - public static String getLanguage(Locale locale) { - String language = locale.getLanguage(); - StringBuilder hl = new StringBuilder(language); - String country = locale.getCountry(); - - if (!TextUtils.isEmpty(country) && useLangCountryHl(language, country)) { - hl.append('-'); - hl.append(country); - } - - if (DBG) Log.d(TAG, "language " + language + ", country " + country + " -> hl=" + hl); - return hl.toString(); - } - - // TODO: This is a workaround for bug 3232296. When that is fixed, this method can be removed. - private static boolean useLangCountryHl(String language, String country) { - // lang-country is currently only supported for a small number of locales - if ("en".equals(language)) { - return "GB".equals(country); - } else if ("zh".equals(language)) { - return "CN".equals(country) || "TW".equals(country); - } else if ("pt".equals(language)) { - return "BR".equals(country) || "PT".equals(country); - } else { - return false; - } - } - - private void handleWebSearchIntent(Intent intent) { - Intent launchUriIntent = createLaunchUriIntentFromSearchIntent(intent); - PendingIntent pending = - intent.getParcelableExtra(SearchManager.EXTRA_WEB_SEARCH_PENDINGINTENT); - if (pending == null || !launchPendingIntent(pending, launchUriIntent)) { - launchIntent(launchUriIntent); - } - } - - private Intent createLaunchUriIntentFromSearchIntent(Intent intent) { - String query = intent.getStringExtra(SearchManager.QUERY); - if (TextUtils.isEmpty(query)) { - Log.w(TAG, "Got search intent with no query."); - return null; - } - - // If the caller specified a 'source' url parameter, use that and if not use default. - Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); - String source = GOOGLE_SEARCH_SOURCE_UNKNOWN; - if (appSearchData != null) { - source = appSearchData.getString(Search.SOURCE); - } - - // The browser can pass along an application id which it uses to figure out which - // window to place a new search into. So if this exists, we'll pass it back to - // the browser. Otherwise, add our own package name as the application id, so that - // the browser can organize all searches launched from this provider together. - String applicationId = intent.getStringExtra(Browser.EXTRA_APPLICATION_ID); - if (applicationId == null) { - applicationId = getPackageName(); - } - - try { - String searchUri = mSearchDomainHelper.getSearchBaseUrl() - + "&source=android-" + source - + "&q=" + URLEncoder.encode(query, "UTF-8"); - Intent launchUriIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); - launchUriIntent.putExtra(Browser.EXTRA_APPLICATION_ID, applicationId); - launchUriIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return launchUriIntent; - } catch (UnsupportedEncodingException e) { - Log.w(TAG, "Error", e); - return null; - } - - } - - private void launchIntent(Intent intent) { - try { - Log.i(TAG, "Launching intent: " + intent.toUri(0)); - startActivity(intent); - } catch (ActivityNotFoundException ex) { - Log.w(TAG, "No activity found to handle: " + intent); - } - } - - private boolean launchPendingIntent(PendingIntent pending, Intent fillIn) { - try { - pending.send(this, Activity.RESULT_OK, fillIn); - return true; - } catch (PendingIntent.CanceledException ex) { - Log.i(TAG, "Pending intent cancelled: " + pending); - return false; - } - } - -} diff --git a/src/com/android/quicksearchbox/google/GoogleSearch.kt b/src/com/android/quicksearchbox/google/GoogleSearch.kt new file mode 100644 index 0000000..9c01cfe --- /dev/null +++ b/src/com/android/quicksearchbox/google/GoogleSearch.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.google + +import android.app.Activity +import android.app.PendingIntent +import android.app.SearchManager +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.Browser +import android.text.TextUtils +import android.util.Log +import com.android.common.Search +import com.android.quicksearchbox.QsbApplication +import java.io.UnsupportedEncodingException +import java.net.URLEncoder +import java.util.Locale + +/** + * This class is purely here to get search queries and route them to the global + * [Intent.ACTION_WEB_SEARCH]. + */ +class GoogleSearch : Activity() { + // Used to figure out which domain to base search requests + // on. + private var mSearchDomainHelper: SearchBaseUrlHelper? = null + + @Override + protected override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val intent: Intent? = getIntent() + val action: String? = if (intent != null) intent.getAction() else null + + // This should probably be moved so as to + // send out the request to /checksearchdomain as early as possible. + mSearchDomainHelper = QsbApplication.get(this).searchBaseUrlHelper + if (Intent.ACTION_WEB_SEARCH.equals(action) || Intent.ACTION_SEARCH.equals(action)) { + handleWebSearchIntent(intent) + } + finish() + } + + private fun handleWebSearchIntent(intent: Intent?) { + val launchUriIntent: Intent? = createLaunchUriIntentFromSearchIntent(intent) + + @Suppress("DEPRECATION") + val pending: PendingIntent? = + intent?.getParcelableExtra(SearchManager.EXTRA_WEB_SEARCH_PENDINGINTENT) + if (pending == null || !launchPendingIntent(pending, launchUriIntent)) { + launchIntent(launchUriIntent) + } + } + + private fun createLaunchUriIntentFromSearchIntent(intent: Intent?): Intent? { + val query: String? = intent?.getStringExtra(SearchManager.QUERY) + if (TextUtils.isEmpty(query)) { + Log.w(TAG, "Got search intent with no query.") + return null + } + + // If the caller specified a 'source' url parameter, use that and if not use default. + val appSearchData: Bundle? = intent?.getBundleExtra(SearchManager.APP_DATA) + var source: String? = GoogleSearch.Companion.GOOGLE_SEARCH_SOURCE_UNKNOWN + if (appSearchData != null) { + source = appSearchData.getString(Search.SOURCE) + } + + // The browser can pass along an application id which it uses to figure out which + // window to place a new search into. So if this exists, we'll pass it back to + // the browser. Otherwise, add our own package name as the application id, so that + // the browser can organize all searches launched from this provider together. + var applicationId: String? = intent?.getStringExtra(Browser.EXTRA_APPLICATION_ID) + if (applicationId == null) { + applicationId = getPackageName() + } + return try { + val searchUri = + (mSearchDomainHelper!!.searchBaseUrl.toString() + + "&source=android-" + + source + + "&q=" + + URLEncoder.encode(query, "UTF-8")) + val launchUriIntent = Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)) + launchUriIntent.putExtra(Browser.EXTRA_APPLICATION_ID, applicationId) + launchUriIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + launchUriIntent + } catch (e: UnsupportedEncodingException) { + Log.w(TAG, "Error", e) + null + } + } + + private fun launchIntent(intent: Intent?) { + try { + Log.i(TAG, "Launching intent: " + intent?.toUri(0)) + startActivity(intent) + } catch (ex: ActivityNotFoundException) { + Log.w(TAG, "No activity found to handle: $intent") + } + } + + private fun launchPendingIntent(pending: PendingIntent, fillIn: Intent?): Boolean { + return try { + pending.send(this, Activity.RESULT_OK, fillIn) + true + } catch (ex: PendingIntent.CanceledException) { + Log.i(TAG, "Pending intent cancelled: $pending") + false + } + } + + companion object { + private const val TAG = "GoogleSearch" + private const val DBG = false + + // "source" parameter for Google search requests from unknown sources (e.g. apps). This will get + // prefixed with the string 'android-' before being sent on the wire. + const val GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown" + + /** Construct the language code (hl= parameter) for the given locale. */ + fun getLanguage(locale: Locale): String { + val language: String = locale.getLanguage() + val hl: StringBuilder = StringBuilder(language) + val country: String = locale.getCountry() + if (!TextUtils.isEmpty(country) && useLangCountryHl(language, country)) { + hl.append('-') + hl.append(country) + } + if (DBG) Log.d(TAG, "language $language, country $country -> hl=$hl") + return hl.toString() + } + + // TODO: This is a workaround for bug 3232296. When that is fixed, this method can be removed. + private fun useLangCountryHl(language: String, country: String): Boolean { + // lang-country is currently only supported for a small number of locales + return if ("en".equals(language)) { + "GB".equals(country) + } else if ("zh".equals(language)) { + "CN".equals(country) || "TW".equals(country) + } else if ("pt".equals(language)) { + "BR".equals(country) || "PT".equals(country) + } else { + false + } + } + } +} diff --git a/src/com/android/quicksearchbox/google/GoogleSource.java b/src/com/android/quicksearchbox/google/GoogleSource.java deleted file mode 100644 index b566817..0000000 --- a/src/com/android/quicksearchbox/google/GoogleSource.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.google; - -import com.android.quicksearchbox.Source; -import com.android.quicksearchbox.SourceResult; -import com.android.quicksearchbox.SuggestionCursor; - -/** - * Special source interface for Google suggestions. - */ -public interface GoogleSource extends Source { - - SuggestionCursor refreshShortcut(String shortcutId, String extraData); - - /** - * Called by QSB to get web suggestions for a query. - */ - SourceResult queryInternal(String query); - - /** - * Called by external apps to get web suggestions for a query. - */ - SourceResult queryExternal(String query); - -} diff --git a/src/com/android/quicksearchbox/google/GoogleSource.kt b/src/com/android/quicksearchbox/google/GoogleSource.kt new file mode 100644 index 0000000..1a82211 --- /dev/null +++ b/src/com/android/quicksearchbox/google/GoogleSource.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.google + +import com.android.quicksearchbox.Source +import com.android.quicksearchbox.SourceResult +import com.android.quicksearchbox.SuggestionCursor + +/** Special source interface for Google suggestions. */ +interface GoogleSource : Source { + fun refreshShortcut(shortcutId: String?, extraData: String?): SuggestionCursor? + + /** Called by QSB to get web suggestions for a query. */ + fun queryInternal(query: String?): SourceResult? + + /** Called by external apps to get web suggestions for a query. */ + fun queryExternal(query: String?): SourceResult? +} diff --git a/src/com/android/quicksearchbox/google/GoogleSuggestClient.java b/src/com/android/quicksearchbox/google/GoogleSuggestClient.java deleted file mode 100644 index 51c5129..0000000 --- a/src/com/android/quicksearchbox/google/GoogleSuggestClient.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.google; - -import android.content.ComponentName; -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Build; -import android.os.Handler; -import android.text.TextUtils; -import android.util.Log; - -import com.android.quicksearchbox.Config; -import com.android.quicksearchbox.R; -import com.android.quicksearchbox.Source; -import com.android.quicksearchbox.SourceResult; -import com.android.quicksearchbox.SuggestionCursor; -import com.android.quicksearchbox.util.NamedTaskExecutor; - -import org.json.JSONArray; -import org.json.JSONException; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.net.URLEncoder; -import java.util.Locale; - -/** - * Use network-based Google Suggests to provide search suggestions. - */ -public class GoogleSuggestClient extends AbstractGoogleSource { - - private static final boolean DBG = false; - private static final String LOG_TAG = "GoogleSearch"; - - private static final String USER_AGENT = "Android/" + Build.VERSION.RELEASE; - private String mSuggestUri; - - // TODO: this should be defined somewhere - private static final String HTTP_TIMEOUT = "http.conn-manager.timeout"; - - private final int mConnectTimeout; - - public GoogleSuggestClient(Context context, Handler uiThread, - NamedTaskExecutor iconLoader, Config config) { - super(context, uiThread, iconLoader); - - mConnectTimeout = config.getHttpConnectTimeout(); - // NOTE: Do not look up the resource here; Localization changes may not have completed - // yet (e.g. we may still be reading the SIM card). - mSuggestUri = null; - } - - @Override - public ComponentName getIntentComponent() { - return new ComponentName(getContext(), GoogleSearch.class); - } - - @Override - public SourceResult queryInternal(String query) { - return query(query); - } - - @Override - public SourceResult queryExternal(String query) { - return query(query); - } - - /** - * Queries for a given search term and returns a cursor containing - * suggestions ordered by best match. - */ - private SourceResult query(String query) { - if (TextUtils.isEmpty(query)) { - return null; - } - if (!isNetworkConnected()) { - Log.i(LOG_TAG, "Not connected to network."); - return null; - } - HttpURLConnection connection = null; - try { - String encodedQuery = URLEncoder.encode(query, "UTF-8"); - if (mSuggestUri == null) { - Locale l = Locale.getDefault(); - String language = GoogleSearch.getLanguage(l); - mSuggestUri = getContext().getResources().getString(R.string.google_suggest_base, - language); - } - - String suggestUri = mSuggestUri + encodedQuery; - if (DBG) Log.d(LOG_TAG, "Sending request: " + suggestUri); - URL url = URI.create(suggestUri).toURL(); - connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(mConnectTimeout); - connection.setRequestProperty("User-Agent", USER_AGENT); - connection.setRequestMethod("GET"); - connection.setDoInput(true); - connection.connect(); - InputStream inputStream = connection.getInputStream(); - if (connection.getResponseCode() == 200) { - - /* Goto http://www.google.com/complete/search?json=true&q=foo - * to see what the data format looks like. It's basically a json - * array containing 4 other arrays. We only care about the middle - * 2 which contain the suggestions and their popularity. - */ - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - reader.close(); - JSONArray results = new JSONArray(sb.toString()); - JSONArray suggestions = results.getJSONArray(1); - JSONArray popularity = results.getJSONArray(2); - if (DBG) Log.d(LOG_TAG, "Got " + suggestions.length() + " results"); - return new GoogleSuggestCursor(this, query, suggestions, popularity); - } else { - if (DBG) - Log.d(LOG_TAG, "Request failed " + connection.getResponseMessage()); - } - } catch (UnsupportedEncodingException e) { - Log.w(LOG_TAG, "Error", e); - } catch (IOException e) { - Log.w(LOG_TAG, "Error", e); - } catch (JSONException e) { - Log.w(LOG_TAG, "Error", e); - } finally { - if (connection != null) connection.disconnect(); - } - return null; - } - - @Override - public SuggestionCursor refreshShortcut(String shortcutId, String oldExtraData) { - return null; - } - - private boolean isNetworkConnected() { - NetworkInfo networkInfo = getActiveNetworkInfo(); - return networkInfo != null && networkInfo.isConnected(); - } - - private NetworkInfo getActiveNetworkInfo() { - ConnectivityManager connectivity = - (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivity == null) { - return null; - } - return connectivity.getActiveNetworkInfo(); - } - - private static class GoogleSuggestCursor extends AbstractGoogleSourceResult { - - /* Contains the actual suggestions */ - private final JSONArray mSuggestions; - - /* This contains the popularity of each suggestion - * i.e. 165,000 results. It's not related to sorting. - */ - private final JSONArray mPopularity; - - public GoogleSuggestCursor(Source source, String userQuery, - JSONArray suggestions, JSONArray popularity) { - super(source, userQuery); - mSuggestions = suggestions; - mPopularity = popularity; - } - - @Override - public int getCount() { - return mSuggestions.length(); - } - - @Override - public String getSuggestionQuery() { - try { - return mSuggestions.getString(getPosition()); - } catch (JSONException e) { - Log.w(LOG_TAG, "Error parsing response: " + e); - return null; - } - } - - @Override - public String getSuggestionText2() { - try { - return mPopularity.getString(getPosition()); - } catch (JSONException e) { - Log.w(LOG_TAG, "Error parsing response: " + e); - return null; - } - } - } -} diff --git a/src/com/android/quicksearchbox/google/GoogleSuggestClient.kt b/src/com/android/quicksearchbox/google/GoogleSuggestClient.kt new file mode 100644 index 0000000..610cfdd --- /dev/null +++ b/src/com/android/quicksearchbox/google/GoogleSuggestClient.kt @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.google + +import android.content.ComponentName +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import android.os.Handler +import android.text.TextUtils +import android.util.Log +import com.android.quicksearchbox.Config +import com.android.quicksearchbox.R +import com.android.quicksearchbox.Source +import com.android.quicksearchbox.SourceResult +import com.android.quicksearchbox.SuggestionCursor +import com.android.quicksearchbox.util.NamedTaskExecutor +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.UnsupportedEncodingException +import java.net.HttpURLConnection +import java.net.URI +import java.net.URL +import java.net.URLEncoder +import java.util.Locale +import org.json.JSONArray +import org.json.JSONException + +/** Use network-based Google Suggests to provide search suggestions. */ +class GoogleSuggestClient( + context: Context?, + uiThread: Handler?, + iconLoader: NamedTaskExecutor, + config: Config +) : AbstractGoogleSource(context, uiThread, iconLoader) { + private var mSuggestUri: String? + private val mConnectTimeout: Int + + @get:Override + override val intentComponent: ComponentName + get() = ComponentName(context!!, GoogleSearch::class.java) + + @Override + override fun queryInternal(query: String?): SourceResult? { + return query(query) + } + + @Override + override fun queryExternal(query: String?): SourceResult? { + return query(query) + } + + /** + * Queries for a given search term and returns a cursor containing suggestions ordered by best + * match. + */ + private fun query(query: String?): SourceResult? { + if (TextUtils.isEmpty(query)) { + return null + } + if (!isNetworkConnected) { + Log.i(LOG_TAG, "Not connected to network.") + return null + } + var connection: HttpURLConnection? = null + try { + val encodedQuery: String = URLEncoder.encode(query, "UTF-8") + if (mSuggestUri == null) { + val l: Locale = Locale.getDefault() + val language: String = GoogleSearch.getLanguage(l) + mSuggestUri = context?.getResources()!!.getString(R.string.google_suggest_base, language) + } + val suggestUri = mSuggestUri + encodedQuery + if (DBG) Log.d(LOG_TAG, "Sending request: $suggestUri") + val url: URL = URI.create(suggestUri).toURL() + connection = url.openConnection() as HttpURLConnection + connection.setConnectTimeout(mConnectTimeout) + connection.setRequestProperty("User-Agent", USER_AGENT) + connection.setRequestMethod("GET") + connection.setDoInput(true) + connection.connect() + val inputStream: InputStream = connection.getInputStream() + if (connection.getResponseCode() == 200) { + + /* Goto http://www.google.com/complete/search?json=true&q=foo + * to see what the data format looks like. It's basically a json + * array containing 4 other arrays. We only care about the middle + * 2 which contain the suggestions and their popularity. + */ + val reader = BufferedReader(InputStreamReader(inputStream)) + val sb: StringBuilder = StringBuilder() + var line: String? + while (reader.readLine().also { line = it } != null) { + sb.append(line).append("\n") + } + reader.close() + val results = JSONArray(sb.toString()) + val suggestions: JSONArray = results.getJSONArray(1) + val popularity: JSONArray = results.getJSONArray(2) + if (DBG) Log.d(LOG_TAG, "Got " + suggestions.length().toString() + " results") + return GoogleSuggestCursor(this, query, suggestions, popularity) + } else { + if (DBG) Log.d(LOG_TAG, "Request failed " + connection.getResponseMessage()) + } + } catch (e: UnsupportedEncodingException) { + Log.w(LOG_TAG, "Error", e) + } catch (e: IOException) { + Log.w(LOG_TAG, "Error", e) + } catch (e: JSONException) { + Log.w(LOG_TAG, "Error", e) + } finally { + if (connection != null) connection.disconnect() + } + return null + } + + @Override + override fun refreshShortcut(shortcutId: String?, extraData: String?): SuggestionCursor? { + return null + } + + private val isNetworkConnected: Boolean + get() { + val actNC = activeNetworkCapabilities + return actNC != null && actNC.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } + private val activeNetworkCapabilities: NetworkCapabilities? + get() { + val connectivityManager = + context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetwork = connectivityManager.getActiveNetwork() + return connectivityManager.getNetworkCapabilities(activeNetwork) + } + + private class GoogleSuggestCursor( + source: Source, + userQuery: String?, + suggestions: JSONArray, + popularity: JSONArray + ) : AbstractGoogleSourceResult(source, userQuery!!) { + /* Contains the actual suggestions */ + private val mSuggestions: JSONArray + + /* This contains the popularity of each suggestion + * i.e. 165,000 results. It's not related to sorting. + */ + private val mPopularity: JSONArray + + @get:Override + override val count: Int + get() = mSuggestions.length() + + @get:Override + override val suggestionQuery: String? + get() = + try { + mSuggestions.getString(position) + } catch (e: JSONException) { + Log.w(LOG_TAG, "Error parsing response: $e") + null + } + + @get:Override + override val suggestionText2: String? + get() = + try { + mPopularity.getString(position) + } catch (e: JSONException) { + Log.w(LOG_TAG, "Error parsing response: $e") + null + } + + init { + mSuggestions = suggestions + mPopularity = popularity + } + } + + companion object { + private const val DBG = false + private const val LOG_TAG = "GoogleSearch" + private val USER_AGENT = "Android/" + Build.VERSION.RELEASE + + // TODO: this should be defined somewhere + private const val HTTP_TIMEOUT = "http.conn-manager.timeout" + } + + init { + mConnectTimeout = config.httpConnectTimeout + // NOTE: Do not look up the resource here; Localization changes may not have completed + // yet (e.g. we may still be reading the SIM card). + mSuggestUri = null + } +} diff --git a/src/com/android/quicksearchbox/google/GoogleSuggestionProvider.java b/src/com/android/quicksearchbox/google/GoogleSuggestionProvider.java deleted file mode 100644 index 02f9d38..0000000 --- a/src/com/android/quicksearchbox/google/GoogleSuggestionProvider.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2008 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.quicksearchbox.google; - -import com.android.quicksearchbox.CursorBackedSourceResult; -import com.android.quicksearchbox.QsbApplication; -import com.android.quicksearchbox.Source; -import com.android.quicksearchbox.SourceResult; -import com.android.quicksearchbox.SuggestionCursorBackedCursor; - -import android.app.SearchManager; -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.UriMatcher; -import android.database.Cursor; -import android.net.Uri; -import android.util.Log; - -/** - * A suggestion provider which provides content from Genie, a service that offers - * a superset of the content provided by Google Suggest. - */ -public class GoogleSuggestionProvider extends ContentProvider { - private static final boolean DBG = false; - private static final String TAG = "QSB.GoogleSuggestionProvider"; - - // UriMatcher constants - private static final int SEARCH_SUGGEST = 0; - private static final int SEARCH_SHORTCUT = 1; - - private UriMatcher mUriMatcher; - - private GoogleSource mSource; - - @Override - public boolean onCreate() { - mSource = QsbApplication.get(getContext()).getGoogleSource(); - mUriMatcher = buildUriMatcher(getContext()); - return true; - } - - /** - * This will always return {@link SearchManager#SUGGEST_MIME_TYPE} as this - * provider is purely to provide suggestions. - */ - @Override - public String getType(Uri uri) { - return SearchManager.SUGGEST_MIME_TYPE; - } - - private SourceResult emptyIfNull(SourceResult result, GoogleSource source, String query) { - return result == null ? new CursorBackedSourceResult(source, query) : result; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - - if (DBG) Log.d(TAG, "query uri=" + uri); - int match = mUriMatcher.match(uri); - - if (match == SEARCH_SUGGEST) { - String query = getQuery(uri); - return new SuggestionCursorBackedCursor( - emptyIfNull(mSource.queryExternal(query), mSource, query)); - } else if (match == SEARCH_SHORTCUT) { - String shortcutId = getQuery(uri); - String extraData = - uri.getQueryParameter(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); - return new SuggestionCursorBackedCursor(mSource.refreshShortcut(shortcutId, extraData)); - } else { - throw new IllegalArgumentException("Unknown URI " + uri); - } - } - - /** - * Gets the search text from a uri. - */ - private String getQuery(Uri uri) { - if (uri.getPathSegments().size() > 1) { - return uri.getLastPathSegment(); - } else { - return ""; - } - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - throw new UnsupportedOperationException(); - } - - @Override - public int update(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - throw new UnsupportedOperationException(); - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); - } - - private UriMatcher buildUriMatcher(Context context) { - String authority = getAuthority(context); - UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); - matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY, - SEARCH_SUGGEST); - matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", - SEARCH_SUGGEST); - matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_SHORTCUT, - SEARCH_SHORTCUT); - matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", - SEARCH_SHORTCUT); - return matcher; - } - - protected String getAuthority(Context context) { - return context.getPackageName() + ".google"; - } - -} diff --git a/src/com/android/quicksearchbox/google/GoogleSuggestionProvider.kt b/src/com/android/quicksearchbox/google/GoogleSuggestionProvider.kt new file mode 100644 index 0000000..337d7fc --- /dev/null +++ b/src/com/android/quicksearchbox/google/GoogleSuggestionProvider.kt @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.google + +import android.app.SearchManager +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.content.UriMatcher +import android.database.Cursor +import android.net.Uri +import android.util.Log +import com.android.quicksearchbox.CursorBackedSourceResult +import com.android.quicksearchbox.QsbApplication +import com.android.quicksearchbox.SourceResult +import com.android.quicksearchbox.SuggestionCursorBackedCursor + +/** + * A suggestion provider which provides content from Genie, a service that offers a superset of the + * content provided by Google Suggest. + */ +class GoogleSuggestionProvider : ContentProvider() { + private var mUriMatcher: UriMatcher? = null + private var mSource: GoogleSource? = null + + @Override + override fun onCreate(): Boolean { + mSource = QsbApplication.get(getContext()).googleSource + mUriMatcher = buildUriMatcher(getContext()) + return true + } + + /** + * This will always return [SearchManager.SUGGEST_MIME_TYPE] as this provider is purely to provide + * suggestions. + */ + @Override + override fun getType(uri: Uri): String? { + return SearchManager.SUGGEST_MIME_TYPE + } + + private fun emptyIfNull( + result: SourceResult?, + source: GoogleSource?, + query: String? + ): SourceResult { + return result ?: CursorBackedSourceResult(source, query) + } + + @Override + override fun query( + uri: Uri, + projection: Array<String?>?, + selection: String?, + selectionArgs: Array<String?>?, + sortOrder: String? + ): Cursor { + if (GoogleSuggestionProvider.Companion.DBG) + Log.d(GoogleSuggestionProvider.Companion.TAG, "query uri=$uri") + val match: Int? = mUriMatcher?.match(uri) + return if (match == GoogleSuggestionProvider.Companion.SEARCH_SUGGEST) { + val query = getQuery(uri) + SuggestionCursorBackedCursor(emptyIfNull(mSource!!.queryExternal(query), mSource, query)) + } else if (match == GoogleSuggestionProvider.Companion.SEARCH_SHORTCUT) { + val shortcutId = getQuery(uri) + val extraData: String? = uri.getQueryParameter(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA) + SuggestionCursorBackedCursor(mSource!!.refreshShortcut(shortcutId, extraData)) + } else { + throw IllegalArgumentException("Unknown URI $uri") + } + } + + /** Gets the search text from a uri. */ + private fun getQuery(uri: Uri): String? { + return if (uri.getPathSegments().size > 1) { + uri.getLastPathSegment() + } else { + "" + } + } + + @Override + override fun insert(uri: Uri, values: ContentValues?): Uri? { + throw UnsupportedOperationException() + } + + @Override + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array<String?>? + ): Int { + throw UnsupportedOperationException() + } + + @Override + override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String?>?): Int { + throw UnsupportedOperationException() + } + + private fun buildUriMatcher(context: Context?): UriMatcher { + val authority = getAuthority(context) + val matcher = UriMatcher(UriMatcher.NO_MATCH) + matcher.addURI( + authority, + SearchManager.SUGGEST_URI_PATH_QUERY, + GoogleSuggestionProvider.Companion.SEARCH_SUGGEST + ) + matcher.addURI( + authority, + SearchManager.SUGGEST_URI_PATH_QUERY.toString() + "/*", + GoogleSuggestionProvider.Companion.SEARCH_SUGGEST + ) + matcher.addURI( + authority, + SearchManager.SUGGEST_URI_PATH_SHORTCUT, + GoogleSuggestionProvider.Companion.SEARCH_SHORTCUT + ) + matcher.addURI( + authority, + SearchManager.SUGGEST_URI_PATH_SHORTCUT.toString() + "/*", + GoogleSuggestionProvider.Companion.SEARCH_SHORTCUT + ) + return matcher + } + + protected fun getAuthority(context: Context?): String { + return context?.getPackageName().toString() + ".google" + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.GoogleSuggestionProvider" + + // UriMatcher constants + private const val SEARCH_SUGGEST = 0 + private const val SEARCH_SHORTCUT = 1 + } +} diff --git a/src/com/android/quicksearchbox/google/SearchBaseUrlHelper.java b/src/com/android/quicksearchbox/google/SearchBaseUrlHelper.java deleted file mode 100644 index d95214f..0000000 --- a/src/com/android/quicksearchbox/google/SearchBaseUrlHelper.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.google; - -import com.android.quicksearchbox.R; -import com.android.quicksearchbox.SearchSettings; -import com.android.quicksearchbox.SearchSettingsImpl; -import com.android.quicksearchbox.util.HttpHelper; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.text.TextUtils; -import android.util.Log; - -import java.util.Locale; - -/** - * Helper to build the base URL for all search requests. - */ -public class SearchBaseUrlHelper implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final boolean DBG = false; - private static final String TAG = "QSB.SearchBaseUrlHelper"; - - private static final String DOMAIN_CHECK_URL = - "https://www.google.com/searchdomaincheck?format=domain"; - - private static final long SEARCH_BASE_URL_EXPIRY_MS = 24 * 3600 * 1000L; - - private final HttpHelper mHttpHelper; - private final Context mContext; - private final SearchSettings mSearchSettings; - - /** - * Note that this constructor will spawn a thread to issue a HTTP - * request if shouldUseGoogleCom is false. - */ - public SearchBaseUrlHelper(Context context, HttpHelper helper, - SearchSettings searchSettings, SharedPreferences prefs) { - mHttpHelper = helper; - mContext = context; - mSearchSettings = searchSettings; - - // Note: This earlier used an inner class, but that causes issues - // because SharedPreferencesImpl uses a WeakHashMap< > and the listener - // will be GC'ed unless we keep a reference to it here. - prefs.registerOnSharedPreferenceChangeListener(this); - - maybeUpdateBaseUrlSetting(false); - } - - /** - * Update the base search url, either: - * (a) it has never been set (first run) - * (b) it has expired - * (c) if the caller forces an update by setting the "force" parameter. - * - * @param force if true, then the URL is reset whether or not it has - * expired. - */ - public void maybeUpdateBaseUrlSetting(boolean force) { - long lastUpdateTime = mSearchSettings.getSearchBaseDomainApplyTime(); - long currentTime = System.currentTimeMillis(); - - if (force || lastUpdateTime == -1 || - currentTime - lastUpdateTime >= SEARCH_BASE_URL_EXPIRY_MS) { - if (mSearchSettings.shouldUseGoogleCom()) { - setSearchBaseDomain(getDefaultBaseDomain()); - } else { - checkSearchDomain(); - } - } - } - - /** - * @return the base url for searches. - */ - public String getSearchBaseUrl() { - return mContext.getResources().getString(R.string.google_search_base_pattern, - getSearchDomain(), GoogleSearch.getLanguage(Locale.getDefault())); - } - - /** - * @return the search domain. This is of the form "google.co.xx" or "google.com", - * used by UI code. - */ - public String getSearchDomain() { - String domain = mSearchSettings.getSearchBaseDomain(); - - if (domain == null) { - if (DBG) { - Log.w(TAG, "Search base domain was null, last apply time=" + - mSearchSettings.getSearchBaseDomainApplyTime()); - } - - // This is required to deal with the case wherein getSearchDomain - // is called before checkSearchDomain returns a valid URL. This will - // happen *only* on the first run of the app when the "use google.com" - // option is unchecked. In other cases, the previously set domain (or - // the default) will be returned. - // - // We have no choice in this case but to use the default search domain. - domain = getDefaultBaseDomain(); - } - - if (domain.startsWith(".")) { - if (DBG) Log.d(TAG, "Prepending www to " + domain); - domain = "www" + domain; - } - return domain; - } - - /** - * Issue a request to google.com/searchdomaincheck to retrieve the base - * URL for search requests. - */ - private void checkSearchDomain() { - final HttpHelper.GetRequest request = new HttpHelper.GetRequest(DOMAIN_CHECK_URL); - - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void ... params) { - if (DBG) Log.d(TAG, "Starting request to /searchdomaincheck"); - String domain; - try { - domain = mHttpHelper.get(request); - } catch (Exception e) { - if (DBG) Log.d(TAG, "Request to /searchdomaincheck failed : " + e); - // Swallow any exceptions thrown by the HTTP helper, in - // this rare case, we just use the default URL. - domain = getDefaultBaseDomain(); - - return null; - } - - if (DBG) Log.d(TAG, "Request to /searchdomaincheck succeeded"); - setSearchBaseDomain(domain); - - return null; - } - }.execute(); - } - - private String getDefaultBaseDomain() { - return mContext.getResources().getString(R.string.default_search_domain); - } - - private void setSearchBaseDomain(String domain) { - if (DBG) Log.d(TAG, "Setting search domain to : " + domain); - - mSearchSettings.setSearchBaseDomain(domain); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences pref, String key) { - // Listen for changes only to the SEARCH_BASE_URL preference. - if (DBG) Log.d(TAG, "Handling changed preference : " + key); - if (SearchSettingsImpl.USE_GOOGLE_COM_PREF.equals(key)) { - maybeUpdateBaseUrlSetting(true); - } - } -}
\ No newline at end of file diff --git a/src/com/android/quicksearchbox/google/SearchBaseUrlHelper.kt b/src/com/android/quicksearchbox/google/SearchBaseUrlHelper.kt new file mode 100644 index 0000000..b78d49e --- /dev/null +++ b/src/com/android/quicksearchbox/google/SearchBaseUrlHelper.kt @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.google + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import com.android.quicksearchbox.R +import com.android.quicksearchbox.SearchSettings +import com.android.quicksearchbox.SearchSettingsImpl +import com.android.quicksearchbox.util.HttpHelper +import java.util.Locale +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async + +/** Helper to build the base URL for all search requests. */ +class SearchBaseUrlHelper( + context: Context?, + helper: HttpHelper, + searchSettings: SearchSettings, + prefs: SharedPreferences +) : SharedPreferences.OnSharedPreferenceChangeListener { + private val mHttpHelper: HttpHelper + private val mContext: Context? + private val mSearchSettings: SearchSettings + private val scope = CoroutineScope(Dispatchers.IO) + + /** + * Update the base search url, either: (a) it has never been set (first run) (b) it has expired + * (c) if the caller forces an update by setting the "force" parameter. + * + * @param force if true, then the URL is reset whether or not it has expired. + */ + fun maybeUpdateBaseUrlSetting(force: Boolean) { + val lastUpdateTime: Long = mSearchSettings.searchBaseDomainApplyTime + val currentTime: Long = System.currentTimeMillis() + if ( + force || lastUpdateTime == -1L || currentTime - lastUpdateTime >= SEARCH_BASE_URL_EXPIRY_MS + ) { + if (mSearchSettings.shouldUseGoogleCom()) { + setSearchBaseDomain(defaultBaseDomain) + } else { + checkSearchDomain() + } + } + } + + /** @return the base url for searches. */ + val searchBaseUrl: String? + get() = + mContext + ?.getResources() + ?.getString( + R.string.google_search_base_pattern, + searchDomain, + GoogleSearch.getLanguage(Locale.getDefault()) + ) // This is required to deal with the case wherein getSearchDomain + // is called before checkSearchDomain returns a valid URL. This will + // happen *only* on the first run of the app when the "use google.com" + // option is unchecked. In other cases, the previously set domain (or + // the default) will be returned. + // + // We have no choice in this case but to use the default search domain. + /** + * @return the search domain. This is of the form "google.co.xx" or "google.com", used by UI code. + */ + val searchDomain: String? + get() { + var domain: String? = mSearchSettings.searchBaseDomain + if (domain == null) { + if (DBG) { + Log.w( + TAG, + "Search base domain was null, last apply time=" + + mSearchSettings.searchBaseDomainApplyTime + ) + } + + // This is required to deal with the case wherein getSearchDomain + // is called before checkSearchDomain returns a valid URL. This will + // happen *only* on the first run of the app when the "use google.com" + // option is unchecked. In other cases, the previously set domain (or + // the default) will be returned. + // + // We have no choice in this case but to use the default search domain. + domain = defaultBaseDomain + } + if (domain?.startsWith(".") == true) { + if (DBG) Log.d(TAG, "Prepending www to $domain") + domain = "www$domain" + } + return domain + } + + /** + * Issue a request to google.com/searchdomaincheck to retrieve the base URL for search requests. + */ + private fun checkSearchDomain() { + val request = HttpHelper.GetRequest(DOMAIN_CHECK_URL) + scope.async { + if (DBG) Log.d(TAG, "Starting request to /searchdomaincheck") + var domain: String? + try { + domain = mHttpHelper[request] + } catch (e: Exception) { + if (DBG) Log.d(TAG, "Request to /searchdomaincheck failed : $e") + // Swallow any exceptions thrown by the HTTP helper, in + // this rare case, we just use the default URL. + domain = defaultBaseDomain + } + if (DBG) Log.d(TAG, "Request to /searchdomaincheck succeeded") + setSearchBaseDomain(domain) + } + } + + private val defaultBaseDomain: String? + get() = mContext?.getResources()?.getString(R.string.default_search_domain) + + private fun setSearchBaseDomain(domain: String?) { + if (DBG) Log.d(TAG, "Setting search domain to : $domain") + mSearchSettings.searchBaseDomain = domain + } + + @Override + override fun onSharedPreferenceChanged(pref: SharedPreferences?, key: String?) { + // Listen for changes only to the SEARCH_BASE_URL preference. + if (DBG) Log.d(TAG, "Handling changed preference : $key") + if (SearchSettingsImpl.USE_GOOGLE_COM_PREF.equals(key)) { + maybeUpdateBaseUrlSetting(true) + } + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.SearchBaseUrlHelper" + private const val DOMAIN_CHECK_URL = "https://www.google.com/searchdomaincheck?format=domain" + private const val SEARCH_BASE_URL_EXPIRY_MS = 24 * 3600 * 1000L + } + + /** + * Note that this constructor will spawn a thread to issue a HTTP request if shouldUseGoogleCom is + * false. + */ + init { + mHttpHelper = helper + mContext = context + mSearchSettings = searchSettings + + // Note: This earlier used an inner class, but that causes issues + // because SharedPreferencesImpl uses a WeakHashMap< > and the listener + // will be GC'ed unless we keep a reference to it here. + prefs.registerOnSharedPreferenceChangeListener(this) + maybeUpdateBaseUrlSetting(false) + } +} diff --git a/src/com/android/quicksearchbox/ui/BaseSuggestionView.java b/src/com/android/quicksearchbox/ui/BaseSuggestionView.java deleted file mode 100644 index ed7f74b..0000000 --- a/src/com/android/quicksearchbox/ui/BaseSuggestionView.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import com.android.quicksearchbox.R; -import com.android.quicksearchbox.Suggestion; - -import android.content.Context; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -/** - * Base class for suggestion views. - */ -public abstract class BaseSuggestionView extends RelativeLayout implements SuggestionView { - - protected TextView mText1; - protected TextView mText2; - protected ImageView mIcon1; - protected ImageView mIcon2; - private long mSuggestionId; - private SuggestionsAdapter<?> mAdapter; - - public BaseSuggestionView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public BaseSuggestionView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public BaseSuggestionView(Context context) { - super(context); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mText1 = (TextView) findViewById(R.id.text1); - mText2 = (TextView) findViewById(R.id.text2); - mIcon1 = (ImageView) findViewById(R.id.icon1); - mIcon2 = (ImageView) findViewById(R.id.icon2); - } - - @Override - public void bindAsSuggestion(Suggestion suggestion, String userQuery) { - setOnClickListener(new ClickListener()); - } - - @Override - public void bindAdapter(SuggestionsAdapter<?> adapter, long suggestionId) { - mAdapter = adapter; - mSuggestionId = suggestionId; - } - - protected boolean isFromHistory(Suggestion suggestion) { - return suggestion.isSuggestionShortcut() || suggestion.isHistorySuggestion(); - } - - /** - * Sets the first text line. - */ - protected void setText1(CharSequence text) { - mText1.setText(text); - } - - /** - * Sets the second text line. - */ - protected void setText2(CharSequence text) { - mText2.setText(text); - if (TextUtils.isEmpty(text)) { - mText2.setVisibility(GONE); - } else { - mText2.setVisibility(VISIBLE); - } - } - - protected void onSuggestionClicked() { - if (mAdapter != null) { - mAdapter.onSuggestionClicked(mSuggestionId); - } - } - - protected void onSuggestionQueryRefineClicked() { - if (mAdapter != null) { - mAdapter.onSuggestionQueryRefineClicked(mSuggestionId); - } - } - - private class ClickListener implements OnClickListener { - @Override - public void onClick(View v) { - onSuggestionClicked(); - } - } - -} diff --git a/src/com/android/quicksearchbox/ui/BaseSuggestionView.kt b/src/com/android/quicksearchbox/ui/BaseSuggestionView.kt new file mode 100644 index 0000000..ec40f50 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/BaseSuggestionView.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.text.TextUtils +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import com.android.quicksearchbox.R +import com.android.quicksearchbox.Suggestion + +/** Base class for suggestion views. */ +abstract class BaseSuggestionView : RelativeLayout, SuggestionView { + @JvmField protected var mText1: TextView? = null + @JvmField protected var mText2: TextView? = null + @JvmField protected var mIcon1: ImageView? = null + @JvmField protected var mIcon2: ImageView? = null + private var mSuggestionId: Long = 0 + private var mAdapter: SuggestionsAdapter<*>? = null + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyle: Int + ) : super(context, attrs, defStyle) + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?) : super(context) + + @Override + protected override fun onFinishInflate() { + super.onFinishInflate() + mText1 = findViewById(R.id.text1) as TextView? + mText2 = findViewById(R.id.text2) as TextView? + mIcon1 = findViewById(R.id.icon1) as ImageView? + mIcon2 = findViewById(R.id.icon2) as ImageView? + } + + @Override + override fun bindAsSuggestion(suggestion: Suggestion?, userQuery: String?) { + setOnClickListener(ClickListener()) + } + + @Override + override fun bindAdapter(adapter: SuggestionsAdapter<*>?, position: Long) { + mAdapter = adapter + mSuggestionId = position + } + + protected fun isFromHistory(suggestion: Suggestion?): Boolean { + return suggestion?.isSuggestionShortcut == true || suggestion?.isHistorySuggestion == true + } + + /** Sets the first text line. */ + protected fun setText1(text: CharSequence?) { + mText1?.setText(text) + } + + /** Sets the second text line. */ + protected fun setText2(text: CharSequence?) { + mText2?.setText(text) + if (TextUtils.isEmpty(text)) { + mText2?.setVisibility(GONE) + } else { + mText2?.setVisibility(VISIBLE) + } + } + + protected fun onSuggestionClicked() { + if (mAdapter != null) { + mAdapter!!.onSuggestionClicked(mSuggestionId) + } + } + + protected fun onSuggestionQueryRefineClicked() { + if (mAdapter != null) { + mAdapter!!.onSuggestionQueryRefineClicked(mSuggestionId) + } + } + + private inner class ClickListener : OnClickListener { + @Override + override fun onClick(v: View?) { + onSuggestionClicked() + } + } +} diff --git a/src/com/android/quicksearchbox/ui/ClusteredSuggestionsView.java b/src/com/android/quicksearchbox/ui/ClusteredSuggestionsView.java deleted file mode 100644 index 9427024..0000000 --- a/src/com/android/quicksearchbox/ui/ClusteredSuggestionsView.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ExpandableListAdapter; -import android.widget.ExpandableListView; - -/** - * Suggestions view that displays suggestions clustered by corpus type. - */ -public class ClusteredSuggestionsView extends ExpandableListView - implements SuggestionsListView<ExpandableListAdapter> { - - SuggestionsAdapter<ExpandableListAdapter> mSuggestionsAdapter; - - public ClusteredSuggestionsView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void setSuggestionsAdapter(SuggestionsAdapter<ExpandableListAdapter> adapter) { - mSuggestionsAdapter = adapter; - super.setAdapter(adapter == null ? null : adapter.getListAdapter()); - } - - public SuggestionsAdapter<ExpandableListAdapter> getSuggestionsAdapter() { - return mSuggestionsAdapter; - } - - public void setLimitSuggestionsToViewHeight(boolean limit) { - // not supported - } - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - setItemsCanFocus(false); - setOnGroupClickListener(new OnGroupClickListener(){ - public boolean onGroupClick( - ExpandableListView parent, View v, int groupPosition, long id) { - // disable collapsing / expanding - return true; - }}); - } - - public void expandAll() { - if (mSuggestionsAdapter != null) { - ExpandableListAdapter adapter = mSuggestionsAdapter.getListAdapter(); - for (int i = 0; i < adapter.getGroupCount(); ++i) { - expandGroup(i); - } - } - } - -} diff --git a/src/com/android/quicksearchbox/ui/ClusteredSuggestionsView.kt b/src/com/android/quicksearchbox/ui/ClusteredSuggestionsView.kt new file mode 100644 index 0000000..b870fa3 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/ClusteredSuggestionsView.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.ExpandableListAdapter +import android.widget.ExpandableListView + +/** Suggestions view that displays suggestions clustered by corpus type. */ +class ClusteredSuggestionsView(context: Context?, attrs: AttributeSet?) : + ExpandableListView(context, attrs), SuggestionsListView<ExpandableListAdapter?> { + + @JvmField var mSuggestionsAdapter: SuggestionsAdapter<ExpandableListAdapter?>? = null + + override fun setSuggestionsAdapter(adapter: SuggestionsAdapter<ExpandableListAdapter?>?) { + mSuggestionsAdapter = adapter + super.setAdapter(adapter?.listAdapter) + } + + override fun getSuggestionsAdapter(): SuggestionsAdapter<ExpandableListAdapter?>? { + return mSuggestionsAdapter + } + + // TODO: this function does not appear to be used currently and remains unimplemented + override fun getSelectedItemId(): Long { + return 0 + } + + @Suppress("UNUSED_PARAMETER") + fun setLimitSuggestionsToViewHeight(limit: Boolean) { + // not supported + } + + @Override + override fun onFinishInflate() { + super.onFinishInflate() + setItemsCanFocus(false) + setOnGroupClickListener( + object : OnGroupClickListener { + override fun onGroupClick( + parent: ExpandableListView?, + v: View?, + groupPosition: Int, + id: Long + ): Boolean { + // disable collapsing / expanding + return true + } + } + ) + } + + fun expandAll() { + if (mSuggestionsAdapter != null) { + val adapter: ExpandableListAdapter? = mSuggestionsAdapter?.listAdapter + for (i in 0 until adapter!!.getGroupCount()) { + expandGroup(i) + } + } + } +} diff --git a/src/com/android/quicksearchbox/ui/ContactBadge.java b/src/com/android/quicksearchbox/ui/ContactBadge.java deleted file mode 100644 index 15b8320..0000000 --- a/src/com/android/quicksearchbox/ui/ContactBadge.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.QuickContactBadge; - -/** - * A {@link QuickContactBadge} that allows setting a click listener. - * The base class may use {@link View#setOnClickListener} internally, - * so this class adds a separate click listener field. - */ -public class ContactBadge extends QuickContactBadge { - - private View.OnClickListener mExtraOnClickListener; - - public ContactBadge(Context context) { - super(context); - } - - public ContactBadge(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public ContactBadge(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public void onClick(View v) { - super.onClick(v); - if (mExtraOnClickListener != null) { - mExtraOnClickListener.onClick(v); - } - } - - public void setExtraOnClickListener(View.OnClickListener extraOnClickListener) { - mExtraOnClickListener = extraOnClickListener; - } - -} diff --git a/src/com/android/quicksearchbox/ui/ContactBadge.kt b/src/com/android/quicksearchbox/ui/ContactBadge.kt new file mode 100644 index 0000000..9b87cc2 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/ContactBadge.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.QuickContactBadge + +/** + * A [QuickContactBadge] that allows setting a click listener. The base class may use + * [View.setOnClickListener] internally, so this class adds a separate click listener field. + */ +class ContactBadge : QuickContactBadge { + private var mExtraOnClickListener: View.OnClickListener? = null + + constructor(context: Context?) : super(context) + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyle: Int + ) : super(context, attrs, defStyle) + + @Override + override fun onClick(v: View?) { + super.onClick(v) + if (mExtraOnClickListener != null) { + mExtraOnClickListener?.onClick(v) + } + } + + fun setExtraOnClickListener(extraOnClickListener: View.OnClickListener?) { + mExtraOnClickListener = extraOnClickListener + } +} diff --git a/src/com/android/quicksearchbox/ui/CorpusView.java b/src/com/android/quicksearchbox/ui/CorpusView.java deleted file mode 100644 index 23982d1..0000000 --- a/src/com/android/quicksearchbox/ui/CorpusView.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox.ui; - -import com.android.quicksearchbox.R; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.ViewDebug; -import android.widget.Checkable; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - - -/** - * A corpus in the corpus selection list. - */ -public class CorpusView extends RelativeLayout implements Checkable { - - private ImageView mIcon; - private TextView mLabel; - private boolean mChecked; - - private static final int[] CHECKED_STATE_SET = { - android.R.attr.state_checked - }; - - public CorpusView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public CorpusView(Context context) { - super(context); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mIcon = (ImageView) findViewById(R.id.source_icon); - mLabel = (TextView) findViewById(R.id.source_label); - } - - public void setLabel(CharSequence label) { - mLabel.setText(label); - } - - public void setIcon(Drawable icon) { - mIcon.setImageDrawable(icon); - } - - @Override - @ViewDebug.ExportedProperty - public boolean isChecked() { - return mChecked; - } - - @Override - public void setChecked(boolean checked) { - if (mChecked != checked) { - mChecked = checked; - refreshDrawableState(); - } - } - - @Override - public void toggle() { - setChecked(!mChecked); - } - - @Override - protected int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - if (isChecked()) { - mergeDrawableStates(drawableState, CHECKED_STATE_SET); - } - return drawableState; - } - -} diff --git a/src/com/android/quicksearchbox/ui/CorpusView.kt b/src/com/android/quicksearchbox/ui/CorpusView.kt new file mode 100644 index 0000000..96eab98 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/CorpusView.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.ViewDebug +import android.widget.Checkable +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import com.android.quicksearchbox.R + +/** A corpus in the corpus selection list. */ +class CorpusView : RelativeLayout, Checkable { + private var mIcon: ImageView? = null + private var mLabel: TextView? = null + private var mChecked = false + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {} + constructor(context: Context?) : super(context) {} + + @Override + protected override fun onFinishInflate() { + super.onFinishInflate() + mIcon = findViewById(R.id.source_icon) as ImageView? + mLabel = findViewById(R.id.source_label) as TextView? + } + + fun setLabel(label: CharSequence?) { + mLabel?.setText(label) + } + + fun setIcon(icon: Drawable?) { + mIcon?.setImageDrawable(icon) + } + + @Override + @ViewDebug.ExportedProperty + override fun isChecked(): Boolean { + return mChecked + } + + @Override + override fun setChecked(checked: Boolean) { + if (mChecked != checked) { + mChecked = checked + refreshDrawableState() + } + } + + @Override + override fun toggle() { + isChecked = !mChecked + } + + @Override + protected override fun onCreateDrawableState(extraSpace: Int): IntArray { + val drawableState: IntArray = super.onCreateDrawableState(extraSpace + 1) + if (isChecked) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET) + } + return drawableState + } + + companion object { + private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked) + } +} diff --git a/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java b/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java deleted file mode 100644 index c946568..0000000 --- a/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox.ui; - -import com.android.quicksearchbox.R; -import com.android.quicksearchbox.Source; -import com.android.quicksearchbox.Suggestion; -import com.android.quicksearchbox.util.Consumer; -import com.android.quicksearchbox.util.NowOrLater; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.text.Html; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -/** - * View for the items in the suggestions list. This includes promoted suggestions, - * sources, and suggestions under each source. - */ -public class DefaultSuggestionView extends BaseSuggestionView { - - private static final boolean DBG = false; - - private static final String VIEW_ID = "default"; - - private final String TAG = "QSB.DefaultSuggestionView"; - - private AsyncIcon mAsyncIcon1; - private AsyncIcon mAsyncIcon2; - - public DefaultSuggestionView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public DefaultSuggestionView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public DefaultSuggestionView(Context context) { - super(context); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mText1 = (TextView) findViewById(R.id.text1); - mText2 = (TextView) findViewById(R.id.text2); - mAsyncIcon1 = new AsyncIcon(mIcon1) { - // override default icon (when no other available) with default source icon - @Override - protected String getFallbackIconId(Source source) { - return source.getSourceIconUri().toString(); - } - @Override - protected Drawable getFallbackIcon(Source source) { - return source.getSourceIcon(); - } - }; - mAsyncIcon2 = new AsyncIcon(mIcon2); - } - - @Override - public void bindAsSuggestion(Suggestion suggestion, String userQuery) { - super.bindAsSuggestion(suggestion, userQuery); - - CharSequence text1 = formatText(suggestion.getSuggestionText1(), suggestion); - CharSequence text2 = suggestion.getSuggestionText2Url(); - if (text2 != null) { - text2 = formatUrl(text2); - } else { - text2 = formatText(suggestion.getSuggestionText2(), suggestion); - } - // If there is no text for the second line, allow the first line to be up to two lines - if (TextUtils.isEmpty(text2)) { - mText1.setSingleLine(false); - mText1.setMaxLines(2); - mText1.setEllipsize(TextUtils.TruncateAt.START); - } else { - mText1.setSingleLine(true); - mText1.setMaxLines(1); - mText1.setEllipsize(TextUtils.TruncateAt.MIDDLE); - } - setText1(text1); - setText2(text2); - mAsyncIcon1.set(suggestion.getSuggestionSource(), suggestion.getSuggestionIcon1()); - mAsyncIcon2.set(suggestion.getSuggestionSource(), suggestion.getSuggestionIcon2()); - - if (DBG) { - Log.d(TAG, "bindAsSuggestion(), text1=" + text1 + ",text2=" + text2 + ",q='" + - userQuery + ",fromHistory=" + isFromHistory(suggestion)); - } - } - - private CharSequence formatUrl(CharSequence url) { - SpannableString text = new SpannableString(url); - ColorStateList colors = getResources().getColorStateList(R.color.url_text); - text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null), - 0, url.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - return text; - } - - private CharSequence formatText(String str, Suggestion suggestion) { - boolean isHtml = "html".equals(suggestion.getSuggestionFormat()); - if (isHtml && looksLikeHtml(str)) { - return Html.fromHtml(str); - } else { - return str; - } - } - - private boolean looksLikeHtml(String str) { - if (TextUtils.isEmpty(str)) return false; - for (int i = str.length() - 1; i >= 0; i--) { - char c = str.charAt(i); - if (c == '>' || c == '&') return true; - } - return false; - } - - /** - * Sets the drawable in an image view, makes sure the view is only visible if there - * is a drawable. - */ - private static void setViewDrawable(ImageView v, Drawable drawable) { - // Set the icon even if the drawable is null, since we need to clear any - // previous icon. - v.setImageDrawable(drawable); - - if (drawable == null) { - v.setVisibility(View.GONE); - } else { - v.setVisibility(View.VISIBLE); - - // This is a hack to get any animated drawables (like a 'working' spinner) - // to animate. You have to setVisible true on an AnimationDrawable to get - // it to start animating, but it must first have been false or else the - // call to setVisible will be ineffective. We need to clear up the story - // about animated drawables in the future, see http://b/1878430. - drawable.setVisible(false, false); - drawable.setVisible(true, false); - } - } - - private class AsyncIcon { - private final ImageView mView; - private String mCurrentId; - private String mWantedId; - - public AsyncIcon(ImageView view) { - mView = view; - } - - public void set(final Source source, final String sourceIconId) { - if (sourceIconId != null) { - // The iconId can just be a package-relative resource ID, which may overlap with - // other packages. Make sure it's globally unique. - Uri iconUri = source.getIconUri(sourceIconId); - final String uniqueIconId = iconUri == null ? null : iconUri.toString(); - mWantedId = uniqueIconId; - if (!TextUtils.equals(mWantedId, mCurrentId)) { - if (DBG) Log.d(TAG, "getting icon Id=" + uniqueIconId); - NowOrLater<Drawable> icon = source.getIcon(sourceIconId); - if (icon.haveNow()) { - if (DBG) Log.d(TAG, "getIcon ready now"); - handleNewDrawable(icon.getNow(), uniqueIconId, source); - } else { - // make sure old icon is not visible while new one is loaded - if (DBG) Log.d(TAG , "getIcon getting later"); - clearDrawable(); - icon.getLater(new Consumer<Drawable>(){ - @Override - public boolean consume(Drawable icon) { - if (DBG) { - Log.d(TAG, "IconConsumer.consume got id " + uniqueIconId + - " want id " + mWantedId); - } - // ensure we have not been re-bound since the request was made. - if (TextUtils.equals(uniqueIconId, mWantedId)) { - handleNewDrawable(icon, uniqueIconId, source); - return true; - } - return false; - }}); - } - } - } else { - mWantedId = null; - handleNewDrawable(null, null, source); - } - } - - private void handleNewDrawable(Drawable icon, String id, Source source) { - if (icon == null) { - mWantedId = getFallbackIconId(source); - if (TextUtils.equals(mWantedId, mCurrentId)) { - return; - } - icon = getFallbackIcon(source); - } - setDrawable(icon, id); - } - - private void setDrawable(Drawable icon, String id) { - mCurrentId = id; - setViewDrawable(mView, icon); - } - - private void clearDrawable() { - mCurrentId = null; - mView.setImageDrawable(null); - } - - protected String getFallbackIconId(Source source) { - return null; - } - - protected Drawable getFallbackIcon(Source source) { - return null; - } - - } - - public static class Factory extends SuggestionViewInflater { - public Factory(Context context) { - super(VIEW_ID, DefaultSuggestionView.class, R.layout.suggestion, context); - } - } - -} diff --git a/src/com/android/quicksearchbox/ui/DefaultSuggestionView.kt b/src/com/android/quicksearchbox/ui/DefaultSuggestionView.kt new file mode 100644 index 0000000..3134d72 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/DefaultSuggestionView.kt @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.net.Uri +import android.text.Html +import android.text.Spannable +import android.text.SpannableString +import android.text.TextUtils +import android.text.style.TextAppearanceSpan +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import com.android.quicksearchbox.R +import com.android.quicksearchbox.Source +import com.android.quicksearchbox.Suggestion +import com.android.quicksearchbox.util.Consumer +import com.android.quicksearchbox.util.NowOrLater + +/** + * View for the items in the suggestions list. This includes promoted suggestions, sources, and + * suggestions under each source. + */ +class DefaultSuggestionView : BaseSuggestionView { + private val TAG = "QSB.DefaultSuggestionView" + private var mAsyncIcon1: DefaultSuggestionView.AsyncIcon? = null + private var mAsyncIcon2: DefaultSuggestionView.AsyncIcon? = null + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyle: Int + ) : super(context, attrs, defStyle) + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?) : super(context) + + @Override + override fun onFinishInflate() { + super.onFinishInflate() + mText1 = findViewById(R.id.text1) as TextView + mText2 = findViewById(R.id.text2) as TextView + mAsyncIcon1 = + object : AsyncIcon(mIcon1) { + // override default icon (when no other available) with default source icon + @Override + override fun getFallbackIconId(source: Source?): String { + return source?.sourceIconUri.toString() + } + + @Override + override fun getFallbackIcon(source: Source?): Drawable? { + return source?.sourceIcon + } + } + mAsyncIcon2 = AsyncIcon(mIcon2) + } + + @Override + override fun bindAsSuggestion(suggestion: Suggestion?, userQuery: String?) { + super.bindAsSuggestion(suggestion, userQuery) + val text1 = formatText(suggestion?.suggestionText1, suggestion) + var text2: CharSequence = suggestion?.suggestionText2Url as CharSequence + text2 = formatUrl(text2) + // If there is no text for the second line, allow the first line to be up to two lines + if (TextUtils.isEmpty(text2)) { + mText1?.setSingleLine(false) + mText1?.setMaxLines(2) + mText1?.setEllipsize(TextUtils.TruncateAt.START) + } else { + mText1?.setSingleLine(true) + mText1?.setMaxLines(1) + mText1?.setEllipsize(TextUtils.TruncateAt.MIDDLE) + } + setText1(text1) + setText2(text2) + mAsyncIcon1?.set(suggestion.suggestionSource, suggestion.suggestionIcon1) + mAsyncIcon2?.set(suggestion.suggestionSource, suggestion.suggestionIcon2) + if (DBG) { + Log.d( + TAG, + "bindAsSuggestion(), text1=" + + text1 + + ",text2=" + + text2 + + ",q='" + + userQuery + + ",fromHistory=" + + isFromHistory(suggestion) + ) + } + } + + private fun formatUrl(url: CharSequence): CharSequence { + val text = SpannableString(url) + val colors: ColorStateList = getResources().getColorStateList(R.color.url_text, null) + text.setSpan( + TextAppearanceSpan(null, 0, 0, colors, null), + 0, + url.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + return text + } + + private fun formatText(str: String?, suggestion: Suggestion?): CharSequence { + val isHtml = "html" == suggestion?.suggestionFormat + return if (isHtml && looksLikeHtml(str)) { + Html.fromHtml(str, Html.FROM_HTML_MODE_LEGACY) + } else { + str as CharSequence + } + } + + private fun looksLikeHtml(str: String?): Boolean { + if (TextUtils.isEmpty(str)) return false + for (i in str!!.length - 1 downTo 0) { + val c: Char = str[i] + if (c == '>' || c == '&') return true + } + return false + } + + private open inner class AsyncIcon(view: ImageView?) { + private val mView: ImageView? + private var mCurrentId: String? = null + private var mWantedId: String? = null + + operator fun set(source: Source?, sourceIconId: String?) { + if (sourceIconId != null) { + // The iconId can just be a package-relative resource ID, which may overlap with + // other packages. Make sure it's globally unique. + val iconUri: Uri? = source?.getIconUri(sourceIconId) + val uniqueIconId: String? = if (iconUri == null) null else iconUri.toString() + mWantedId = uniqueIconId + if (!TextUtils.equals(mWantedId, mCurrentId)) { + if (DBG) Log.d(TAG, "getting icon Id=$uniqueIconId") + val icon: NowOrLater<Drawable?>? = source?.getIcon(sourceIconId) + if (icon!!.haveNow()) { + if (DBG) Log.d(TAG, "getIcon ready now") + handleNewDrawable(icon.now, uniqueIconId, source) + } else { + // make sure old icon is not visible while new one is loaded + if (DBG) Log.d(TAG, "getIcon getting later") + clearDrawable() + icon.getLater( + object : Consumer<Drawable?> { + @Override + override fun consume(value: Drawable?): Boolean { + if (DBG) { + Log.d(TAG, "IconConsumer.consume got id $uniqueIconId want id $mWantedId") + } + // ensure we have not been re-bound since the request was made. + if (TextUtils.equals(uniqueIconId, mWantedId)) { + handleNewDrawable(value, uniqueIconId, source) + return true + } + return false + } + } + ) + } + } + } else { + mWantedId = null + handleNewDrawable(null, null, source) + } + } + + private fun handleNewDrawable(icon: Drawable?, id: String?, source: Source?) { + var mIcon: Drawable? = icon + if (mIcon == null) { + mWantedId = getFallbackIconId(source) + if (TextUtils.equals(mWantedId, mCurrentId)) { + return + } + mIcon = getFallbackIcon(source) + } + setDrawable(mIcon, id) + } + + private fun setDrawable(icon: Drawable?, id: String?) { + mCurrentId = id + setViewDrawable(mView, icon) + } + + private fun clearDrawable() { + mCurrentId = null + mView?.setImageDrawable(null) + } + + protected open fun getFallbackIconId(source: Source?): String? { + return null + } + + protected open fun getFallbackIcon(source: Source?): Drawable? { + return null + } + + init { + mView = view + } + } + + class Factory(context: Context?) : + SuggestionViewInflater( + VIEW_ID, + DefaultSuggestionView::class.java, + R.layout.suggestion, + context + ) + + companion object { + private const val DBG = false + private const val VIEW_ID = "default" + + /** + * Sets the drawable in an image view, makes sure the view is only visible if there is a + * drawable. + */ + private fun setViewDrawable(v: ImageView?, drawable: Drawable?) { + // Set the icon even if the drawable is null, since we need to clear any + // previous icon. + v?.setImageDrawable(drawable) + if (drawable == null) { + v?.setVisibility(View.GONE) + } else { + v?.setVisibility(View.VISIBLE) + + // This is a hack to get any animated drawables (like a 'working' spinner) + // to animate. You have to setVisible true on an AnimationDrawable to get + // it to start animating, but it must first have been false or else the + // call to setVisible will be ineffective. We need to clear up the story + // about animated drawables in the future, see http://b/1878430. + drawable.setVisible(false, false) + drawable.setVisible(true, false) + } + } + } +} diff --git a/src/com/android/quicksearchbox/ui/DefaultSuggestionViewFactory.java b/src/com/android/quicksearchbox/ui/DefaultSuggestionViewFactory.java deleted file mode 100644 index ed4625f..0000000 --- a/src/com/android/quicksearchbox/ui/DefaultSuggestionViewFactory.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; - -import com.android.quicksearchbox.Suggestion; -import com.android.quicksearchbox.SuggestionCursor; - -import java.util.Collection; -import java.util.HashSet; -import java.util.LinkedList; - -/** - * Suggestion view factory for Google suggestions. - */ -public class DefaultSuggestionViewFactory implements SuggestionViewFactory { - - private final LinkedList<SuggestionViewFactory> mFactories - = new LinkedList<SuggestionViewFactory>(); - private final SuggestionViewFactory mDefaultFactory; - private HashSet<String> mViewTypes; - - public DefaultSuggestionViewFactory(Context context) { - mDefaultFactory = new DefaultSuggestionView.Factory(context); - addFactory(new WebSearchSuggestionView.Factory(context)); - } - - /** - * Must only be called from the constructor - */ - protected final void addFactory(SuggestionViewFactory factory) { - mFactories.addFirst(factory); - } - - @Override - public Collection<String> getSuggestionViewTypes() { - if (mViewTypes == null) { - mViewTypes = new HashSet<String>(); - mViewTypes.addAll(mDefaultFactory.getSuggestionViewTypes()); - for (SuggestionViewFactory factory : mFactories) { - mViewTypes.addAll(factory.getSuggestionViewTypes()); - } - } - return mViewTypes; - } - - @Override - public View getView(SuggestionCursor suggestion, String userQuery, - View convertView, ViewGroup parent) { - for (SuggestionViewFactory factory : mFactories) { - if (factory.canCreateView(suggestion)) { - return factory.getView(suggestion, userQuery, convertView, parent); - } - } - return mDefaultFactory.getView(suggestion, userQuery, convertView, parent); - } - - @Override - public String getViewType(Suggestion suggestion) { - for (SuggestionViewFactory factory : mFactories) { - if (factory.canCreateView(suggestion)) { - return factory.getViewType(suggestion); - } - } - return mDefaultFactory.getViewType(suggestion); - } - - @Override - public boolean canCreateView(Suggestion suggestion) { - return true; - } - -} diff --git a/src/com/android/quicksearchbox/ui/DefaultSuggestionViewFactory.kt b/src/com/android/quicksearchbox/ui/DefaultSuggestionViewFactory.kt new file mode 100644 index 0000000..5559f13 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/DefaultSuggestionViewFactory.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import com.android.quicksearchbox.Suggestion +import com.android.quicksearchbox.SuggestionCursor +import java.util.LinkedList + +/** Suggestion view factory for Google suggestions. */ +class DefaultSuggestionViewFactory(context: Context?) : SuggestionViewFactory { + private val mFactories: LinkedList<SuggestionViewFactory> = LinkedList<SuggestionViewFactory>() + private val mDefaultFactory: SuggestionViewFactory + private var mViewTypes: HashSet<String>? = null + + /** Must only be called from the constructor */ + protected fun addFactory(factory: SuggestionViewFactory?) { + mFactories.addFirst(factory) + } + + @get:Override + override val suggestionViewTypes: Collection<String> + get() { + if (mViewTypes == null) { + mViewTypes = hashSetOf() + mViewTypes?.addAll(mDefaultFactory.suggestionViewTypes) + for (factory in mFactories) { + mViewTypes?.addAll(factory.suggestionViewTypes) + } + } + return mViewTypes as Collection<String> + } + + @Override + override fun getView( + suggestion: SuggestionCursor?, + userQuery: String?, + convertView: View?, + parent: ViewGroup? + ): View? { + for (factory in mFactories) { + if (factory.canCreateView(suggestion)) { + return factory.getView(suggestion, userQuery, convertView, parent) + } + } + return mDefaultFactory.getView(suggestion, userQuery, convertView, parent) + } + + @Override + override fun getViewType(suggestion: Suggestion?): String { + for (factory in mFactories) { + if (factory.canCreateView(suggestion)) { + return factory.getViewType(suggestion)!! + } + } + return mDefaultFactory.getViewType(suggestion)!! + } + + @Override + override fun canCreateView(suggestion: Suggestion?): Boolean { + return true + } + + init { + mDefaultFactory = DefaultSuggestionView.Factory(context) + addFactory(WebSearchSuggestionView.Factory(context)) + } +} diff --git a/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.java b/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.java deleted file mode 100644 index 6b7d47e..0000000 --- a/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox.ui; - -import com.android.quicksearchbox.SuggestionCursor; -import com.android.quicksearchbox.SuggestionPosition; -import com.android.quicksearchbox.Suggestions; - -import android.database.DataSetObserver; -import android.util.Log; -import android.view.View.OnFocusChangeListener; - -/** - * A {@link SuggestionsListAdapter} that doesn't expose the new suggestions - * until there are some results to show. - */ -public class DelayingSuggestionsAdapter<A> implements SuggestionsAdapter<A> { - - private static final boolean DBG = false; - private static final String TAG = "QSB.DelayingSuggestionsAdapter"; - - private DataSetObserver mPendingDataSetObserver; - - private Suggestions mPendingSuggestions; - - private final SuggestionsAdapterBase<A> mDelayedAdapter; - - public DelayingSuggestionsAdapter(SuggestionsAdapterBase<A> delayed) { - mDelayedAdapter = delayed; - } - - public void close() { - setPendingSuggestions(null); - mDelayedAdapter.close(); - } - - @Override - public void setSuggestions(Suggestions suggestions) { - if (suggestions == null) { - mDelayedAdapter.setSuggestions(null); - setPendingSuggestions(null); - return; - } - if (shouldPublish(suggestions)) { - if (DBG) Log.d(TAG, "Publishing suggestions immediately: " + suggestions); - mDelayedAdapter.setSuggestions(suggestions); - // Clear any old pending suggestions. - setPendingSuggestions(null); - } else { - if (DBG) Log.d(TAG, "Delaying suggestions publishing: " + suggestions); - setPendingSuggestions(suggestions); - } - } - - /** - * Gets whether the given suggestions are non-empty for the selected source. - */ - private boolean shouldPublish(Suggestions suggestions) { - if (suggestions.isDone()) return true; - SuggestionCursor cursor = suggestions.getResult(); - if (cursor != null && cursor.getCount() > 0) { - return true; - } - return false; - } - - private void setPendingSuggestions(Suggestions suggestions) { - if (mPendingSuggestions == suggestions) { - return; - } - if (mDelayedAdapter.isClosed()) { - if (suggestions != null) { - suggestions.release(); - } - return; - } - if (mPendingDataSetObserver == null) { - mPendingDataSetObserver = new PendingSuggestionsObserver(); - } - if (mPendingSuggestions != null) { - mPendingSuggestions.unregisterDataSetObserver(mPendingDataSetObserver); - // Close old suggestions, but only if they are not also the current - // suggestions. - if (mPendingSuggestions != getSuggestions()) { - mPendingSuggestions.release(); - } - } - mPendingSuggestions = suggestions; - if (mPendingSuggestions != null) { - mPendingSuggestions.registerDataSetObserver(mPendingDataSetObserver); - } - } - - protected void onPendingSuggestionsChanged() { - if (DBG) { - Log.d(TAG, "onPendingSuggestionsChanged(), mPendingSuggestions=" - + mPendingSuggestions); - } - if (shouldPublish(mPendingSuggestions)) { - if (DBG) Log.d(TAG, "Suggestions now available, publishing: " + mPendingSuggestions); - mDelayedAdapter.setSuggestions(mPendingSuggestions); - // The suggestions are no longer pending. - setPendingSuggestions(null); - } - } - - private class PendingSuggestionsObserver extends DataSetObserver { - @Override - public void onChanged() { - onPendingSuggestionsChanged(); - } - } - - @Override - public A getListAdapter() { - return mDelayedAdapter.getListAdapter(); - } - - public SuggestionCursor getCurrentPromotedSuggestions() { - return mDelayedAdapter.getCurrentSuggestions(); - } - - @Override - public Suggestions getSuggestions() { - return mDelayedAdapter.getSuggestions(); - } - - @Override - public SuggestionPosition getSuggestion(long suggestionId) { - return mDelayedAdapter.getSuggestion(suggestionId); - } - - @Override - public void onSuggestionClicked(long suggestionId) { - mDelayedAdapter.onSuggestionClicked(suggestionId); - } - - @Override - public void onSuggestionQueryRefineClicked(long suggestionId) { - mDelayedAdapter.onSuggestionQueryRefineClicked(suggestionId); - } - - @Override - public void setOnFocusChangeListener(OnFocusChangeListener l) { - mDelayedAdapter.setOnFocusChangeListener(l); - } - - @Override - public void setSuggestionClickListener(SuggestionClickListener listener) { - mDelayedAdapter.setSuggestionClickListener(listener); - } - - @Override - public boolean isEmpty() { - return mDelayedAdapter.isEmpty(); - } - -} diff --git a/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.kt b/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.kt new file mode 100644 index 0000000..a65a5da --- /dev/null +++ b/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.database.DataSetObserver +import android.util.Log +import android.view.View.OnFocusChangeListener +import com.android.quicksearchbox.SuggestionCursor +import com.android.quicksearchbox.SuggestionPosition +import com.android.quicksearchbox.Suggestions + +/** + * A [SuggestionsListAdapter] that doesn't expose the new suggestions until there are some results + * to show. + */ +class DelayingSuggestionsAdapter<A>(private val mDelayedAdapter: SuggestionsAdapterBase<A>) : + SuggestionsAdapter<A> { + private var mPendingDataSetObserver: DataSetObserver? = null + private var mPendingSuggestions: Suggestions? = null + fun close() { + setPendingSuggestions(null) + mDelayedAdapter.close() + } + + /** Gets whether the given suggestions are non-empty for the selected source. */ + private fun shouldPublish(suggestions: Suggestions?): Boolean { + if (suggestions!!.isDone) return true + val cursor: SuggestionCursor? = suggestions.getResult() + return cursor != null && cursor.count > 0 + } + + private fun setPendingSuggestions(suggestions: Suggestions?) { + if (mPendingSuggestions === suggestions) { + return + } + if (mDelayedAdapter.isClosed) { + suggestions?.release() + return + } + if (mPendingDataSetObserver == null) { + mPendingDataSetObserver = PendingSuggestionsObserver() + } + if (mPendingSuggestions != null) { + mPendingSuggestions!!.unregisterDataSetObserver(mPendingDataSetObserver) + // Close old suggestions, but only if they are not also the current + // suggestions. + if (mPendingSuggestions !== this.suggestions) { + mPendingSuggestions!!.release() + } + } + mPendingSuggestions = suggestions + if (mPendingSuggestions != null) { + mPendingSuggestions!!.registerDataSetObserver(mPendingDataSetObserver) + } + } + + protected fun onPendingSuggestionsChanged() { + if (DBG) Log.d(TAG, "onPendingSuggestionsChanged(), mPendingSuggestions=" + mPendingSuggestions) + if (shouldPublish(mPendingSuggestions)) { + if (DBG) Log.d(TAG, "Suggestions now available, publishing: $mPendingSuggestions") + mDelayedAdapter.suggestions = mPendingSuggestions + // The suggestions are no longer pending. + setPendingSuggestions(null) + } + } + + private inner class PendingSuggestionsObserver : DataSetObserver() { + @Override + override fun onChanged() { + onPendingSuggestionsChanged() + } + } + + @get:Override + override val listAdapter: A + get() = mDelayedAdapter.listAdapter + val currentPromotedSuggestions: SuggestionCursor? + get() = mDelayedAdapter.currentSuggestions + + // Clear any old pending suggestions. + @get:Override + @set:Override + override var suggestions: Suggestions? + get() = mDelayedAdapter.suggestions + set(suggestions) { + if (suggestions == null) { + mDelayedAdapter.suggestions = null + setPendingSuggestions(null) + return + } + if (shouldPublish(suggestions)) { + if (DBG) Log.d(TAG, "Publishing suggestions immediately: $suggestions") + mDelayedAdapter.suggestions = suggestions + // Clear any old pending suggestions. + setPendingSuggestions(null) + } else { + if (DBG) Log.d(TAG, "Delaying suggestions publishing: $suggestions") + setPendingSuggestions(suggestions) + } + } + + @Override + override fun getSuggestion(suggestionId: Long): SuggestionPosition? { + return mDelayedAdapter.getSuggestion(suggestionId) + } + + @Override + override fun onSuggestionClicked(suggestionId: Long) { + mDelayedAdapter.onSuggestionClicked(suggestionId) + } + + @Override + override fun onSuggestionQueryRefineClicked(suggestionId: Long) { + mDelayedAdapter.onSuggestionQueryRefineClicked(suggestionId) + } + + @Override + override fun setOnFocusChangeListener(l: OnFocusChangeListener?) { + mDelayedAdapter.setOnFocusChangeListener(l) + } + + @Override + override fun setSuggestionClickListener(listener: SuggestionClickListener?) { + mDelayedAdapter.setSuggestionClickListener(listener) + } + + @get:Override + override val isEmpty: Boolean + get() = mDelayedAdapter.isEmpty + + companion object { + private const val DBG = false + private const val TAG = "QSB.DelayingSuggestionsAdapter" + } +} diff --git a/src/com/android/quicksearchbox/ui/QueryTextView.java b/src/com/android/quicksearchbox/ui/QueryTextView.java deleted file mode 100644 index 2531204..0000000 --- a/src/com/android/quicksearchbox/ui/QueryTextView.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import android.content.Context; -import android.util.AttributeSet; -import android.util.Log; -import android.view.inputmethod.CompletionInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; - -/** - * The query text field. - */ -public class QueryTextView extends EditText { - - private static final boolean DBG = false; - private static final String TAG = "QSB.QueryTextView"; - - private CommitCompletionListener mCommitCompletionListener; - - public QueryTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public QueryTextView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public QueryTextView(Context context) { - super(context); - } - - /** - * Sets the text selection in the query text view. - * - * @param selectAll If {@code true}, selects the entire query. - * If {@false}, no characters are selected, and the cursor is placed - * at the end of the query. - */ - public void setTextSelection(boolean selectAll) { - if (selectAll) { - selectAll(); - } else { - setSelection(length()); - } - } - - protected void replaceText(CharSequence text) { - clearComposingText(); - setText(text); - setTextSelection(false); - } - - public void setCommitCompletionListener(CommitCompletionListener listener) { - mCommitCompletionListener = listener; - } - - private InputMethodManager getInputMethodManager() { - return (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - } - - public void showInputMethod() { - InputMethodManager imm = getInputMethodManager(); - if (imm != null) { - imm.showSoftInput(this, 0); - } - } - - public void hideInputMethod() { - InputMethodManager imm = getInputMethodManager(); - if (imm != null) { - imm.hideSoftInputFromWindow(getWindowToken(), 0); - } - } - - @Override - public void onCommitCompletion(CompletionInfo completion) { - if (DBG) Log.d(TAG, "onCommitCompletion(" + completion + ")"); - hideInputMethod(); - replaceText(completion.getText()); - if (mCommitCompletionListener != null) { - mCommitCompletionListener.onCommitCompletion(completion.getPosition()); - } - } - - public interface CommitCompletionListener { - void onCommitCompletion(int position); - } - -} diff --git a/src/com/android/quicksearchbox/ui/QueryTextView.kt b/src/com/android/quicksearchbox/ui/QueryTextView.kt new file mode 100644 index 0000000..e4b82d2 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/QueryTextView.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.inputmethod.CompletionInfo +import android.view.inputmethod.InputMethodManager +import android.widget.EditText + +/** The query text field. */ +class QueryTextView : EditText { + private var mCommitCompletionListener: CommitCompletionListener? = null + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyle: Int + ) : super(context, attrs, defStyle) + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?) : super(context) + + /** + * Sets the text selection in the query text view. + * + * @param selectAll If `true`, selects the entire query. If {@false}, no characters are selected, + * and the cursor is placed at the end of the query. + */ + fun setTextSelection(selectAll: Boolean) { + if (selectAll) { + selectAll() + } else { + setSelection(length()) + } + } + + protected fun replaceText(text: CharSequence?) { + clearComposingText() + setText(text) + setTextSelection(false) + } + + fun setCommitCompletionListener(listener: CommitCompletionListener?) { + mCommitCompletionListener = listener + } + + private val inputMethodManager: InputMethodManager? + get() = getContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + + fun showInputMethod() { + val imm: InputMethodManager? = inputMethodManager + if (imm != null) { + imm.showSoftInput(this, 0) + } + } + + fun hideInputMethod() { + val imm: InputMethodManager? = inputMethodManager + if (imm != null) { + imm.hideSoftInputFromWindow(getWindowToken(), 0) + } + } + + @Override + override fun onCommitCompletion(completion: CompletionInfo) { + if (DBG) Log.d(TAG, "onCommitCompletion($completion)") + hideInputMethod() + replaceText(completion.getText()) + if (mCommitCompletionListener != null) { + mCommitCompletionListener?.onCommitCompletion(completion.getPosition()) + } + } + + interface CommitCompletionListener { + fun onCommitCompletion(position: Int) + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.QueryTextView" + } +} diff --git a/src/com/android/quicksearchbox/ui/SearchActivityView.java b/src/com/android/quicksearchbox/ui/SearchActivityView.java deleted file mode 100644 index 6060e4f..0000000 --- a/src/com/android/quicksearchbox/ui/SearchActivityView.java +++ /dev/null @@ -1,563 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import android.content.Context; -import android.database.DataSetObserver; -import android.graphics.drawable.Drawable; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.AttributeSet; -import android.util.Log; -import android.view.KeyEvent; -import android.view.View; -import android.view.inputmethod.CompletionInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.AbsListView; -import android.widget.ImageButton; -import android.widget.ListAdapter; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.TextView.OnEditorActionListener; - -import com.android.quicksearchbox.Logger; -import com.android.quicksearchbox.QsbApplication; -import com.android.quicksearchbox.R; -import com.android.quicksearchbox.SearchActivity; -import com.android.quicksearchbox.SourceResult; -import com.android.quicksearchbox.SuggestionCursor; -import com.android.quicksearchbox.Suggestions; -import com.android.quicksearchbox.VoiceSearch; - -import java.util.ArrayList; -import java.util.Arrays; - -public abstract class SearchActivityView extends RelativeLayout { - protected static final boolean DBG = false; - protected static final String TAG = "QSB.SearchActivityView"; - - // The string used for privateImeOptions to identify to the IME that it should not show - // a microphone button since one already exists in the search dialog. - // TODO: This should move to android-common or something. - private static final String IME_OPTION_NO_MICROPHONE = "nm"; - - protected QueryTextView mQueryTextView; - // True if the query was empty on the previous call to updateQuery() - protected boolean mQueryWasEmpty = true; - protected Drawable mQueryTextEmptyBg; - protected Drawable mQueryTextNotEmptyBg; - - protected SuggestionsListView<ListAdapter> mSuggestionsView; - protected SuggestionsAdapter<ListAdapter> mSuggestionsAdapter; - - protected ImageButton mSearchGoButton; - protected ImageButton mVoiceSearchButton; - - protected ButtonsKeyListener mButtonsKeyListener; - - private boolean mUpdateSuggestions; - - private QueryListener mQueryListener; - private SearchClickListener mSearchClickListener; - protected View.OnClickListener mExitClickListener; - - public SearchActivityView(Context context) { - super(context); - } - - public SearchActivityView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public SearchActivityView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected void onFinishInflate() { - mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text); - - mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); - mSuggestionsView.setOnScrollListener(new InputMethodCloser()); - mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener()); - mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener()); - - mSuggestionsAdapter = createSuggestionsAdapter(); - // TODO: why do we need focus listeners both on the SuggestionsView and the individual - // suggestions? - mSuggestionsAdapter.setOnFocusChangeListener(new SuggestListFocusListener()); - - mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); - mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); - mVoiceSearchButton.setImageDrawable(getVoiceSearchIcon()); - - mQueryTextView.addTextChangedListener(new SearchTextWatcher()); - mQueryTextView.setOnEditorActionListener(new QueryTextEditorActionListener()); - mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); - mQueryTextEmptyBg = mQueryTextView.getBackground(); - - mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); - - mButtonsKeyListener = new ButtonsKeyListener(); - mSearchGoButton.setOnKeyListener(mButtonsKeyListener); - mVoiceSearchButton.setOnKeyListener(mButtonsKeyListener); - - mUpdateSuggestions = true; - } - - public abstract void onResume(); - - public abstract void onStop(); - - public void onPause() { - // Override if necessary - } - - public void start() { - mSuggestionsAdapter.getListAdapter().registerDataSetObserver(new SuggestionsObserver()); - mSuggestionsView.setSuggestionsAdapter(mSuggestionsAdapter); - } - - public void destroy() { - mSuggestionsView.setSuggestionsAdapter(null); // closes mSuggestionsAdapter - } - - // TODO: Get rid of this. To make it more easily testable, - // the SearchActivityView should not depend on QsbApplication. - protected QsbApplication getQsbApplication() { - return QsbApplication.get(getContext()); - } - - protected Drawable getVoiceSearchIcon() { - return getResources().getDrawable(R.drawable.ic_btn_speak_now); - } - - protected VoiceSearch getVoiceSearch() { - return getQsbApplication().getVoiceSearch(); - } - - protected SuggestionsAdapter<ListAdapter> createSuggestionsAdapter() { - return new DelayingSuggestionsAdapter<ListAdapter>(new SuggestionsListAdapter( - getQsbApplication().getSuggestionViewFactory())); - } - - public void setMaxPromotedResults(int maxPromoted) { - } - - public void limitResultsToViewHeight() { - } - - public void setQueryListener(QueryListener listener) { - mQueryListener = listener; - } - - public void setSearchClickListener(SearchClickListener listener) { - mSearchClickListener = listener; - } - - public void setVoiceSearchButtonClickListener(View.OnClickListener listener) { - if (mVoiceSearchButton != null) { - mVoiceSearchButton.setOnClickListener(listener); - } - } - - public void setSuggestionClickListener(final SuggestionClickListener listener) { - mSuggestionsAdapter.setSuggestionClickListener(listener); - mQueryTextView.setCommitCompletionListener(new QueryTextView.CommitCompletionListener() { - @Override - public void onCommitCompletion(int position) { - mSuggestionsAdapter.onSuggestionClicked(position); - } - }); - } - - public void setExitClickListener(final View.OnClickListener listener) { - mExitClickListener = listener; - } - - public Suggestions getSuggestions() { - return mSuggestionsAdapter.getSuggestions(); - } - - public SuggestionCursor getCurrentSuggestions() { - return mSuggestionsAdapter.getSuggestions().getResult(); - } - - public void setSuggestions(Suggestions suggestions) { - suggestions.acquire(); - mSuggestionsAdapter.setSuggestions(suggestions); - } - - public void clearSuggestions() { - mSuggestionsAdapter.setSuggestions(null); - } - - public String getQuery() { - CharSequence q = mQueryTextView.getText(); - return q == null ? "" : q.toString(); - } - - public boolean isQueryEmpty() { - return TextUtils.isEmpty(getQuery()); - } - - /** - * Sets the text in the query box. Does not update the suggestions. - */ - public void setQuery(String query, boolean selectAll) { - mUpdateSuggestions = false; - mQueryTextView.setText(query); - mQueryTextView.setTextSelection(selectAll); - mUpdateSuggestions = true; - } - - protected SearchActivity getActivity() { - Context context = getContext(); - if (context instanceof SearchActivity) { - return (SearchActivity) context; - } else { - return null; - } - } - - public void hideSuggestions() { - mSuggestionsView.setVisibility(GONE); - } - - public void showSuggestions() { - mSuggestionsView.setVisibility(VISIBLE); - } - - public void focusQueryTextView() { - mQueryTextView.requestFocus(); - } - - protected void updateUi() { - updateUi(isQueryEmpty()); - } - - protected void updateUi(boolean queryEmpty) { - updateQueryTextView(queryEmpty); - updateSearchGoButton(queryEmpty); - updateVoiceSearchButton(queryEmpty); - } - - protected void updateQueryTextView(boolean queryEmpty) { - if (queryEmpty) { - mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg); - mQueryTextView.setHint(null); - } else { - mQueryTextView.setBackgroundResource(R.drawable.textfield_search); - } - } - - private void updateSearchGoButton(boolean queryEmpty) { - if (queryEmpty) { - mSearchGoButton.setVisibility(View.GONE); - } else { - mSearchGoButton.setVisibility(View.VISIBLE); - } - } - - protected void updateVoiceSearchButton(boolean queryEmpty) { - if (shouldShowVoiceSearch(queryEmpty) - && getVoiceSearch().shouldShowVoiceSearch()) { - mVoiceSearchButton.setVisibility(View.VISIBLE); - mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); - } else { - mVoiceSearchButton.setVisibility(View.GONE); - mQueryTextView.setPrivateImeOptions(null); - } - } - - protected boolean shouldShowVoiceSearch(boolean queryEmpty) { - return queryEmpty; - } - - /** - * Hides the input method. - */ - protected void hideInputMethod() { - InputMethodManager imm = (InputMethodManager) - getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.hideSoftInputFromWindow(getWindowToken(), 0); - } - } - - public abstract void considerHidingInputMethod(); - - public void showInputMethodForQuery() { - mQueryTextView.showInputMethod(); - } - - /** - * Dismiss the activity if BACK is pressed when the search box is empty. - */ - @Override - public boolean dispatchKeyEventPreIme(KeyEvent event) { - SearchActivity activity = getActivity(); - if (activity != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK - && isQueryEmpty()) { - KeyEvent.DispatcherState state = getKeyDispatcherState(); - if (state != null) { - if (event.getAction() == KeyEvent.ACTION_DOWN - && event.getRepeatCount() == 0) { - state.startTracking(event, this); - return true; - } else if (event.getAction() == KeyEvent.ACTION_UP - && !event.isCanceled() && state.isTracking(event)) { - hideInputMethod(); - activity.onBackPressed(); - return true; - } - } - } - return super.dispatchKeyEventPreIme(event); - } - - /** - * If the input method is in fullscreen mode, and the selector corpus - * is All or Web, use the web search suggestions as completions. - */ - protected void updateInputMethodSuggestions() { - InputMethodManager imm = (InputMethodManager) - getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm == null || !imm.isFullscreenMode()) return; - Suggestions suggestions = mSuggestionsAdapter.getSuggestions(); - if (suggestions == null) return; - CompletionInfo[] completions = webSuggestionsToCompletions(suggestions); - if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")"); - imm.displayCompletions(mQueryTextView, completions); - } - - private CompletionInfo[] webSuggestionsToCompletions(Suggestions suggestions) { - SourceResult cursor = suggestions.getWebResult(); - if (cursor == null) return null; - int count = cursor.getCount(); - ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count); - for (int i = 0; i < count; i++) { - cursor.moveTo(i); - String text1 = cursor.getSuggestionText1(); - completions.add(new CompletionInfo(i, i, text1)); - } - return completions.toArray(new CompletionInfo[completions.size()]); - } - - protected void onSuggestionsChanged() { - updateInputMethodSuggestions(); - } - - protected boolean onSuggestionKeyDown(SuggestionsAdapter<?> adapter, - long suggestionId, int keyCode, KeyEvent event) { - // Treat enter or search as a click - if ( keyCode == KeyEvent.KEYCODE_ENTER - || keyCode == KeyEvent.KEYCODE_SEARCH - || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { - if (adapter != null) { - adapter.onSuggestionClicked(suggestionId); - return true; - } else { - return false; - } - } - - return false; - } - - protected boolean onSearchClicked(int method) { - if (mSearchClickListener != null) { - return mSearchClickListener.onSearchClicked(method); - } - return false; - } - - /** - * Filters the suggestions list when the search text changes. - */ - private class SearchTextWatcher implements TextWatcher { - @Override - public void afterTextChanged(Editable s) { - boolean empty = s.length() == 0; - if (empty != mQueryWasEmpty) { - mQueryWasEmpty = empty; - updateUi(empty); - } - if (mUpdateSuggestions) { - if (mQueryListener != null) { - mQueryListener.onQueryChanged(); - } - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - } - - /** - * Handles key events on the suggestions list view. - */ - protected class SuggestionsViewKeyListener implements View.OnKeyListener { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_DOWN - && v instanceof SuggestionsListView<?>) { - SuggestionsListView<?> listView = (SuggestionsListView<?>) v; - if (onSuggestionKeyDown(listView.getSuggestionsAdapter(), - listView.getSelectedItemId(), keyCode, event)) { - return true; - } - } - return forwardKeyToQueryTextView(keyCode, event); - } - } - - private class InputMethodCloser implements SuggestionsView.OnScrollListener { - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, - int totalItemCount) { - } - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - considerHidingInputMethod(); - } - } - - /** - * Listens for clicks on the source selector. - */ - private class SearchGoButtonClickListener implements View.OnClickListener { - @Override - public void onClick(View view) { - onSearchClicked(Logger.SEARCH_METHOD_BUTTON); - } - } - - /** - * This class handles enter key presses in the query text view. - */ - private class QueryTextEditorActionListener implements OnEditorActionListener { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - boolean consumed = false; - if (event != null) { - if (event.getAction() == KeyEvent.ACTION_UP) { - consumed = onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); - } else if (event.getAction() == KeyEvent.ACTION_DOWN) { - // we have to consume the down event so that we receive the up event too - consumed = true; - } - } - if (DBG) Log.d(TAG, "onEditorAction consumed=" + consumed); - return consumed; - } - } - - /** - * Handles key events on the search and voice search buttons, - * by refocusing to EditText. - */ - private class ButtonsKeyListener implements View.OnKeyListener { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - return forwardKeyToQueryTextView(keyCode, event); - } - } - - private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { - if (!event.isSystem() && shouldForwardToQueryTextView(keyCode)) { - if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); - if (mQueryTextView.requestFocus()) { - return mQueryTextView.dispatchKeyEvent(event); - } - } - return false; - } - - private boolean shouldForwardToQueryTextView(int keyCode) { - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_ENTER: - case KeyEvent.KEYCODE_SEARCH: - return false; - default: - return true; - } - } - - /** - * Hides the input method when the suggestions get focus. - */ - private class SuggestListFocusListener implements OnFocusChangeListener { - @Override - public void onFocusChange(View v, boolean focused) { - if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); - if (focused) { - considerHidingInputMethod(); - } - } - } - - private class QueryTextViewFocusListener implements OnFocusChangeListener { - @Override - public void onFocusChange(View v, boolean focused) { - if (DBG) Log.d(TAG, "Query focus change, now: " + focused); - if (focused) { - // The query box got focus, show the input method - showInputMethodForQuery(); - } - } - } - - protected class SuggestionsObserver extends DataSetObserver { - @Override - public void onChanged() { - onSuggestionsChanged(); - } - } - - public interface QueryListener { - void onQueryChanged(); - } - - public interface SearchClickListener { - boolean onSearchClicked(int method); - } - - private class CloseClickListener implements OnClickListener { - @Override - public void onClick(View v) { - if (!isQueryEmpty()) { - mQueryTextView.setText(""); - } else { - mExitClickListener.onClick(v); - } - } - } -} diff --git a/src/com/android/quicksearchbox/ui/SearchActivityView.kt b/src/com/android/quicksearchbox/ui/SearchActivityView.kt new file mode 100644 index 0000000..8e8dcac --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SearchActivityView.kt @@ -0,0 +1,509 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.database.DataSetObserver +import android.graphics.drawable.Drawable +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.util.AttributeSet +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.CompletionInfo +import android.view.inputmethod.InputMethodManager +import android.widget.AbsListView +import android.widget.ImageButton +import android.widget.ListAdapter +import android.widget.RelativeLayout +import android.widget.TextView +import android.widget.TextView.OnEditorActionListener +import com.android.quicksearchbox.* +import com.android.quicksearchbox.R +import java.util.Arrays +import kotlin.collections.ArrayList + +abstract class SearchActivityView : RelativeLayout { + @JvmField protected var mQueryTextView: QueryTextView? = null + + // True if the query was empty on the previous call to updateQuery() + @JvmField protected var mQueryWasEmpty = true + @JvmField protected var mQueryTextEmptyBg: Drawable? = null + protected var mQueryTextNotEmptyBg: Drawable? = null + @JvmField protected var mSuggestionsView: SuggestionsListView<ListAdapter?>? = null + @JvmField protected var mSuggestionsAdapter: SuggestionsAdapter<ListAdapter?>? = null + @JvmField protected var mSearchGoButton: ImageButton? = null + @JvmField protected var mVoiceSearchButton: ImageButton? = null + @JvmField protected var mButtonsKeyListener: ButtonsKeyListener? = null + private var mUpdateSuggestions = false + private var mQueryListener: QueryListener? = null + private var mSearchClickListener: SearchClickListener? = null + @JvmField protected var mExitClickListener: View.OnClickListener? = null + + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context?, + attrs: AttributeSet?, + defStyle: Int + ) : super(context, attrs, defStyle) + + @Override + protected override fun onFinishInflate() { + mQueryTextView = findViewById(R.id.search_src_text) as QueryTextView? + mSuggestionsView = findViewById(R.id.suggestions) as SuggestionsView? + mSuggestionsView!!.setOnScrollListener(InputMethodCloser() as AbsListView.OnScrollListener?) + mSuggestionsView!!.setOnKeyListener(SuggestionsViewKeyListener()) + mSuggestionsView!!.setOnFocusChangeListener(SuggestListFocusListener()) + mSuggestionsAdapter = createSuggestionsAdapter() + // TODO: why do we need focus listeners both on the SuggestionsView and the individual + // suggestions? + mSuggestionsAdapter!!.setOnFocusChangeListener(SuggestListFocusListener()) + mSearchGoButton = findViewById(R.id.search_go_btn) as ImageButton? + mVoiceSearchButton = findViewById(R.id.search_voice_btn) as ImageButton? + mVoiceSearchButton?.setImageDrawable(voiceSearchIcon) + mQueryTextView?.addTextChangedListener(SearchTextWatcher()) + mQueryTextView?.setOnEditorActionListener(QueryTextEditorActionListener()) + mQueryTextView?.setOnFocusChangeListener(QueryTextViewFocusListener()) + mQueryTextEmptyBg = mQueryTextView?.getBackground() + mSearchGoButton?.setOnClickListener(SearchGoButtonClickListener()) + mButtonsKeyListener = ButtonsKeyListener() + mSearchGoButton?.setOnKeyListener(mButtonsKeyListener) + mVoiceSearchButton?.setOnKeyListener(mButtonsKeyListener) + mUpdateSuggestions = true + } + + abstract fun onResume() + abstract fun onStop() + fun onPause() { + // Override if necessary + } + + fun start() { + mSuggestionsAdapter?.listAdapter?.registerDataSetObserver(SuggestionsObserver()) + mSuggestionsView!!.setSuggestionsAdapter(mSuggestionsAdapter) + } + + fun destroy() { + mSuggestionsView!!.setSuggestionsAdapter(null) // closes mSuggestionsAdapter + } + + // TODO: Get rid of this. To make it more easily testable, + // the SearchActivityView should not depend on QsbApplication. + protected val qsbApplication: QsbApplication + get() = QsbApplication[getContext()] + protected val voiceSearchIcon: Drawable + get() = getResources().getDrawable(R.drawable.ic_btn_speak_now, null) + protected val voiceSearch: VoiceSearch? + get() = qsbApplication.voiceSearch + + protected fun createSuggestionsAdapter(): SuggestionsAdapter<ListAdapter?> { + return DelayingSuggestionsAdapter(SuggestionsListAdapter(qsbApplication.suggestionViewFactory)) + } + + @Suppress("UNUSED_PARAMETER") fun setMaxPromotedResults(maxPromoted: Int) {} + + fun limitResultsToViewHeight() {} + + fun setQueryListener(listener: QueryListener?) { + mQueryListener = listener + } + + fun setSearchClickListener(listener: SearchClickListener?) { + mSearchClickListener = listener + } + + fun setVoiceSearchButtonClickListener(listener: View.OnClickListener?) { + if (mVoiceSearchButton != null) { + mVoiceSearchButton?.setOnClickListener(listener) + } + } + + fun setSuggestionClickListener(listener: SuggestionClickListener?) { + mSuggestionsAdapter!!.setSuggestionClickListener(listener) + mQueryTextView!!.setCommitCompletionListener( + object : QueryTextView.CommitCompletionListener { + @Override + override fun onCommitCompletion(position: Int) { + mSuggestionsAdapter!!.onSuggestionClicked(position.toLong()) + } + } + ) + } + + fun setExitClickListener(listener: View.OnClickListener?) { + mExitClickListener = listener + } + + var suggestions: Suggestions? + get() = mSuggestionsAdapter?.suggestions + set(suggestions) { + suggestions?.acquire() + mSuggestionsAdapter?.suggestions = suggestions + } + val currentSuggestions: SuggestionCursor + get() = mSuggestionsAdapter?.suggestions?.getResult() as SuggestionCursor + + fun clearSuggestions() { + mSuggestionsAdapter?.suggestions = null + } + + val query: String + get() { + val q: CharSequence? = mQueryTextView?.getText() + return q.toString() + } + val isQueryEmpty: Boolean + get() = TextUtils.isEmpty(query) + + /** Sets the text in the query box. Does not update the suggestions. */ + fun setQuery(query: String?, selectAll: Boolean) { + mUpdateSuggestions = false + mQueryTextView?.setText(query) + mQueryTextView!!.setTextSelection(selectAll) + mUpdateSuggestions = true + } + + protected val activity: SearchActivity? + get() { + val context: Context = getContext() + return if (context is SearchActivity) { + context + } else { + null + } + } + + fun hideSuggestions() { + mSuggestionsView!!.setVisibility(GONE) + } + + fun showSuggestions() { + mSuggestionsView!!.setVisibility(VISIBLE) + } + + fun focusQueryTextView() { + mQueryTextView?.requestFocus() + } + + protected fun updateUi(queryEmpty: Boolean = isQueryEmpty) { + updateQueryTextView(queryEmpty) + updateSearchGoButton(queryEmpty) + updateVoiceSearchButton(queryEmpty) + } + + protected fun updateQueryTextView(queryEmpty: Boolean) { + if (queryEmpty) { + mQueryTextView?.setBackground(mQueryTextEmptyBg) + mQueryTextView?.setHint(null) + } else { + mQueryTextView?.setBackgroundResource(R.drawable.textfield_search) + } + } + + private fun updateSearchGoButton(queryEmpty: Boolean) { + if (queryEmpty) { + mSearchGoButton?.setVisibility(View.GONE) + } else { + mSearchGoButton?.setVisibility(View.VISIBLE) + } + } + + protected fun updateVoiceSearchButton(queryEmpty: Boolean) { + if (shouldShowVoiceSearch(queryEmpty) && voiceSearch!!.shouldShowVoiceSearch()) { + mVoiceSearchButton?.setVisibility(View.VISIBLE) + mQueryTextView?.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE) + } else { + mVoiceSearchButton?.setVisibility(View.GONE) + mQueryTextView?.setPrivateImeOptions(null) + } + } + + protected fun shouldShowVoiceSearch(queryEmpty: Boolean): Boolean { + return queryEmpty + } + + /** Hides the input method. */ + protected fun hideInputMethod() { + val imm: InputMethodManager? = + getContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + if (imm != null) { + imm.hideSoftInputFromWindow(getWindowToken(), 0) + } + } + + abstract fun considerHidingInputMethod() + fun showInputMethodForQuery() { + mQueryTextView!!.showInputMethod() + } + + /** Dismiss the activity if BACK is pressed when the search box is empty. */ + @Suppress("Deprecation") + @Override + override fun dispatchKeyEventPreIme(event: KeyEvent): Boolean { + val activity = activity + if (activity != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK && isQueryEmpty) { + val state: KeyEvent.DispatcherState? = getKeyDispatcherState() + if (state != null) { + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + state.startTracking(event, this) + return true + } else if ( + event.getAction() == KeyEvent.ACTION_UP && !event.isCanceled() && state.isTracking(event) + ) { + hideInputMethod() + activity.onBackPressed() + return true + } + } + } + return super.dispatchKeyEventPreIme(event) + } + + /** + * If the input method is in fullscreen mode, and the selector corpus is All or Web, use the web + * search suggestions as completions. + */ + protected fun updateInputMethodSuggestions() { + val imm: InputMethodManager? = + getContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + if (imm == null || !imm.isFullscreenMode()) return + val suggestions: Suggestions = mSuggestionsAdapter?.suggestions ?: return + val completions: Array<CompletionInfo>? = webSuggestionsToCompletions(suggestions) + if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions).toString() + ")") + imm.displayCompletions(mQueryTextView, completions) + } + + private fun webSuggestionsToCompletions(suggestions: Suggestions): Array<CompletionInfo>? { + val cursor = suggestions.getWebResult() ?: return null + val count: Int = cursor.count + val completions: ArrayList<CompletionInfo> = ArrayList<CompletionInfo>(count) + for (i in 0 until count) { + cursor.moveTo(i) + val text1: String? = cursor.suggestionText1 + completions.add(CompletionInfo(i.toLong(), i, text1)) + } + return completions.toArray(arrayOfNulls<CompletionInfo>(completions.size)) + } + + protected fun onSuggestionsChanged() { + updateInputMethodSuggestions() + } + + @Suppress("UNUSED_PARAMETER") + protected fun onSuggestionKeyDown( + adapter: SuggestionsAdapter<*>?, + suggestionId: Long, + keyCode: Int, + event: KeyEvent? + ): Boolean { + // Treat enter or search as a click + return if ( + keyCode == KeyEvent.KEYCODE_ENTER || + keyCode == KeyEvent.KEYCODE_SEARCH || + keyCode == KeyEvent.KEYCODE_DPAD_CENTER + ) { + if (adapter != null) { + adapter.onSuggestionClicked(suggestionId) + true + } else { + false + } + } else false + } + + protected fun onSearchClicked(method: Int): Boolean { + return if (mSearchClickListener != null) { + mSearchClickListener!!.onSearchClicked(method) + } else false + } + + /** Filters the suggestions list when the search text changes. */ + private inner class SearchTextWatcher : TextWatcher { + @Override + override fun afterTextChanged(s: Editable) { + val empty = s.length == 0 + if (empty != mQueryWasEmpty) { + mQueryWasEmpty = empty + updateUi(empty) + } + if (mUpdateSuggestions) { + if (mQueryListener != null) { + mQueryListener!!.onQueryChanged() + } + } + } + + @Override + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + @Override override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + } + + /** Handles key events on the suggestions list view. */ + protected inner class SuggestionsViewKeyListener : View.OnKeyListener { + @Override + override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean { + if (event.getAction() == KeyEvent.ACTION_DOWN && v is SuggestionsListView<*>) { + val listView = v as SuggestionsListView<*> + if ( + onSuggestionKeyDown( + listView.getSuggestionsAdapter(), + listView.getSelectedItemId(), + keyCode, + event + ) + ) { + return true + } + } + return forwardKeyToQueryTextView(keyCode, event) + } + } + + private inner class InputMethodCloser : AbsListView.OnScrollListener { + @Override + override fun onScroll( + view: AbsListView?, + firstVisibleItem: Int, + visibleItemCount: Int, + totalItemCount: Int + ) {} + + @Override + override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) { + considerHidingInputMethod() + } + } + + /** Listens for clicks on the source selector. */ + private inner class SearchGoButtonClickListener : View.OnClickListener { + @Override + override fun onClick(view: View?) { + onSearchClicked(Logger.SEARCH_METHOD_BUTTON) + } + } + + /** This class handles enter key presses in the query text view. */ + private inner class QueryTextEditorActionListener : OnEditorActionListener { + @Override + override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { + var consumed = false + if (event != null) { + if (event.getAction() == KeyEvent.ACTION_UP) { + consumed = onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD) + } else if (event.getAction() == KeyEvent.ACTION_DOWN) { + // we have to consume the down event so that we receive the up event too + consumed = true + } + } + if (DBG) Log.d(TAG, "onEditorAction consumed=$consumed") + return consumed + } + } + + /** Handles key events on the search and voice search buttons, by refocusing to EditText. */ + protected inner class ButtonsKeyListener : View.OnKeyListener { + @Override + override fun onKey(v: View?, keyCode: Int, event: KeyEvent): Boolean { + return forwardKeyToQueryTextView(keyCode, event) + } + } + + private fun forwardKeyToQueryTextView(keyCode: Int, event: KeyEvent): Boolean { + if (!event.isSystem() && shouldForwardToQueryTextView(keyCode)) { + if (DBG) Log.d(TAG, "Forwarding key to query box: $event") + if (mQueryTextView!!.requestFocus()) { + return mQueryTextView!!.dispatchKeyEvent(event) + } + } + return false + } + + private fun shouldForwardToQueryTextView(keyCode: Int): Boolean { + return when (keyCode) { + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_SEARCH -> false + else -> true + } + } + + /** Hides the input method when the suggestions get focus. */ + private inner class SuggestListFocusListener : OnFocusChangeListener { + @Override + override fun onFocusChange(v: View?, focused: Boolean) { + if (DBG) Log.d(TAG, "Suggestions focus change, now: $focused") + if (focused) { + considerHidingInputMethod() + } + } + } + + private inner class QueryTextViewFocusListener : OnFocusChangeListener { + @Override + override fun onFocusChange(v: View?, focused: Boolean) { + if (DBG) Log.d(TAG, "Query focus change, now: $focused") + if (focused) { + // The query box got focus, show the input method + showInputMethodForQuery() + } + } + } + + protected inner class SuggestionsObserver : DataSetObserver() { + @Override + override fun onChanged() { + onSuggestionsChanged() + } + } + + interface QueryListener { + fun onQueryChanged() + } + + interface SearchClickListener { + fun onSearchClicked(method: Int): Boolean + } + + private inner class CloseClickListener : OnClickListener { + @Override + override fun onClick(v: View?) { + if (!isQueryEmpty) { + mQueryTextView?.setText("") + } else { + mExitClickListener?.onClick(v) + } + } + } + + companion object { + protected const val DBG = false + protected const val TAG = "QSB.SearchActivityView" + + // The string used for privateImeOptions to identify to the IME that it should not show + // a microphone button since one already exists in the search dialog. + // TODO: This should move to android-common or something. + private const val IME_OPTION_NO_MICROPHONE = "nm" + } +} diff --git a/src/com/android/quicksearchbox/ui/SearchActivityViewSinglePane.java b/src/com/android/quicksearchbox/ui/SearchActivityViewSinglePane.java deleted file mode 100644 index 9288fb6..0000000 --- a/src/com/android/quicksearchbox/ui/SearchActivityViewSinglePane.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import com.android.quicksearchbox.R; -import com.android.quicksearchbox.Source; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ImageButton; - -/** - * Finishes the containing activity on BACK, even if input method is showing. - */ -public class SearchActivityViewSinglePane extends SearchActivityView { - - public SearchActivityViewSinglePane(Context context) { - super(context); - } - - public SearchActivityViewSinglePane(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public SearchActivityViewSinglePane(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public void onResume() { - focusQueryTextView(); - } - - @Override - public void considerHidingInputMethod() { - mQueryTextView.hideInputMethod(); - } - - @Override - public void onStop() { - } - -} diff --git a/src/com/android/quicksearchbox/ui/SearchActivityViewSinglePane.kt b/src/com/android/quicksearchbox/ui/SearchActivityViewSinglePane.kt new file mode 100644 index 0000000..b4485e7 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SearchActivityViewSinglePane.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.util.AttributeSet + +/** Finishes the containing activity on BACK, even if input method is showing. */ +class SearchActivityViewSinglePane : SearchActivityView { + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context?, + attrs: AttributeSet?, + defStyle: Int + ) : super(context, attrs, defStyle) + + @Override + override fun onResume() { + focusQueryTextView() + } + + @Override + override fun considerHidingInputMethod() { + mQueryTextView!!.hideInputMethod() + } + + @Override override fun onStop() {} +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionClickListener.java b/src/com/android/quicksearchbox/ui/SuggestionClickListener.java deleted file mode 100644 index da062cd..0000000 --- a/src/com/android/quicksearchbox/ui/SuggestionClickListener.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox.ui; - -/** - * Listener interface for clicks on suggestions. - */ -public interface SuggestionClickListener { - - /** - * Called when a suggestion is clicked. - * - * @param adapter Adapter that contains the clicked suggestion. - * @param suggestionId The ID of the suggestion clicked. If the suggestion list is flat, this - * will be the position within the list. - */ - void onSuggestionClicked(SuggestionsAdapter<?> adapter, long suggestionId); - - /** - * Called when the "query refine" button of a suggestion is clicked. - * - * @param adapter Adapter that contains the clicked suggestion. - * @param suggestionId The ID of the suggestion clicked. If the suggestion list is flat, this - * will be the position within the list. - */ - void onSuggestionQueryRefineClicked(SuggestionsAdapter<?> adapter, long suggestionId); -} diff --git a/src/com/android/quicksearchbox/ui/SuggestionClickListener.kt b/src/com/android/quicksearchbox/ui/SuggestionClickListener.kt new file mode 100644 index 0000000..ae00ec1 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionClickListener.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +/** Listener interface for clicks on suggestions. */ +interface SuggestionClickListener { + /** + * Called when a suggestion is clicked. + * + * @param adapter Adapter that contains the clicked suggestion. + * @param suggestionId The ID of the suggestion clicked. If the suggestion list is flat, this will + * be the position within the list. + */ + fun onSuggestionClicked(adapter: SuggestionsAdapter<*>?, suggestionId: Long) + + /** + * Called when the "query refine" button of a suggestion is clicked. + * + * @param adapter Adapter that contains the clicked suggestion. + * @param suggestionId The ID of the suggestion clicked. If the suggestion list is flat, this will + * be the position within the list. + */ + fun onSuggestionQueryRefineClicked(adapter: SuggestionsAdapter<*>?, suggestionId: Long) +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionView.java b/src/com/android/quicksearchbox/ui/SuggestionView.java deleted file mode 100644 index 636ecd5..0000000 --- a/src/com/android/quicksearchbox/ui/SuggestionView.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import com.android.quicksearchbox.Suggestion; - -/** - * Interface to be implemented by any view appearing in the list of suggestions. - */ -public interface SuggestionView { - /** - * Set the view's contents based on the given suggestion. - */ - void bindAsSuggestion(Suggestion suggestion, String userQuery); - - /** - * Binds this view to a list adapter. - * - * @param adapter The adapter of the list which the view is appearing in - * @param position The position of this view with the list. - */ - void bindAdapter(SuggestionsAdapter<?> adapter, long position); - -} diff --git a/src/com/android/quicksearchbox/ui/SuggestionView.kt b/src/com/android/quicksearchbox/ui/SuggestionView.kt new file mode 100644 index 0000000..7c4364e --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionView.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import com.android.quicksearchbox.Suggestion + +/** Interface to be implemented by any view appearing in the list of suggestions. */ +interface SuggestionView { + /** Set the view's contents based on the given suggestion. */ + fun bindAsSuggestion(suggestion: Suggestion?, userQuery: String?) + + /** + * Binds this view to a list adapter. + * + * @param adapter The adapter of the list which the view is appearing in + * @param position The position of this view with the list. + */ + fun bindAdapter(adapter: SuggestionsAdapter<*>?, position: Long) +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionViewFactory.java b/src/com/android/quicksearchbox/ui/SuggestionViewFactory.java deleted file mode 100644 index 27cb596..0000000 --- a/src/com/android/quicksearchbox/ui/SuggestionViewFactory.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import com.android.quicksearchbox.Suggestion; -import com.android.quicksearchbox.SuggestionCursor; - -import android.view.View; -import android.view.ViewGroup; - -import java.util.Collection; - -/** - * Factory interface for suggestion views. - */ -public interface SuggestionViewFactory { - - /** - * Returns all the view types that are used by this factory. Each view type corresponds to a - * specific layout that is used to display suggestions. The returned set must have at least one - * item in it. - * - * View types must be unique across all suggestion view factories. - */ - Collection<String> getSuggestionViewTypes(); - - /** - * Returns the view type to be used for displaying the given suggestion. This MUST correspond to - * one of the view types returned by {@link #getSuggestionViewTypes()}. - */ - String getViewType(Suggestion suggestion); - - /** - * Gets a view corresponding to the current suggestion in the given cursor. - * - * @param convertView The old view to reuse, if possible. Note: You should check that this view - * is non-null and of an appropriate type before using. If it is not possible to convert - * this view to display the correct data, this method can create a new view. - * @param parent The parent that this view will eventually be attached to - * @return A View corresponding to the data within this suggestion. - */ - View getView(SuggestionCursor suggestion, String userQuery, View convertView, ViewGroup parent); - - /** - * Checks whether this factory can create views for the given suggestion. - */ - boolean canCreateView(Suggestion suggestion); - -} diff --git a/src/com/android/quicksearchbox/ui/SuggestionViewFactory.kt b/src/com/android/quicksearchbox/ui/SuggestionViewFactory.kt new file mode 100644 index 0000000..a33886c --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionViewFactory.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.view.View +import android.view.ViewGroup +import com.android.quicksearchbox.Suggestion +import com.android.quicksearchbox.SuggestionCursor + +/** Factory interface for suggestion views. */ +interface SuggestionViewFactory { + /** + * Returns all the view types that are used by this factory. Each view type corresponds to a + * specific layout that is used to display suggestions. The returned set must have at least one + * item in it. + * + * View types must be unique across all suggestion view factories. + */ + val suggestionViewTypes: Collection<String> + + /** + * Returns the view type to be used for displaying the given suggestion. This MUST correspond to + * one of the view types returned by [.getSuggestionViewTypes]. + */ + fun getViewType(suggestion: Suggestion?): String? + + /** + * Gets a view corresponding to the current suggestion in the given cursor. + * + * @param convertView The old view to reuse, if possible. Note: You should check that this view is + * non-null and of an appropriate type before using. If it is not possible to convert this view to + * display the correct data, this method can create a new view. + * @param parent The parent that this view will eventually be attached to + * @return A View corresponding to the data within this suggestion. + */ + fun getView( + suggestion: SuggestionCursor?, + userQuery: String?, + convertView: View?, + parent: ViewGroup? + ): View? + + /** Checks whether this factory can create views for the given suggestion. */ + fun canCreateView(suggestion: Suggestion?): Boolean +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionViewInflater.java b/src/com/android/quicksearchbox/ui/SuggestionViewInflater.java deleted file mode 100644 index 9275b6e..0000000 --- a/src/com/android/quicksearchbox/ui/SuggestionViewInflater.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import com.android.quicksearchbox.Suggestion; -import com.android.quicksearchbox.SuggestionCursor; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import java.util.Collection; -import java.util.Collections; - -/** - * Suggestion view factory that inflates views from XML. - */ -public class SuggestionViewInflater implements SuggestionViewFactory { - - private final String mViewType; - private final Class<?> mViewClass; - private final int mLayoutId; - private final Context mContext; - - /** - * @param viewType The unique type of views inflated by this factory - * @param viewClass The expected type of view classes. - * @param layoutId resource ID of layout to use. - * @param context Context to use for inflating the views. - */ - public SuggestionViewInflater(String viewType, Class<? extends SuggestionView> viewClass, - int layoutId, Context context) { - mViewType = viewType; - mViewClass = viewClass; - mLayoutId = layoutId; - mContext = context; - } - - protected LayoutInflater getInflater() { - return (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - public Collection<String> getSuggestionViewTypes() { - return Collections.singletonList(mViewType); - } - - public View getView(SuggestionCursor suggestion, String userQuery, - View convertView, ViewGroup parent) { - if (convertView == null || !convertView.getClass().equals(mViewClass)) { - int layoutId = mLayoutId; - convertView = getInflater().inflate(layoutId, parent, false); - } - if (!(convertView instanceof SuggestionView)) { - throw new IllegalArgumentException("Not a SuggestionView: " + convertView); - } - ((SuggestionView) convertView).bindAsSuggestion(suggestion, userQuery); - return convertView; - } - - public String getViewType(Suggestion suggestion) { - return mViewType; - } - - public boolean canCreateView(Suggestion suggestion) { - return true; - } - -} diff --git a/src/com/android/quicksearchbox/ui/SuggestionViewInflater.kt b/src/com/android/quicksearchbox/ui/SuggestionViewInflater.kt new file mode 100644 index 0000000..acfa592 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionViewInflater.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.android.quicksearchbox.Suggestion +import com.android.quicksearchbox.SuggestionCursor + +/** Suggestion view factory that inflates views from XML. */ +open class SuggestionViewInflater( + private val mViewType: String, + viewClass: Class<out SuggestionView?>, + layoutId: Int, + context: Context? +) : SuggestionViewFactory { + private val mViewClass: Class<*> + private val mLayoutId: Int + private val mContext: Context? + + protected val inflater: LayoutInflater + get() = mContext?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + + override val suggestionViewTypes: Collection<String> + get() = listOf(mViewType) + + override fun getView( + suggestion: SuggestionCursor?, + userQuery: String?, + convertView: View?, + parent: ViewGroup? + ): View? { + var mConvertView: View? = convertView + if (mConvertView == null || !mConvertView::class.equals(mViewClass)) { + val layoutId = mLayoutId + mConvertView = inflater.inflate(layoutId, parent, false) + } + if (mConvertView !is SuggestionView) { + throw IllegalArgumentException("Not a SuggestionView: $mConvertView") + } + (mConvertView as SuggestionView).bindAsSuggestion(suggestion, userQuery) + return mConvertView + } + + override fun getViewType(suggestion: Suggestion?): String { + return mViewType + } + + override fun canCreateView(suggestion: Suggestion?): Boolean { + return true + } + + /** + * @param viewType The unique type of views inflated by this factory + * @param viewClass The expected type of view classes. + * @param layoutId resource ID of layout to use. + * @param context Context to use for inflating the views. + */ + init { + mViewClass = viewClass + mLayoutId = layoutId + mContext = context + } +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java b/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java deleted file mode 100644 index 825ae0d..0000000 --- a/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import com.android.quicksearchbox.SuggestionCursor; -import com.android.quicksearchbox.SuggestionPosition; -import com.android.quicksearchbox.Suggestions; - -import android.view.View.OnFocusChangeListener; -import android.widget.ExpandableListAdapter; -import android.widget.ListAdapter; - -/** - * Interface for suggestions adapters. - * - * @param <A> the adapter class used by the UI, probably either {@link ListAdapter} or - * {@link ExpandableListAdapter}. - */ -public interface SuggestionsAdapter<A> { - - /** - * Sets the listener to be notified of clicks on suggestions. - */ - void setSuggestionClickListener(SuggestionClickListener listener); - - /** - * Sets the listener to be notified of focus change events on suggestion views. - */ - void setOnFocusChangeListener(OnFocusChangeListener l); - - /** - * Sets the current suggestions. - */ - void setSuggestions(Suggestions suggestions); - - /** - * Indicates if there's any suggestions in this adapter. - */ - boolean isEmpty(); - - /** - * Gets the current suggestions. - */ - Suggestions getSuggestions(); - - /** - * Gets the cursor and position corresponding to the given suggestion ID. - * @param suggestionId Suggestion ID. - */ - SuggestionPosition getSuggestion(long suggestionId); - - /** - * Handles a regular click on a suggestion. - * - * @param suggestionId The ID of the suggestion clicked. If the suggestion list is flat, this - * will be the position within the list. - */ - void onSuggestionClicked(long suggestionId); - - /** - * Handles a click on the query refinement button. - * - * @param suggestionId The ID of the suggestion clicked. If the suggestion list is flat, this - * will be the position within the list. - */ - void onSuggestionQueryRefineClicked(long suggestionId); - - /** - * Gets the adapter to be used by the UI view. - */ - A getListAdapter(); - -} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsAdapter.kt b/src/com/android/quicksearchbox/ui/SuggestionsAdapter.kt new file mode 100644 index 0000000..687fd42 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionsAdapter.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.view.View.OnFocusChangeListener +import android.widget.ExpandableListAdapter +import android.widget.ListAdapter +import com.android.quicksearchbox.SuggestionPosition +import com.android.quicksearchbox.Suggestions + +/** + * Interface for suggestions adapters. + * + * @param <A> the adapter class used by the UI, probably either [ListAdapter] or + * [ExpandableListAdapter]. + */ +interface SuggestionsAdapter<A> { + /** Sets the listener to be notified of clicks on suggestions. */ + fun setSuggestionClickListener(listener: SuggestionClickListener?) + + /** Sets the listener to be notified of focus change events on suggestion views. */ + fun setOnFocusChangeListener(l: OnFocusChangeListener?) + + /** Indicates if there's any suggestions in this adapter. */ + val isEmpty: Boolean + /** Gets the current suggestions. */ + /** Sets the current suggestions. */ + var suggestions: Suggestions? + + /** + * Gets the cursor and position corresponding to the given suggestion ID. + * @param suggestionId Suggestion ID. + */ + fun getSuggestion(suggestionId: Long): SuggestionPosition? + + /** + * Handles a regular click on a suggestion. + * + * @param suggestionId The ID of the suggestion clicked. If the suggestion list is flat, this will + * be the position within the list. + */ + fun onSuggestionClicked(suggestionId: Long) + + /** + * Handles a click on the query refinement button. + * + * @param suggestionId The ID of the suggestion clicked. If the suggestion list is flat, this will + * be the position within the list. + */ + fun onSuggestionQueryRefineClicked(suggestionId: Long) + + /** Gets the adapter to be used by the UI view. */ + val listAdapter: A +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsAdapterBase.java b/src/com/android/quicksearchbox/ui/SuggestionsAdapterBase.java deleted file mode 100644 index 244e3f9..0000000 --- a/src/com/android/quicksearchbox/ui/SuggestionsAdapterBase.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import com.android.quicksearchbox.Suggestion; -import com.android.quicksearchbox.SuggestionCursor; -import com.android.quicksearchbox.SuggestionPosition; -import com.android.quicksearchbox.Suggestions; - -import android.database.DataSetObserver; -import android.util.Log; -import android.view.View; -import android.view.View.OnFocusChangeListener; -import android.view.ViewGroup; - -import java.util.HashMap; - -/** - * Base class for suggestions adapters. The templated class A is the list adapter class. - */ -public abstract class SuggestionsAdapterBase<A> implements SuggestionsAdapter<A> { - - private static final boolean DBG = false; - private static final String TAG = "QSB.SuggestionsAdapter"; - - private DataSetObserver mDataSetObserver; - - private SuggestionCursor mCurrentSuggestions; - private final HashMap<String, Integer> mViewTypeMap; - private final SuggestionViewFactory mViewFactory; - - private Suggestions mSuggestions; - - private SuggestionClickListener mSuggestionClickListener; - private OnFocusChangeListener mOnFocusChangeListener; - - private boolean mClosed = false; - - protected SuggestionsAdapterBase(SuggestionViewFactory viewFactory) { - mViewFactory = viewFactory; - mViewTypeMap = new HashMap<String, Integer>(); - for (String viewType : mViewFactory.getSuggestionViewTypes()) { - if (!mViewTypeMap.containsKey(viewType)) { - mViewTypeMap.put(viewType, mViewTypeMap.size()); - } - } - } - - @Override - public abstract boolean isEmpty(); - - public boolean isClosed() { - return mClosed; - } - - public void close() { - setSuggestions(null); - mClosed = true; - } - - @Override - public void setSuggestionClickListener(SuggestionClickListener listener) { - mSuggestionClickListener = listener; - } - - @Override - public void setOnFocusChangeListener(OnFocusChangeListener l) { - mOnFocusChangeListener = l; - } - - @Override - public void setSuggestions(Suggestions suggestions) { - if (mSuggestions == suggestions) { - return; - } - if (mClosed) { - if (suggestions != null) { - suggestions.release(); - } - return; - } - if (mDataSetObserver == null) { - mDataSetObserver = new MySuggestionsObserver(); - } - // TODO: delay the change if there are no suggestions for the currently visible tab. - if (mSuggestions != null) { - mSuggestions.unregisterDataSetObserver(mDataSetObserver); - mSuggestions.release(); - } - mSuggestions = suggestions; - if (mSuggestions != null) { - mSuggestions.registerDataSetObserver(mDataSetObserver); - } - onSuggestionsChanged(); - } - - @Override - public Suggestions getSuggestions() { - return mSuggestions; - } - - @Override - public abstract SuggestionPosition getSuggestion(long suggestionId); - - protected int getCount() { - return mCurrentSuggestions == null ? 0 : mCurrentSuggestions.getCount(); - } - - protected SuggestionPosition getSuggestion(int position) { - if (mCurrentSuggestions == null) return null; - return new SuggestionPosition(mCurrentSuggestions, position); - } - - protected int getViewTypeCount() { - return mViewTypeMap.size(); - } - - private String suggestionViewType(Suggestion suggestion) { - String viewType = mViewFactory.getViewType(suggestion); - if (!mViewTypeMap.containsKey(viewType)) { - throw new IllegalStateException("Unknown viewType " + viewType); - } - return viewType; - } - - protected int getSuggestionViewType(SuggestionCursor cursor, int position) { - if (cursor == null) { - return 0; - } - cursor.moveTo(position); - return mViewTypeMap.get(suggestionViewType(cursor)); - } - - protected int getSuggestionViewTypeCount() { - return mViewTypeMap.size(); - } - - protected View getView(SuggestionCursor suggestions, int position, long suggestionId, - View convertView, ViewGroup parent) { - suggestions.moveTo(position); - View v = mViewFactory.getView(suggestions, suggestions.getUserQuery(), convertView, parent); - if (v instanceof SuggestionView) { - ((SuggestionView) v).bindAdapter(this, suggestionId); - } else { - SuggestionViewClickListener l = new SuggestionViewClickListener(suggestionId); - v.setOnClickListener(l); - } - - if (mOnFocusChangeListener != null) { - v.setOnFocusChangeListener(mOnFocusChangeListener); - } - return v; - } - - protected void onSuggestionsChanged() { - if (DBG) Log.d(TAG, "onSuggestionsChanged(" + mSuggestions + ")"); - SuggestionCursor cursor = null; - if (mSuggestions != null) { - cursor = mSuggestions.getResult(); - } - changeSuggestions(cursor); - } - - public SuggestionCursor getCurrentSuggestions() { - return mCurrentSuggestions; - } - - /** - * Replace the cursor. - * - * This does not close the old cursor. Instead, all the cursors are closed in - * {@link #setSuggestions(Suggestions)}. - */ - private void changeSuggestions(SuggestionCursor newCursor) { - if (DBG) { - Log.d(TAG, "changeCursor(" + newCursor + ") count=" + - (newCursor == null ? 0 : newCursor.getCount())); - } - if (newCursor == mCurrentSuggestions) { - if (newCursor != null) { - // Shortcuts may have changed without the cursor changing. - notifyDataSetChanged(); - } - return; - } - mCurrentSuggestions = newCursor; - if (mCurrentSuggestions != null) { - notifyDataSetChanged(); - } else { - notifyDataSetInvalidated(); - } - } - - @Override - public void onSuggestionClicked(long suggestionId) { - if (mClosed) { - Log.w(TAG, "onSuggestionClicked after close"); - } else if (mSuggestionClickListener != null) { - mSuggestionClickListener.onSuggestionClicked(this, suggestionId); - } - } - - @Override - public void onSuggestionQueryRefineClicked(long suggestionId) { - if (mClosed) { - Log.w(TAG, "onSuggestionQueryRefineClicked after close"); - } else if (mSuggestionClickListener != null) { - mSuggestionClickListener.onSuggestionQueryRefineClicked(this, suggestionId); - } - } - - @Override - public abstract A getListAdapter(); - - protected abstract void notifyDataSetInvalidated(); - - protected abstract void notifyDataSetChanged(); - - private class MySuggestionsObserver extends DataSetObserver { - @Override - public void onChanged() { - onSuggestionsChanged(); - } - } - - private class SuggestionViewClickListener implements View.OnClickListener { - private final long mSuggestionId; - public SuggestionViewClickListener(long suggestionId) { - mSuggestionId = suggestionId; - } - @Override - public void onClick(View v) { - onSuggestionClicked(mSuggestionId); - } - } - -} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsAdapterBase.kt b/src/com/android/quicksearchbox/ui/SuggestionsAdapterBase.kt new file mode 100644 index 0000000..25218a5 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionsAdapterBase.kt @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.database.DataSetObserver +import android.util.Log +import android.view.View +import android.view.View.OnFocusChangeListener +import android.view.ViewGroup +import com.android.quicksearchbox.Suggestion +import com.android.quicksearchbox.SuggestionCursor +import com.android.quicksearchbox.SuggestionPosition +import com.android.quicksearchbox.Suggestions +import kotlin.collections.HashMap + +/** Base class for suggestions adapters. The templated class A is the list adapter class. */ +abstract class SuggestionsAdapterBase<A> +protected constructor(private val mViewFactory: SuggestionViewFactory) : SuggestionsAdapter<A> { + private var mDataSetObserver: DataSetObserver? = null + var currentSuggestions: SuggestionCursor? = null + private set + private val mViewTypeMap: HashMap<String, Int> + private var mSuggestions: Suggestions? = null + private var mSuggestionClickListener: SuggestionClickListener? = null + private var mOnFocusChangeListener: OnFocusChangeListener? = null + var isClosed = false + private set + + @get:Override abstract override val isEmpty: Boolean + fun close() { + suggestions = null + isClosed = true + } + + @Override + override fun setSuggestionClickListener(listener: SuggestionClickListener?) { + mSuggestionClickListener = listener + } + + @Override + override fun setOnFocusChangeListener(l: OnFocusChangeListener?) { + mOnFocusChangeListener = l + } + + // TODO: delay the change if there are no suggestions for the currently visible tab. + @get:Override + @set:Override + override var suggestions: Suggestions? + get() = mSuggestions!! + set(suggestions) { + if (mSuggestions === suggestions) { + return + } + if (isClosed) { + suggestions?.release() + return + } + if (mDataSetObserver == null) { + mDataSetObserver = MySuggestionsObserver() + } + // TODO: delay the change if there are no suggestions for the currently visible tab. + if (mSuggestions != null) { + mSuggestions!!.unregisterDataSetObserver(mDataSetObserver) + mSuggestions!!.release() + } + mSuggestions = suggestions + if (mSuggestions != null) { + mSuggestions!!.registerDataSetObserver(mDataSetObserver) + } + onSuggestionsChanged() + } + + @Override abstract override fun getSuggestion(suggestionId: Long): SuggestionPosition + protected val count: Int + get() = if (currentSuggestions == null) 0 else currentSuggestions!!.count + + protected fun getSuggestion(position: Int): SuggestionPosition? { + return if (currentSuggestions == null) null + else SuggestionPosition(currentSuggestions!!, position) + } + + protected val viewTypeCount: Int + get() = mViewTypeMap.size + + private fun suggestionViewType(suggestion: Suggestion): String? { + val viewType = mViewFactory.getViewType(suggestion) + if (!mViewTypeMap.containsKey(viewType)) { + throw IllegalStateException("Unknown viewType $viewType") + } + return viewType + } + + protected fun getSuggestionViewType(cursor: SuggestionCursor?, position: Int): Int { + if (cursor == null) { + return 0 + } + cursor.moveTo(position) + return mViewTypeMap.get(suggestionViewType(cursor)!!) as Int + } + + protected val suggestionViewTypeCount: Int + get() = mViewTypeMap.size + + protected fun getView( + suggestions: SuggestionCursor?, + position: Int, + suggestionId: Long, + convertView: View?, + parent: ViewGroup? + ): View? { + suggestions?.moveTo(position) + val v: View? = mViewFactory.getView(suggestions, suggestions?.userQuery, convertView, parent) + if (v is SuggestionView) { + (v as SuggestionView?)!!.bindAdapter(this, suggestionId) + } else { + val l = SuggestionViewClickListener(suggestionId) + v?.setOnClickListener(l) + } + if (mOnFocusChangeListener != null) { + v?.setOnFocusChangeListener(mOnFocusChangeListener) + } + return v + } + + protected fun onSuggestionsChanged() { + if (DBG) Log.d(TAG, "onSuggestionsChanged($mSuggestions)") + var cursor: SuggestionCursor? = null + if (mSuggestions != null) { + cursor = mSuggestions!!.getResult() + } + changeSuggestions(cursor) + } + + /** + * Replace the cursor. + * + * This does not close the old cursor. Instead, all the cursors are closed in [.setSuggestions]. + */ + private fun changeSuggestions(newCursor: SuggestionCursor?) { + if (DBG) { + Log.d(TAG, "changeCursor(" + newCursor + ") count=" + (newCursor?.count ?: 0)) + } + if (newCursor === currentSuggestions) { + if (newCursor != null) { + // Shortcuts may have changed without the cursor changing. + notifyDataSetChanged() + } + return + } + currentSuggestions = newCursor + if (currentSuggestions != null) { + notifyDataSetChanged() + } else { + notifyDataSetInvalidated() + } + } + + @Override + override fun onSuggestionClicked(suggestionId: Long) { + if (isClosed) { + Log.w(TAG, "onSuggestionClicked after close") + } else if (mSuggestionClickListener != null) { + mSuggestionClickListener!!.onSuggestionClicked(this, suggestionId) + } + } + + @Override + override fun onSuggestionQueryRefineClicked(suggestionId: Long) { + if (isClosed) { + Log.w(TAG, "onSuggestionQueryRefineClicked after close") + } else if (mSuggestionClickListener != null) { + mSuggestionClickListener!!.onSuggestionQueryRefineClicked(this, suggestionId) + } + } + + @get:Override abstract override val listAdapter: A + protected abstract fun notifyDataSetInvalidated() + protected abstract fun notifyDataSetChanged() + private inner class MySuggestionsObserver : DataSetObserver() { + @Override + override fun onChanged() { + onSuggestionsChanged() + } + } + + private inner class SuggestionViewClickListener(private val mSuggestionId: Long) : + View.OnClickListener { + @Override + override fun onClick(v: View?) { + onSuggestionClicked(mSuggestionId) + } + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.SuggestionsAdapter" + } + + init { + mViewTypeMap = hashMapOf<String, Int>() + for (viewType in mViewFactory.suggestionViewTypes) { + if (!mViewTypeMap.containsKey(viewType)) { + mViewTypeMap.put(viewType, mViewTypeMap.size) + } + } + } +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsListAdapter.java b/src/com/android/quicksearchbox/ui/SuggestionsListAdapter.java deleted file mode 100644 index 8bbdfbf..0000000 --- a/src/com/android/quicksearchbox/ui/SuggestionsListAdapter.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox.ui; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ListAdapter; - -import com.android.quicksearchbox.SuggestionCursor; -import com.android.quicksearchbox.SuggestionPosition; -import com.android.quicksearchbox.Suggestions; - -/** - * Uses a {@link Suggestions} object to back a {@link SuggestionsView}. - */ -public class SuggestionsListAdapter extends SuggestionsAdapterBase<ListAdapter> { - - private Adapter mAdapter; - - public SuggestionsListAdapter(SuggestionViewFactory viewFactory) { - super(viewFactory); - mAdapter = new Adapter(); - } - - @Override - public boolean isEmpty() { - return mAdapter.getCount() == 0; - } - - @Override - public SuggestionPosition getSuggestion(long suggestionId) { - return new SuggestionPosition(getCurrentSuggestions(), (int) suggestionId); - } - - @Override - public BaseAdapter getListAdapter() { - return mAdapter; - } - - @Override - public void notifyDataSetChanged() { - mAdapter.notifyDataSetChanged(); - } - - @Override - public void notifyDataSetInvalidated() { - mAdapter.notifyDataSetInvalidated(); - } - - class Adapter extends BaseAdapter { - - @Override - public int getCount() { - SuggestionCursor s = getCurrentSuggestions(); - return s == null ? 0 : s.getCount(); - } - - @Override - public Object getItem(int position) { - return getSuggestion(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - return SuggestionsListAdapter.this.getView( - getCurrentSuggestions(), position, position, convertView, parent); - } - - @Override - public int getItemViewType(int position) { - return getSuggestionViewType(getCurrentSuggestions(), position); - } - - @Override - public int getViewTypeCount() { - return getSuggestionViewTypeCount(); - } - - } - -} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsListAdapter.kt b/src/com/android/quicksearchbox/ui/SuggestionsListAdapter.kt new file mode 100644 index 0000000..1762341 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionsListAdapter.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ListAdapter +import com.android.quicksearchbox.SuggestionCursor +import com.android.quicksearchbox.SuggestionPosition + +/** Uses a [Suggestions] object to back a [SuggestionsView]. */ +class SuggestionsListAdapter(viewFactory: SuggestionViewFactory?) : + SuggestionsAdapterBase<ListAdapter?>(viewFactory!!) { + private val mAdapter: SuggestionsListAdapter.Adapter + + @get:Override + override val isEmpty: Boolean + get() = mAdapter.getCount() == 0 + + @Override + override fun getSuggestion(suggestionId: Long): SuggestionPosition { + return SuggestionPosition(currentSuggestions, suggestionId.toInt()) + } + + @get:Override + override val listAdapter: BaseAdapter + get() = mAdapter + + @Override + public override fun notifyDataSetChanged() { + mAdapter.notifyDataSetChanged() + } + + @Override + public override fun notifyDataSetInvalidated() { + mAdapter.notifyDataSetInvalidated() + } + + internal inner class Adapter : BaseAdapter() { + @Override + override fun getCount(): Int { + val s: SuggestionCursor? = currentSuggestions + return s?.count ?: 0 + } + + @Override + override fun getItem(position: Int): Any? { + return getSuggestion(position) + } + + @Override + override fun getItemId(position: Int): Long { + return position.toLong() + } + + @Override + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? { + return this@SuggestionsListAdapter.getView( + currentSuggestions, + position, + position.toLong(), + convertView, + parent + ) + } + + @Override + override fun getItemViewType(position: Int): Int { + return getSuggestionViewType(currentSuggestions, position) + } + + @Override + override fun getViewTypeCount(): Int { + return suggestionViewTypeCount + } + } + + init { + mAdapter = Adapter() + } +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsListView.java b/src/com/android/quicksearchbox/ui/SuggestionsListView.java deleted file mode 100644 index a162f3a..0000000 --- a/src/com/android/quicksearchbox/ui/SuggestionsListView.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import android.view.View; -import android.widget.AbsListView; - -/** - * Interface for suggestions list UI views. - */ -public interface SuggestionsListView<A> { - - /** - * See {@link View#setOnKeyListener}. - */ - void setOnKeyListener(View.OnKeyListener l); - - /** - * See {@link AbsListView#setOnScrollListener}. - */ - void setOnScrollListener(AbsListView.OnScrollListener l); - - /** - * See {@link View#setOnFocusChangeListener}. - */ - void setOnFocusChangeListener(View.OnFocusChangeListener l); - - /** - * See {@link View#setVisibility}. - */ - void setVisibility(int visibility); - - /** - * Sets the adapter for the list. See {@link AbsListView#setAdapter} - */ - void setSuggestionsAdapter(SuggestionsAdapter<A> adapter); - - /** - * Gets the adapter for the list. - */ - SuggestionsAdapter<A> getSuggestionsAdapter(); - - /** - * Gets the ID of the currently selected item. - */ - long getSelectedItemId(); - -} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsListView.kt b/src/com/android/quicksearchbox/ui/SuggestionsListView.kt new file mode 100644 index 0000000..6b0b6c0 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionsListView.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.view.View +import android.widget.AbsListView + +/** Interface for suggestions list UI views. */ +interface SuggestionsListView<A> { + /** See [View.setOnKeyListener]. */ + fun setOnKeyListener(l: View.OnKeyListener?) + + /** See [AbsListView.setOnScrollListener]. */ + fun setOnScrollListener(l: AbsListView.OnScrollListener?) + + /** See [View.setOnFocusChangeListener]. */ + fun setOnFocusChangeListener(l: View.OnFocusChangeListener?) + + /** See [View.setVisibility]. */ + fun setVisibility(visibility: Int) + + /** Sets the adapter for the list. See [AbsListView.setAdapter] */ + fun setSuggestionsAdapter(adapter: SuggestionsAdapter<A?>?) + + /** Gets the adapter for the list. */ + fun getSuggestionsAdapter(): SuggestionsAdapter<A?>? + + /** Gets the ID of the currently selected item. */ + fun getSelectedItemId(): Long +} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsView.java b/src/com/android/quicksearchbox/ui/SuggestionsView.java deleted file mode 100644 index 51deb67..0000000 --- a/src/com/android/quicksearchbox/ui/SuggestionsView.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox.ui; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.ListAdapter; -import android.widget.ListView; - -import com.android.quicksearchbox.SuggestionPosition; - -/** - * Holds a list of suggestions. - */ -public class SuggestionsView extends ListView implements SuggestionsListView<ListAdapter> { - - private static final boolean DBG = false; - private static final String TAG = "QSB.SuggestionsView"; - - private SuggestionsAdapter<ListAdapter> mSuggestionsAdapter; - - public SuggestionsView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void setSuggestionsAdapter(SuggestionsAdapter<ListAdapter> adapter) { - super.setAdapter(adapter == null ? null : adapter.getListAdapter()); - mSuggestionsAdapter = adapter; - } - - @Override - public SuggestionsAdapter<ListAdapter> getSuggestionsAdapter() { - return mSuggestionsAdapter; - } - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - setItemsCanFocus(true); - } - - /** - * Gets the position of the selected suggestion. - * - * @return A 0-based index, or {@code -1} if no suggestion is selected. - */ - public int getSelectedPosition() { - return getSelectedItemPosition(); - } - - /** - * Gets the selected suggestion. - * - * @return {@code null} if no suggestion is selected. - */ - public SuggestionPosition getSelectedSuggestion() { - return (SuggestionPosition) getSelectedItem(); - } - - -} diff --git a/src/com/android/quicksearchbox/ui/SuggestionsView.kt b/src/com/android/quicksearchbox/ui/SuggestionsView.kt new file mode 100644 index 0000000..c13df95 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/SuggestionsView.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.ListAdapter +import android.widget.ListView +import com.android.quicksearchbox.SuggestionPosition + +/** Holds a list of suggestions. */ +class SuggestionsView(context: Context?, attrs: AttributeSet?) : + ListView(context, attrs), SuggestionsListView<ListAdapter?> { + private var mSuggestionsAdapter: SuggestionsAdapter<ListAdapter?>? = null + + @Override + override fun setSuggestionsAdapter(adapter: SuggestionsAdapter<ListAdapter?>?) { + super.setAdapter(adapter?.listAdapter) + mSuggestionsAdapter = adapter + } + + @Override + override fun getSuggestionsAdapter(): SuggestionsAdapter<ListAdapter?>? { + return mSuggestionsAdapter + } + + @Override + override fun onFinishInflate() { + super.onFinishInflate() + setItemsCanFocus(true) + } + + /** + * Gets the position of the selected suggestion. + * + * @return A 0-based index, or `-1` if no suggestion is selected. + */ + val selectedPosition: Int + get() = getSelectedItemPosition() + + /** + * Gets the selected suggestion. + * + * @return `null` if no suggestion is selected. + */ + val selectedSuggestion: SuggestionPosition + get() = getSelectedItem() as SuggestionPosition + + companion object { + private const val DBG = false + private const val TAG = "QSB.SuggestionsView" + } +} diff --git a/src/com/android/quicksearchbox/ui/WebSearchSuggestionView.java b/src/com/android/quicksearchbox/ui/WebSearchSuggestionView.java deleted file mode 100644 index e01bd7e..0000000 --- a/src/com/android/quicksearchbox/ui/WebSearchSuggestionView.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.ui; - -import com.android.quicksearchbox.QsbApplication; -import com.android.quicksearchbox.R; -import com.android.quicksearchbox.Suggestion; -import com.android.quicksearchbox.SuggestionFormatter; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.View; - -/** - * View for web search suggestions. - */ -public class WebSearchSuggestionView extends BaseSuggestionView { - - private static final String VIEW_ID = "web_search"; - - private final SuggestionFormatter mSuggestionFormatter; - - public WebSearchSuggestionView(Context context, AttributeSet attrs) { - super(context, attrs); - mSuggestionFormatter = QsbApplication.get(context).getSuggestionFormatter(); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - KeyListener keyListener = new KeyListener(); - setOnKeyListener(keyListener); - mIcon2.setOnKeyListener(keyListener); - mIcon2.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - onSuggestionQueryRefineClicked(); - } - }); - mIcon2.setFocusable(true); - } - - @Override - public void bindAsSuggestion(Suggestion suggestion, String userQuery) { - super.bindAsSuggestion(suggestion, userQuery); - - CharSequence text1 = mSuggestionFormatter.formatSuggestion(userQuery, - suggestion.getSuggestionText1()); - setText1(text1); - setIsHistorySuggestion(suggestion.isHistorySuggestion()); - } - - private void setIsHistorySuggestion(boolean isHistory) { - if (isHistory) { - mIcon1.setImageResource(R.drawable.ic_history_suggestion); - mIcon1.setVisibility(VISIBLE); - } else { - mIcon1.setVisibility(INVISIBLE); - } - } - - private class KeyListener implements View.OnKeyListener { - public boolean onKey(View v, int keyCode, KeyEvent event) { - boolean consumed = false; - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && v != mIcon2) { - consumed = mIcon2.requestFocus(); - } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && v == mIcon2) { - consumed = requestFocus(); - } - } - return consumed; - } - } - - public static class Factory extends SuggestionViewInflater { - - public Factory(Context context) { - super(VIEW_ID, WebSearchSuggestionView.class, R.layout.web_search_suggestion, context); - } - - @Override - public boolean canCreateView(Suggestion suggestion) { - return suggestion.isWebSearchSuggestion(); - } - } - -} diff --git a/src/com/android/quicksearchbox/ui/WebSearchSuggestionView.kt b/src/com/android/quicksearchbox/ui/WebSearchSuggestionView.kt new file mode 100644 index 0000000..daebb46 --- /dev/null +++ b/src/com/android/quicksearchbox/ui/WebSearchSuggestionView.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.View +import com.android.quicksearchbox.QsbApplication +import com.android.quicksearchbox.R +import com.android.quicksearchbox.Suggestion +import com.android.quicksearchbox.SuggestionFormatter + +/** View for web search suggestions. */ +class WebSearchSuggestionView(context: Context?, attrs: AttributeSet?) : + BaseSuggestionView(context, attrs) { + private val mSuggestionFormatter: SuggestionFormatter? + + @Override + override fun onFinishInflate() { + super.onFinishInflate() + val keyListener: WebSearchSuggestionView.KeyListener = KeyListener() + setOnKeyListener(keyListener) + mIcon2?.setOnKeyListener(keyListener) + mIcon2?.setOnClickListener( + object : OnClickListener { + override fun onClick(v: View?) { + onSuggestionQueryRefineClicked() + } + } + ) + mIcon2?.setFocusable(true) + } + + @Override + override fun bindAsSuggestion(suggestion: Suggestion?, userQuery: String?) { + super.bindAsSuggestion(suggestion, userQuery) + val text1 = mSuggestionFormatter?.formatSuggestion(userQuery, suggestion?.suggestionText1) + setText1(text1) + setIsHistorySuggestion(suggestion?.isHistorySuggestion) + } + + private fun setIsHistorySuggestion(isHistory: Boolean?) { + if (isHistory == true) { + mIcon1?.setImageResource(R.drawable.ic_history_suggestion) + mIcon1?.setVisibility(VISIBLE) + } else { + mIcon1?.setVisibility(INVISIBLE) + } + } + + private inner class KeyListener : View.OnKeyListener { + override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean { + var consumed = false + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && v !== mIcon2) { + consumed = mIcon2!!.requestFocus() + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && v === mIcon2) { + consumed = requestFocus() + } + } + return consumed + } + } + + class Factory(context: Context?) : + SuggestionViewInflater( + VIEW_ID, + WebSearchSuggestionView::class.java, + R.layout.web_search_suggestion, + context + ) { + @Override + override fun canCreateView(suggestion: Suggestion?): Boolean { + return suggestion!!.isWebSearchSuggestion + } + } + + companion object { + private const val VIEW_ID = "web_search" + } + + init { + mSuggestionFormatter = QsbApplication[context].suggestionFormatter + } +} diff --git a/src/com/android/quicksearchbox/util/AsyncDataSetObservable.java b/src/com/android/quicksearchbox/util/AsyncDataSetObservable.java deleted file mode 100644 index 5a75ff6..0000000 --- a/src/com/android/quicksearchbox/util/AsyncDataSetObservable.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -import android.database.DataSetObservable; -import android.os.Handler; - -/** - * A version of {@link DataSetObservable} that performs callbacks on given {@link Handler}. - */ -public class AsyncDataSetObservable extends DataSetObservable { - - private final Handler mHandler; - - private final Runnable mChangedRunnable = new Runnable() { - public void run() { - AsyncDataSetObservable.super.notifyChanged(); - } - }; - - private final Runnable mInvalidatedRunnable = new Runnable() { - public void run() { - AsyncDataSetObservable.super.notifyInvalidated(); - } - }; - - /** - * @param handler Handler to run callbacks on. - */ - public AsyncDataSetObservable(Handler handler) { - mHandler = handler; - } - - @Override - public void notifyChanged() { - if (mHandler == null) { - super.notifyChanged(); - } else { - mHandler.post(mChangedRunnable); - } - } - - @Override - public void notifyInvalidated() { - if (mHandler == null) { - super.notifyInvalidated(); - } else { - mHandler.post(mInvalidatedRunnable); - } - } - -} diff --git a/src/com/android/quicksearchbox/util/AsyncDataSetObservable.kt b/src/com/android/quicksearchbox/util/AsyncDataSetObservable.kt new file mode 100644 index 0000000..1732af8 --- /dev/null +++ b/src/com/android/quicksearchbox/util/AsyncDataSetObservable.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import android.database.DataSetObservable +import android.os.Handler + +/** A version of [DataSetObservable] that performs callbacks on given [Handler]. */ +class AsyncDataSetObservable(handler: Handler?) : DataSetObservable() { + private val mHandler: Handler? + private val mChangedRunnable: Runnable = + object : Runnable { + override fun run() { + super@AsyncDataSetObservable.notifyChanged() + } + } + private val mInvalidatedRunnable: Runnable = + object : Runnable { + override fun run() { + super@AsyncDataSetObservable.notifyInvalidated() + } + } + + @Override + override fun notifyChanged() { + if (mHandler == null) { + super.notifyChanged() + } else { + mHandler.post(mChangedRunnable) + } + } + + @Override + override fun notifyInvalidated() { + if (mHandler == null) { + super.notifyInvalidated() + } else { + mHandler.post(mInvalidatedRunnable) + } + } + + /** @param handler Handler to run callbacks on. */ + init { + mHandler = handler + } +} diff --git a/src/com/android/quicksearchbox/util/BarrierConsumer.java b/src/com/android/quicksearchbox/util/BarrierConsumer.java deleted file mode 100644 index d02ae79..0000000 --- a/src/com/android/quicksearchbox/util/BarrierConsumer.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -import java.util.ArrayList; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -/** - * A consumer that consumes a fixed number of values. When the expected number of values - * has been consumed, further values are rejected. - */ -public class BarrierConsumer<A> implements Consumer<A> { - - private final Lock mLock = new ReentrantLock(); - private final Condition mNotFull = mLock.newCondition(); - - private final int mExpectedCount; - - // Set to null when getValues() returns. - private ArrayList<A> mValues; - - /** - * Constructs a new BarrierConsumer. - * - * @param expectedCount The number of values to consume. - */ - public BarrierConsumer(int expectedCount) { - mExpectedCount = expectedCount; - mValues = new ArrayList<A>(expectedCount); - } - - /** - * Blocks until the expected number of results is available, or until the thread is - * interrupted. This method should not be called multiple times. - * - * @return A list of values, never {@code null}. - */ - public ArrayList<A> getValues() { - mLock.lock(); - try { - try { - while (!isFull()) { - mNotFull.await(); - } - } catch (InterruptedException ex) { - // Return the values that we've gotten so far - } - ArrayList<A> values = mValues; - mValues = null; // mark that getValues() has returned - return values; - } finally { - mLock.unlock(); - } - } - - public boolean consume(A value) { - mLock.lock(); - try { - // Do nothing if getValues() has alrady returned, - // or enough values have already been consumed - if (mValues == null || isFull()) { - return false; - } - mValues.add(value); - if (isFull()) { - // Wake up any thread waiting in getValues() - mNotFull.signal(); - } - return true; - } finally { - mLock.unlock(); - } - } - - private boolean isFull() { - return mValues.size() == mExpectedCount; - } -} diff --git a/src/com/android/quicksearchbox/util/BarrierConsumer.kt b/src/com/android/quicksearchbox/util/BarrierConsumer.kt new file mode 100644 index 0000000..83ff1d2 --- /dev/null +++ b/src/com/android/quicksearchbox/util/BarrierConsumer.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import java.util.ArrayList +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock + +/** + * A consumer that consumes a fixed number of values. When the expected number of values has been + * consumed, further values are rejected. + */ +class BarrierConsumer<A>(private val mExpectedCount: Int) : Consumer<A> { + private val mLock: Lock = ReentrantLock() + private val mNotFull: Condition = mLock.newCondition() + + // Set to null when getValues() returns. + private var mValues: ArrayList<A>? + + /** + * Blocks until the expected number of results is available, or until the thread is interrupted. + * This method should not be called multiple times. + * + * @return A list of values, never `null`. + */ + val values: ArrayList<A>? + get() { + mLock.lock() + return try { + try { + while (!isFull) { + mNotFull.await() + } + } catch (ex: InterruptedException) { + // Return the values that we've gotten so far + } + val values = mValues + mValues = null // mark that getValues() has returned + values + } finally { + mLock.unlock() + } + } + + override fun consume(value: A): Boolean { + mLock.lock() + return try { + // Do nothing if getValues() has already returned, + // or enough values have already been consumed + if (mValues == null || isFull) { + return false + } + mValues?.add(value) + if (isFull) { + // Wake up any thread waiting in getValues() + mNotFull.signal() + } + true + } finally { + mLock.unlock() + } + } + + private val isFull: Boolean + get() = mValues!!.size == mExpectedCount + + /** + * Constructs a new BarrierConsumer. + * + * @param expectedCount The number of values to consume. + */ + init { + mValues = ArrayList<A>(mExpectedCount) + } +} diff --git a/src/com/android/quicksearchbox/util/BatchingNamedTaskExecutor.java b/src/com/android/quicksearchbox/util/BatchingNamedTaskExecutor.java deleted file mode 100644 index 08d3ed7..0000000 --- a/src/com/android/quicksearchbox/util/BatchingNamedTaskExecutor.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - - -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; - -/** - * Executes NamedTasks in batches of a given size. Tasks are queued until - * executeNextBatch is called. - */ -public class BatchingNamedTaskExecutor implements NamedTaskExecutor { - - private static final boolean DBG = false; - private static final String TAG = "QSB.BatchingNamedTaskExecutor"; - - private final NamedTaskExecutor mExecutor; - - /** Queue of tasks waiting to be dispatched to mExecutor **/ - private final ArrayList<NamedTask> mQueuedTasks = new ArrayList<NamedTask>(); - - /** - * Creates a new BatchingSourceTaskExecutor. - * - * @param executor A SourceTaskExecutor for actually executing the tasks. - */ - public BatchingNamedTaskExecutor(NamedTaskExecutor executor) { - mExecutor = executor; - } - - public void execute(NamedTask task) { - synchronized (mQueuedTasks) { - if (DBG) Log.d(TAG, "Queuing " + task); - mQueuedTasks.add(task); - } - } - - private void dispatch(NamedTask task) { - if (DBG) Log.d(TAG, "Dispatching " + task); - mExecutor.execute(task); - } - - /** - * Instructs the executor to submit the next batch of results. - * @param batchSize the maximum number of entries to execute. - */ - public void executeNextBatch(int batchSize) { - NamedTask[] batch = new NamedTask[0]; - synchronized (mQueuedTasks) { - int count = Math.min(mQueuedTasks.size(), batchSize); - List<NamedTask> nextTasks = mQueuedTasks.subList(0, count); - batch = nextTasks.toArray(batch); - nextTasks.clear(); - if (DBG) Log.d(TAG, "Dispatching batch of " + count); - } - - for (NamedTask task : batch) { - dispatch(task); - } - } - - /** - * Cancel any unstarted tasks running in this executor. This instance - * should not be re-used after calling this method. - */ - public void cancelPendingTasks() { - synchronized (mQueuedTasks) { - mQueuedTasks.clear(); - } - } - - public void close() { - cancelPendingTasks(); - mExecutor.close(); - } -} diff --git a/src/com/android/quicksearchbox/util/BatchingNamedTaskExecutor.kt b/src/com/android/quicksearchbox/util/BatchingNamedTaskExecutor.kt new file mode 100644 index 0000000..a4fbc26 --- /dev/null +++ b/src/com/android/quicksearchbox/util/BatchingNamedTaskExecutor.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import android.util.Log + +/** + * Executes NamedTasks in batches of a given size. Tasks are queued until executeNextBatch is + * called. + * @param executor A SourceTaskExecutor for actually executing the tasks. + */ +class BatchingNamedTaskExecutor(private val mExecutor: NamedTaskExecutor) : NamedTaskExecutor { + /** Queue of tasks waiting to be dispatched to mExecutor */ + private val mQueuedTasks: ArrayList<NamedTask?> = arrayListOf() + override fun execute(task: NamedTask?) { + synchronized(mQueuedTasks) { + if (DBG) Log.d(TAG, "Queuing $task") + mQueuedTasks.add(task) + } + } + + private fun dispatch(task: NamedTask?) { + if (DBG) Log.d(TAG, "Dispatching $task") + mExecutor.execute(task) + } + + /** + * Instructs the executor to submit the next batch of results. + * @param batchSize the maximum number of entries to execute. + */ + fun executeNextBatch(batchSize: Int) { + var batch = arrayOfNulls<NamedTask?>(0) + synchronized(mQueuedTasks) { + val count: Int = Math.min(mQueuedTasks.size, batchSize) + val nextTasks: ArrayList<NamedTask?> = mQueuedTasks.subList(0, count) as ArrayList<NamedTask?> + batch = nextTasks.toArray(batch) + nextTasks.clear() + if (DBG) Log.d(TAG, "Dispatching batch of $count") + } + for (task in batch) { + dispatch(task) + } + } + + /** + * Cancel any un-started tasks running in this executor. This instance should not be re-used after + * calling this method. + */ + override fun cancelPendingTasks() { + synchronized(mQueuedTasks) { mQueuedTasks.clear() } + } + + override fun close() { + cancelPendingTasks() + mExecutor.close() + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.BatchingNamedTaskExecutor" + } +} diff --git a/src/com/android/quicksearchbox/util/CachedLater.java b/src/com/android/quicksearchbox/util/CachedLater.java deleted file mode 100644 index 49e86ba..0000000 --- a/src/com/android/quicksearchbox/util/CachedLater.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; - -/** - * Abstract base class for a one-place cache that holds a value that is produced - * asynchronously. - * - * @param <A> The type of the data held in the cache. - */ -public abstract class CachedLater<A> implements NowOrLater<A> { - - private static final String TAG = "QSB.AsyncCache"; - private static final boolean DBG = false; - - private final Object mLock = new Object(); - - private A mValue; - - private boolean mCreating; - private boolean mValid; - - private List<Consumer<? super A>> mWaitingConsumers; - - /** - * Creates the object to store in the cache. This method must call - * {@link #store} when it's done. - * This method must not block. - */ - protected abstract void create(); - - /** - * Saves a new value to the cache. - */ - protected void store(A value) { - if (DBG) Log.d(TAG, "store()"); - List<Consumer<? super A>> waitingConsumers; - synchronized (mLock) { - mValue = value; - mValid = true; - mCreating = false; - waitingConsumers = mWaitingConsumers; - mWaitingConsumers = null; - } - if (waitingConsumers != null) { - for (Consumer<? super A> consumer : waitingConsumers) { - if (DBG) Log.d(TAG, "Calling consumer: " + consumer); - consumer.consume(value); - } - } - } - - /** - * Gets the value. - * - * @param consumer A consumer that will be given the cached value. - * The consumer may be called synchronously, or asynchronously on - * an unspecified thread. - */ - public void getLater(Consumer<? super A> consumer) { - if (DBG) Log.d(TAG, "getLater()"); - boolean valid; - A value; - synchronized (mLock) { - valid = mValid; - value = mValue; - if (!valid) { - if (mWaitingConsumers == null) { - mWaitingConsumers = new ArrayList<Consumer<? super A>>(); - } - mWaitingConsumers.add(consumer); - } - } - if (valid) { - if (DBG) Log.d(TAG, "valid, calling consumer synchronously"); - consumer.consume(value); - } else { - boolean create = false; - synchronized (mLock) { - if (!mCreating) { - mCreating = true; - create = true; - } - } - if (create) { - if (DBG) Log.d(TAG, "not valid, calling create()"); - create(); - } else { - if (DBG) Log.d(TAG, "not valid, already creating"); - } - } - } - - /** - * Clears the cache. - */ - public void clear() { - if (DBG) Log.d(TAG, "clear()"); - synchronized (mLock) { - mValue = null; - mValid = false; - } - } - - public boolean haveNow() { - synchronized (mLock) { - return mValid; - } - } - - public synchronized A getNow() { - synchronized (mLock) { - if (!haveNow()) { - throw new IllegalStateException("getNow() called when haveNow() is false"); - } - return mValue; - } - } - -} diff --git a/src/com/android/quicksearchbox/util/CachedLater.kt b/src/com/android/quicksearchbox/util/CachedLater.kt new file mode 100644 index 0000000..a198683 --- /dev/null +++ b/src/com/android/quicksearchbox/util/CachedLater.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import android.util.Log +import kotlin.collections.MutableList + +/** + * Abstract base class for a one-place cache that holds a value that is produced asynchronously. + * + * @param <A> The type of the data held in the cache. + */ +abstract class CachedLater<A> : NowOrLater<A> { + private val mLock: Any = Any() + private var mValue: A? = null + private var mCreating = false + private var mValid = false + private var mWaitingConsumers: MutableList<Consumer<in A>>? = null + + /** + * Creates the object to store in the cache. This method must call [.store] when it's done. This + * method must not block. + */ + protected abstract fun create() + + /** Saves a new value to the cache. */ + protected fun store(value: A) { + if (DBG) Log.d(TAG, "store()") + var waitingConsumers: MutableList<Consumer<in A>>? + synchronized(mLock) { + mValue = value + mValid = true + mCreating = false + waitingConsumers = mWaitingConsumers + mWaitingConsumers = null + } + if (waitingConsumers != null) { + for (consumer in waitingConsumers!!) { + if (DBG) Log.d(TAG, "Calling consumer: $consumer") + consumer.consume(value) + } + } + } + + /** + * Gets the value. + * + * @param consumer A consumer that will be given the cached value. The consumer may be called + * synchronously, or asynchronously on an unspecified thread. + */ + override fun getLater(consumer: Consumer<in A>?) { + if (DBG) Log.d(TAG, "getLater()") + var valid: Boolean + var value: A? + synchronized(mLock) { + valid = mValid + value = mValue + if (!valid) { + if (mWaitingConsumers == null) { + mWaitingConsumers = mutableListOf() + } + mWaitingConsumers?.add(consumer!!) + } + } + if (valid) { + if (DBG) Log.d(TAG, "valid, calling consumer synchronously") + consumer!!.consume(value!!) + } else { + var create = false + synchronized(mLock) { + if (!mCreating) { + mCreating = true + create = true + } + } + if (create) { + if (DBG) Log.d(TAG, "not valid, calling create()") + create() + } else { + if (DBG) Log.d(TAG, "not valid, already creating") + } + } + } + + /** Clears the cache. */ + fun clear() { + if (DBG) Log.d(TAG, "clear()") + synchronized(mLock) { + mValue = null + mValid = false + } + } + + override fun haveNow(): Boolean { + synchronized(mLock) { + return mValid + } + } + + @get:Synchronized + override val now: A + get() { + synchronized(mLock) { + if (!haveNow()) { + throw IllegalStateException("getNow() called when haveNow() is false") + } + return mValue!! + } + } + + companion object { + private const val TAG = "QSB.AsyncCache" + private const val DBG = false + } +} diff --git a/src/com/android/quicksearchbox/util/Consumer.java b/src/com/android/quicksearchbox/util/Consumer.kt index 942b5dc..185eaa2 100644 --- a/src/com/android/quicksearchbox/util/Consumer.java +++ b/src/com/android/quicksearchbox/util/Consumer.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2022 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. @@ -13,22 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package com.android.quicksearchbox.util; +package com.android.quicksearchbox.util /** * Interface for data consumers. * - * @param <A> The type of data to consume. + * @param <A> The type of data to consume. </A> */ -public interface Consumer<A> { - - /** - * Consumes a value. - * - * @param value The value to consume. - * @return {@code true} if the value was accepted, {@code false} otherwise. - */ - boolean consume(A value); - +interface Consumer<A> { + /** + * Consumes a value. + * + * @param value The value to consume. + * @return `true` if the value was accepted, `false` otherwise. + */ + fun consume(value: A): Boolean } diff --git a/src/com/android/quicksearchbox/util/Consumers.java b/src/com/android/quicksearchbox/util/Consumers.java deleted file mode 100644 index 52a97b1..0000000 --- a/src/com/android/quicksearchbox/util/Consumers.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -import android.os.Handler; - -/** - * Consumer utilities. - */ -public class Consumers { - - private Consumers() {} - - public static <A extends QuietlyCloseable> void consumeCloseable(Consumer<A> consumer, - A value) { - boolean accepted = false; - try { - accepted = consumer.consume(value); - } finally { - if (!accepted && value != null) value.close(); - } - } - - public static <A> void consumeAsync(Handler handler, - final Consumer<A> consumer, final A value) { - if (handler == null) { - consumer.consume(value); - } else { - handler.post(new Runnable() { - public void run() { - consumer.consume(value); - } - }); - } - } - - public static <A extends QuietlyCloseable> void consumeCloseableAsync(Handler handler, - final Consumer<A> consumer, final A value) { - if (handler == null) { - consumeCloseable(consumer, value); - } else { - handler.post(new Runnable() { - public void run() { - consumeCloseable(consumer, value); - } - }); - } - } - - public static <A> Consumer<A> createAsyncConsumer( - final Handler handler, final Consumer<A> consumer) { - return new Consumer<A>() { - public boolean consume(A value) { - consumeAsync(handler, consumer, value); - return true; - } - }; - } - - public static <A extends QuietlyCloseable> Consumer<A> createAsyncCloseableConsumer( - final Handler handler, final Consumer<A> consumer) { - return new Consumer<A>() { - public boolean consume(A value) { - consumeCloseableAsync(handler, consumer, value); - return true; - } - }; - } - -} diff --git a/src/com/android/quicksearchbox/util/Consumers.kt b/src/com/android/quicksearchbox/util/Consumers.kt new file mode 100644 index 0000000..481d24c --- /dev/null +++ b/src/com/android/quicksearchbox/util/Consumers.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import android.os.Handler + +/** Consumer utilities. */ +object Consumers { + @JvmStatic + fun <A : QuietlyCloseable?> consumeCloseable(consumer: Consumer<A>?, value: A?) { + var accepted = false + try { + accepted = consumer!!.consume(value!!) + } finally { + if (!accepted && value != null) value.close() + } + } + + @JvmStatic + fun <A> consumeAsync(handler: Handler?, consumer: Consumer<A?>?, value: A?) { + if (handler == null) { + consumer?.consume(value) + } else { + handler.post( + object : Runnable { + override fun run() { + consumer?.consume(value) + } + } + ) + } + } + + @JvmStatic + fun <A : QuietlyCloseable?> consumeCloseableAsync( + handler: Handler?, + consumer: Consumer<A>?, + value: A? + ) { + if (handler == null) { + consumeCloseable(consumer, value) + } else { + handler.post( + object : Runnable { + override fun run() { + consumeCloseable(consumer, value) + } + } + ) + } + } + + fun <A> createAsyncConsumer(handler: Handler?, consumer: Consumer<A?>?): Consumer<A?> { + return object : Consumer<A?> { + override fun consume(value: A?): Boolean { + consumeAsync(handler, consumer, value) + return true + } + } + } + + fun <A : QuietlyCloseable?> createAsyncCloseableConsumer( + handler: Handler?, + consumer: Consumer<A>? + ): Consumer<A?> { + return object : Consumer<A?> { + override fun consume(value: A?): Boolean { + consumeCloseableAsync(handler, consumer, value) + return true + } + } + } +} diff --git a/src/com/android/quicksearchbox/util/Factory.java b/src/com/android/quicksearchbox/util/Factory.kt index 8aebe5c..9c25ff5 100644 --- a/src/com/android/quicksearchbox/util/Factory.java +++ b/src/com/android/quicksearchbox/util/Factory.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2022 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. @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.quicksearchbox.util -package com.android.quicksearchbox.util; - -public interface Factory<A> { - - A create(); - +interface Factory<A> { + fun create(): A } diff --git a/src/com/android/quicksearchbox/util/HttpHelper.java b/src/com/android/quicksearchbox/util/HttpHelper.java deleted file mode 100644 index f300db4..0000000 --- a/src/com/android/quicksearchbox/util/HttpHelper.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -/** - * An interface that can issue HTTP GET / POST requests - * with timeouts. - */ -public interface HttpHelper { - - public String get(GetRequest request) throws IOException, HttpException; - - public String get(String url, Map<String,String> requestHeaders) - throws IOException, HttpException; - - public String post(PostRequest request) throws IOException, HttpException; - - public String post(String url, Map<String,String> requestHeaders, String content) - throws IOException, HttpException; - - public void setConnectTimeout(int timeoutMillis); - - public void setReadTimeout(int timeoutMillis); - - public static class GetRequest { - private String mUrl; - private Map<String,String> mHeaders; - - /** - * Creates a new request. - */ - public GetRequest() { - } - - /** - * Creates a new request. - * - * @param url Request URI. - */ - public GetRequest(String url) { - mUrl = url; - } - - /** - * Gets the request URI. - */ - public String getUrl() { - return mUrl; - } - /** - * Sets the request URI. - */ - public void setUrl(String url) { - mUrl = url; - } - - /** - * Gets the request headers. - * - * @return The response headers. May return {@code null} if no headers are set. - */ - public Map<String, String> getHeaders() { - return mHeaders; - } - - /** - * Sets a request header. - * - * @param name Header name. - * @param value Header value. - */ - public void setHeader(String name, String value) { - if (mHeaders == null) { - mHeaders = new HashMap<String,String>(); - } - mHeaders.put(name, value); - } - } - - public static class PostRequest extends GetRequest { - - private String mContent; - - public PostRequest() { - } - - public PostRequest(String url) { - super(url); - } - - public void setContent(String content) { - mContent = content; - } - - public String getContent() { - return mContent; - } - } - - /** - * A HTTP exception. - */ - public static class HttpException extends IOException { - private final int mStatusCode; - private final String mReasonPhrase; - - public HttpException(int statusCode, String reasonPhrase) { - super(statusCode + " " + reasonPhrase); - mStatusCode = statusCode; - mReasonPhrase = reasonPhrase; - } - - /** - * Gets the HTTP response status code. - */ - public int getStatusCode() { - return mStatusCode; - } - - /** - * Gets the HTTP response reason phrase. - */ - public String getReasonPhrase() { - return mReasonPhrase; - } - } - - /** - * An interface for URL rewriting. - */ - public static interface UrlRewriter { - public String rewrite(String url); - } -} diff --git a/src/com/android/quicksearchbox/util/HttpHelper.kt b/src/com/android/quicksearchbox/util/HttpHelper.kt new file mode 100644 index 0000000..0daf8d0 --- /dev/null +++ b/src/com/android/quicksearchbox/util/HttpHelper.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import java.io.IOException + +/** An interface that can issue HTTP GET / POST requests with timeouts. */ +interface HttpHelper { + @Throws(IOException::class, HttpException::class) operator fun get(request: GetRequest?): String? + + @Throws(IOException::class, HttpException::class) + operator fun get(url: String?, requestHeaders: MutableMap<String, String>?): String? + + @Throws(IOException::class, HttpException::class) fun post(request: PostRequest?): String? + + @Throws(IOException::class, HttpException::class) + fun post(url: String?, requestHeaders: MutableMap<String, String>?, content: String?): String? + fun setConnectTimeout(timeoutMillis: Int) + fun setReadTimeout(timeoutMillis: Int) + open class GetRequest { + /** Gets the request URI. */ + /** Sets the request URI. */ + var url: String? = null + + /** + * Gets the request headers. + * + * @return The response headers. May return `null` if no headers are set. + */ + var headers: MutableMap<String, String>? = null + private set + + /** Creates a new request. */ + constructor() + + /** + * Creates a new request. + * + * @param url Request URI. + */ + constructor(url: String?) { + this.url = url + } + + /** + * Sets a request header. + * + * @param name Header name. + * @param value Header value. + */ + fun setHeader(name: String, value: String) { + if (headers == null) { + headers = mutableMapOf() + } + headers?.put(name, value) + } + } + + class PostRequest : GetRequest { + var content: String? = null + + constructor() + constructor(url: String?) : super(url) + } + + /** A HTTP exception. */ + class HttpException( + /** Gets the HTTP response status code. */ + val statusCode: Int, + /** Gets the HTTP response reason phrase. */ + val reasonPhrase: String + ) : IOException("$statusCode $reasonPhrase") + + /** An interface for URL rewriting. */ + interface UrlRewriter { + fun rewrite(url: String): String + } +} diff --git a/src/com/android/quicksearchbox/util/JavaNetHttpHelper.java b/src/com/android/quicksearchbox/util/JavaNetHttpHelper.java deleted file mode 100644 index 5a0c8b9..0000000 --- a/src/com/android/quicksearchbox/util/JavaNetHttpHelper.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -import android.os.Build; -import android.util.Log; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; - -/** - * Simple HTTP client API. - */ -public class JavaNetHttpHelper implements HttpHelper { - private static final String TAG = "QSB.JavaNetHttpHelper"; - private static final boolean DBG = false; - - private static final int BUFFER_SIZE = 1024 * 4; - private static final String USER_AGENT_HEADER = "User-Agent"; - private static final String DEFAULT_CHARSET = "UTF-8"; - - private int mConnectTimeout; - private int mReadTimeout; - private final String mUserAgent; - private final HttpHelper.UrlRewriter mRewriter; - - /** - * Creates a new HTTP helper. - * - * @param rewriter URI rewriter - * @param userAgent User agent string, e.g. "MyApp/1.0". - */ - public JavaNetHttpHelper(UrlRewriter rewriter, String userAgent) { - mUserAgent = userAgent + " (" + Build.DEVICE + " " + Build.ID + ")"; - mRewriter = rewriter; - } - - /** - * Executes a GET request and returns the response content. - * - * @param request Request. - * @return The response content. This is the empty string if the response - * contained no content. - * @throws IOException If an IO error occurs. - * @throws HttpException If the response has a status code other than 200. - */ - public String get(GetRequest request) throws IOException, HttpException { - return get(request.getUrl(), request.getHeaders()); - } - - /** - * Executes a GET request and returns the response content. - * - * @param url Request URI. - * @param requestHeaders Request headers. - * @return The response content. This is the empty string if the response - * contained no content. - * @throws IOException If an IO error occurs. - * @throws HttpException If the response has a status code other than 200. - */ - public String get(String url, Map<String,String> requestHeaders) - throws IOException, HttpException { - HttpURLConnection c = null; - try { - c = createConnection(url, requestHeaders); - c.setRequestMethod("GET"); - c.connect(); - return getResponseFrom(c); - } finally { - if (c != null) { - c.disconnect(); - } - } - } - - @Override - public String post(PostRequest request) throws IOException, HttpException { - return post(request.getUrl(), request.getHeaders(), request.getContent()); - } - - public String post(String url, Map<String,String> requestHeaders, String content) - throws IOException, HttpException { - HttpURLConnection c = null; - try { - if (requestHeaders == null) { - requestHeaders = new HashMap<String, String>(); - } - requestHeaders.put("Content-Length", - Integer.toString(content == null ? 0 : content.length())); - c = createConnection(url, requestHeaders); - c.setDoOutput(content != null); - c.setRequestMethod("POST"); - c.connect(); - if (content != null) { - OutputStreamWriter writer = new OutputStreamWriter(c.getOutputStream()); - writer.write(content); - writer.close(); - } - return getResponseFrom(c); - } finally { - if (c != null) { - c.disconnect(); - } - } - } - - private HttpURLConnection createConnection(String url, Map<String, String> headers) - throws IOException, HttpException { - URL u = new URL(mRewriter.rewrite(url)); - if (DBG) Log.d(TAG, "URL=" + url + " rewritten='" + u + "'"); - HttpURLConnection c = (HttpURLConnection) u.openConnection(); - if (headers != null) { - for (Map.Entry<String,String> e : headers.entrySet()) { - String name = e.getKey(); - String value = e.getValue(); - if (DBG) Log.d(TAG, " " + name + ": " + value); - c.addRequestProperty(name, value); - } - } - c.addRequestProperty(USER_AGENT_HEADER, mUserAgent); - if (mConnectTimeout != 0) { - c.setConnectTimeout(mConnectTimeout); - } - if (mReadTimeout != 0) { - c.setReadTimeout(mReadTimeout); - } - return c; - } - - private String getResponseFrom(HttpURLConnection c) throws IOException, HttpException { - if (c.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new HttpException(c.getResponseCode(), c.getResponseMessage()); - } - if (DBG) { - Log.d(TAG, "Content-Type: " + c.getContentType() + " (assuming " + - DEFAULT_CHARSET + ")"); - } - BufferedReader reader = new BufferedReader( - new InputStreamReader(c.getInputStream(), DEFAULT_CHARSET)); - StringBuilder string = new StringBuilder(); - char[] chars = new char[BUFFER_SIZE]; - int bytes; - while ((bytes = reader.read(chars)) != -1) { - string.append(chars, 0, bytes); - } - return string.toString(); - } - - public void setConnectTimeout(int timeoutMillis) { - mConnectTimeout = timeoutMillis; - } - - public void setReadTimeout(int timeoutMillis) { - mReadTimeout = timeoutMillis; - } - - /** - * A Url rewriter that does nothing, i.e., returns the - * url that is passed to it. - */ - public static class PassThroughRewriter implements UrlRewriter { - @Override - public String rewrite(String url) { - return url; - } - } -} diff --git a/src/com/android/quicksearchbox/util/JavaNetHttpHelper.kt b/src/com/android/quicksearchbox/util/JavaNetHttpHelper.kt new file mode 100644 index 0000000..06a45d1 --- /dev/null +++ b/src/com/android/quicksearchbox/util/JavaNetHttpHelper.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import android.os.Build +import android.util.Log +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader +import java.io.OutputStreamWriter +import java.net.HttpURLConnection +import java.net.URL + +/** Simple HTTP client API. */ +class JavaNetHttpHelper(rewriter: HttpHelper.UrlRewriter, userAgent: String) : HttpHelper { + private var mConnectTimeout = 0 + private var mReadTimeout = 0 + private val mUserAgent: String + private val mRewriter: HttpHelper.UrlRewriter + + /** + * Executes a GET request and returns the response content. + * + * @param request Request. + * @return The response content. This is the empty string if the response contained no content. + * @throws IOException If an IO error occurs. + * @throws HttpException If the response has a status code other than 200. + */ + @Throws(IOException::class, HttpHelper.HttpException::class) + override operator fun get(request: HttpHelper.GetRequest?): String? { + return get(request?.url, request?.headers) + } + + /** + * Executes a GET request and returns the response content. + * + * @param url Request URI. + * @param requestHeaders Request headers. + * @return The response content. This is the empty string if the response contained no content. + * @throws IOException If an IO error occurs. + * @throws HttpException If the response has a status code other than 200. + */ + @Throws(IOException::class, HttpHelper.HttpException::class) + override operator fun get(url: String?, requestHeaders: MutableMap<String, String>?): String? { + var c: HttpURLConnection? = null + return try { + c = createConnection(url!!, requestHeaders) + c.setRequestMethod("GET") + c.connect() + getResponseFrom(c) + } finally { + if (c != null) { + c.disconnect() + } + } + } + + @Override + @Throws(IOException::class, HttpHelper.HttpException::class) + override fun post(request: HttpHelper.PostRequest?): String? { + return post(request?.url, request?.headers, request?.content) + } + + @Throws(IOException::class, HttpHelper.HttpException::class) + override fun post( + url: String?, + requestHeaders: MutableMap<String, String>?, + content: String? + ): String? { + var mRequestHeaders: MutableMap<String, String>? = requestHeaders + var c: HttpURLConnection? = null + return try { + if (mRequestHeaders == null) { + mRequestHeaders = mutableMapOf() + } + mRequestHeaders.put("Content-Length", Integer.toString(content?.length ?: 0)) + c = createConnection(url!!, mRequestHeaders) + c.setDoOutput(content != null) + c.setRequestMethod("POST") + c.connect() + if (content != null) { + val writer = OutputStreamWriter(c.getOutputStream()) + writer.write(content) + writer.close() + } + getResponseFrom(c) + } finally { + if (c != null) { + c.disconnect() + } + } + } + + @Throws(IOException::class, HttpHelper.HttpException::class) + private fun createConnection(url: String, headers: Map<String, String>?): HttpURLConnection { + val u = URL(mRewriter.rewrite(url)) + if (DBG) Log.d(TAG, "URL=$url rewritten='$u'") + val c: HttpURLConnection = u.openConnection() as HttpURLConnection + if (headers != null) { + for (e in headers.entries) { + val name: String = e.key + val value: String = e.value + if (DBG) Log.d(TAG, " $name: $value") + c.addRequestProperty(name, value) + } + } + c.addRequestProperty(USER_AGENT_HEADER, mUserAgent) + if (mConnectTimeout != 0) { + c.setConnectTimeout(mConnectTimeout) + } + if (mReadTimeout != 0) { + c.setReadTimeout(mReadTimeout) + } + return c + } + + @Throws(IOException::class, HttpHelper.HttpException::class) + private fun getResponseFrom(c: HttpURLConnection): String { + if (c.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw HttpHelper.HttpException(c.getResponseCode(), c.getResponseMessage()) + } + if (DBG) { + Log.d( + TAG, + "Content-Type: " + c.getContentType().toString() + " (assuming " + DEFAULT_CHARSET + ")" + ) + } + val reader = BufferedReader(InputStreamReader(c.getInputStream(), DEFAULT_CHARSET)) + val string: StringBuilder = StringBuilder() + val chars = CharArray(BUFFER_SIZE) + var bytes: Int + while (reader.read(chars).also { bytes = it } != -1) { + string.append(chars, 0, bytes) + } + return string.toString() + } + + override fun setConnectTimeout(timeoutMillis: Int) { + mConnectTimeout = timeoutMillis + } + + override fun setReadTimeout(timeoutMillis: Int) { + mReadTimeout = timeoutMillis + } + + /** A Url rewriter that does nothing, i.e., returns the url that is passed to it. */ + class PassThroughRewriter : HttpHelper.UrlRewriter { + @Override + override fun rewrite(url: String): String { + return url + } + } + + companion object { + private const val TAG = "QSB.JavaNetHttpHelper" + private const val DBG = false + private const val BUFFER_SIZE = 1024 * 4 + private const val USER_AGENT_HEADER = "User-Agent" + private const val DEFAULT_CHARSET = "UTF-8" + } + + /** + * Creates a new HTTP helper. + * + * @param rewriter URI rewriter + * @param userAgent User agent string, e.g. "MyApp/1.0". + */ + init { + mUserAgent = userAgent + " (" + Build.DEVICE + " " + Build.ID + ")" + mRewriter = rewriter + } +} diff --git a/src/com/android/quicksearchbox/util/LevenshteinDistance.java b/src/com/android/quicksearchbox/util/LevenshteinDistance.java deleted file mode 100644 index ad86d41..0000000 --- a/src/com/android/quicksearchbox/util/LevenshteinDistance.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -/** - * This class represents the matrix used in the Levenshtein distance algorithm, together - * with the algorithm itself which operates on the matrix. - * - * We also track of the individual operations applied to transform the source string into the - * target string so we can trace the path taken through the matrix afterwards, in order to - * perform the formatting as required. - */ -public class LevenshteinDistance { - public static final int EDIT_DELETE = 0; - public static final int EDIT_INSERT = 1; - public static final int EDIT_REPLACE = 2; - public static final int EDIT_UNCHANGED = 3; - - private final Token[] mSource; - private final Token[] mTarget; - private final int[][] mEditTypeTable; - private final int[][] mDistanceTable; - - public LevenshteinDistance(Token[] source, Token[] target) { - final int sourceSize = source.length; - final int targetSize = target.length; - final int[][] editTab = new int[sourceSize+1][targetSize+1]; - final int[][] distTab = new int[sourceSize+1][targetSize+1]; - editTab[0][0] = EDIT_UNCHANGED; - distTab[0][0] = 0; - for (int i = 1; i <= sourceSize; ++i) { - editTab[i][0] = EDIT_DELETE; - distTab[i][0] = i; - } - for (int i = 1; i <= targetSize; ++i) { - editTab[0][i] = EDIT_INSERT; - distTab[0][i] = i; - } - mEditTypeTable = editTab; - mDistanceTable = distTab; - mSource = source; - mTarget = target; - } - - /** - * Implementation of Levenshtein distance algorithm. - * - * @return The Levenshtein distance. - */ - public int calculate() { - final Token[] src = mSource; - final Token[] trg = mTarget; - final int sourceLen = src.length; - final int targetLen = trg.length; - final int[][] distTab = mDistanceTable; - final int[][] editTab = mEditTypeTable; - for (int s = 1; s <= sourceLen; ++s) { - Token sourceToken = src[s-1]; - for (int t = 1; t <= targetLen; ++t) { - Token targetToken = trg[t-1]; - int cost = sourceToken.prefixOf(targetToken) ? 0 : 1; - - int distance = distTab[s-1][t] + 1; - int type = EDIT_DELETE; - - int d = distTab[s][t - 1]; - if (d + 1 < distance ) { - distance = d + 1; - type = EDIT_INSERT; - } - - d = distTab[s - 1][t - 1]; - if (d + cost < distance) { - distance = d + cost; - type = cost == 0 ? EDIT_UNCHANGED : EDIT_REPLACE; - } - distTab[s][t] = distance; - editTab[s][t] = type; - } - } - return distTab[sourceLen][targetLen]; - } - - /** - * Gets the list of operations which were applied to each target token; {@link #calculate} must - * have been called on this object before using this method. - * @return A list of {@link EditOperation}s indicating the origin of each token in the target - * string. The position of the token indicates the position in the source string of the - * token that was unchanged/replaced, or the position in the source after which a target - * token was inserted. - */ - public EditOperation[] getTargetOperations() { - final int trgLen = mTarget.length; - final EditOperation[] ops = new EditOperation[trgLen]; - int targetPos = trgLen; - int sourcePos = mSource.length; - final int[][] editTab = mEditTypeTable; - while (targetPos > 0) { - int editType = editTab[sourcePos][targetPos]; - switch (editType) { - case LevenshteinDistance.EDIT_DELETE: - sourcePos--; - break; - case LevenshteinDistance.EDIT_INSERT: - targetPos--; - ops[targetPos] = new EditOperation(editType, sourcePos); - break; - case LevenshteinDistance.EDIT_UNCHANGED: - case LevenshteinDistance.EDIT_REPLACE: - targetPos--; - sourcePos--; - ops[targetPos] = new EditOperation(editType, sourcePos); - break; - } - } - - return ops; - } - - public static final class EditOperation { - private final int mType; - private final int mPosition; - public EditOperation(int type, int position) { - mType = type; - mPosition = position; - } - public int getType() { - return mType; - } - public int getPosition() { - return mPosition; - } - } - - public static final class Token implements CharSequence { - private final char[] mContainer; - public final int mStart; - public final int mEnd; - - public Token(char[] container, int start, int end) { - mContainer = container; - mStart = start; - mEnd = end; - } - - public int length() { - return mEnd - mStart; - } - - @Override - public String toString() { - // used in tests only. - return subSequence(0, length()); - } - - public boolean prefixOf(final Token that) { - final int len = length(); - if (len > that.length()) return false; - final int thisStart = mStart; - final int thatStart = that.mStart; - final char[] thisContainer = mContainer; - final char[] thatContainer = that.mContainer; - for (int i = 0; i < len; ++i) { - if (thisContainer[thisStart + i] != thatContainer[thatStart + i]) { - return false; - } - } - return true; - } - - public char charAt(int index) { - return mContainer[index + mStart]; - } - - public String subSequence(int start, int end) { - return new String(mContainer, mStart + start, length()); - } - - } -} diff --git a/src/com/android/quicksearchbox/util/LevenshteinDistance.kt b/src/com/android/quicksearchbox/util/LevenshteinDistance.kt new file mode 100644 index 0000000..f6035c7 --- /dev/null +++ b/src/com/android/quicksearchbox/util/LevenshteinDistance.kt @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +/** + * This class represents the matrix used in the Levenshtein distance algorithm, together with the + * algorithm itself which operates on the matrix. + * + * We also track of the individual operations applied to transform the source string into the target + * string so we can trace the path taken through the matrix afterwards, in order to perform the + * formatting as required. + */ +class LevenshteinDistance(source: Array<Token?>?, target: Array<Token?>?) { + private val mSource: Array<Token?>? + private val mTarget: Array<Token?>? + private val mEditTypeTable: Array<IntArray> + private val mDistanceTable: Array<IntArray> + + /** + * Implementation of Levenshtein distance algorithm. + * + * @return The Levenshtein distance. + */ + fun calculate(): Int { + val src = mSource + val trg = mTarget + val sourceLen = src!!.size + val targetLen = trg!!.size + val distTab = mDistanceTable + val editTab = mEditTypeTable + for (s in 1..sourceLen) { + val sourceToken = src[s - 1] + for (t in 1..targetLen) { + val targetToken = trg[t - 1] + val cost = if (sourceToken?.prefixOf(targetToken) == true) 0 else 1 + var distance = distTab[s - 1][t] + 1 + var type: Int = EDIT_DELETE + var d = distTab[s][t - 1] + if (d + 1 < distance) { + distance = d + 1 + type = EDIT_INSERT + } + d = distTab[s - 1][t - 1] + if (d + cost < distance) { + distance = d + cost + type = if (cost == 0) EDIT_UNCHANGED else EDIT_REPLACE + } + distTab[s][t] = distance + editTab[s][t] = type + } + } + return distTab[sourceLen][targetLen] + } + + /** + * Gets the list of operations which were applied to each target token; [.calculate] must have + * been called on this object before using this method. + * @return A list of [EditOperation]s indicating the origin of each token in the target string. + * The position of the token indicates the position in the source string of the token that was + * unchanged/replaced, or the position in the source after which a target token was inserted. + */ + val targetOperations: Array<EditOperation?> + get() { + val trgLen = mTarget!!.size + val ops = arrayOfNulls<EditOperation>(trgLen) + var targetPos = trgLen + var sourcePos = mSource!!.size + val editTab = mEditTypeTable + while (targetPos > 0) { + val editType = editTab[sourcePos][targetPos] + when (editType) { + EDIT_DELETE -> sourcePos-- + EDIT_INSERT -> { + targetPos-- + ops[targetPos] = EditOperation(editType, sourcePos) + } + EDIT_UNCHANGED, + EDIT_REPLACE -> { + targetPos-- + sourcePos-- + ops[targetPos] = EditOperation(editType, sourcePos) + } + } + } + return ops + } + + class EditOperation(val type: Int, val position: Int) + class Token(private val mContainer: CharArray, val mStart: Int, val mEnd: Int) : CharSequence { + @get:Override + override val length: Int + get() = mEnd - mStart + + @Override + override fun toString(): String { + // used in tests only. + return subSequence(0, length) + } + + fun prefixOf(that: Token?): Boolean { + val len = length + if (len > that!!.length) return false + val thisStart = mStart + val thatStart: Int = that.mStart + val thisContainer = mContainer + val thatContainer: CharArray = that.mContainer + for (i in 0 until len) { + if (thisContainer[thisStart + i] != thatContainer[thatStart + i]) { + return false + } + } + return true + } + + override fun get(index: Int): Char { + return mContainer[index + mStart] + } + + override fun subSequence(startIndex: Int, endIndex: Int): String { + return String(mContainer, mStart + startIndex, length) + } + } + + companion object { + const val EDIT_DELETE = 0 + const val EDIT_INSERT = 1 + const val EDIT_REPLACE = 2 + const val EDIT_UNCHANGED = 3 + } + + init { + val sourceSize = source!!.size + val targetSize = target!!.size + val editTab = Array(sourceSize + 1) { IntArray(targetSize + 1) } + val distTab = Array(sourceSize + 1) { IntArray(targetSize + 1) } + editTab[0][0] = EDIT_UNCHANGED + distTab[0][0] = 0 + for (i in 1..sourceSize) { + editTab[i][0] = EDIT_DELETE + distTab[i][0] = i + } + for (i in 1..targetSize) { + editTab[0][i] = EDIT_INSERT + distTab[0][i] = i + } + mEditTypeTable = editTab + mDistanceTable = distTab + mSource = source + mTarget = target + } +} diff --git a/src/com/android/quicksearchbox/util/NamedTask.java b/src/com/android/quicksearchbox/util/NamedTask.kt index fa11267..5d6b1a5 100644 --- a/src/com/android/quicksearchbox/util/NamedTask.java +++ b/src/com/android/quicksearchbox/util/NamedTask.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * Copyright (C) 2022 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. @@ -13,14 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.quicksearchbox.util -package com.android.quicksearchbox.util; - -/** - * A task that has a name. - */ -public interface NamedTask extends Runnable { - - String getName(); - +/** A task that has a name. */ +interface NamedTask : Runnable { + val name: String? } diff --git a/src/com/android/quicksearchbox/util/NamedTaskExecutor.java b/src/com/android/quicksearchbox/util/NamedTaskExecutor.java deleted file mode 100644 index 67670af..0000000 --- a/src/com/android/quicksearchbox/util/NamedTaskExecutor.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox.util; - -/** - * Runs tasks that have a name tag. - */ -public interface NamedTaskExecutor { - - /** - * Schedules a task for execution. Implementations should not throw - * {@link java.util.concurrent.RejectedExecutionException} if the task - * cannot be run. They should drop it silently instead. - */ - void execute(NamedTask task); - - /** - * Stops any unstarted tasks from running. Implementations of this method must be - * idempotent. - */ - void cancelPendingTasks(); - - /** - * Shuts down this executor, freeing any resources that it owns. The executor - * may not be used after calling this method. Implementations of this method must be - * idempotent. - */ - void close(); - -} diff --git a/src/com/android/quicksearchbox/util/NamedTaskExecutor.kt b/src/com/android/quicksearchbox/util/NamedTaskExecutor.kt new file mode 100644 index 0000000..c955244 --- /dev/null +++ b/src/com/android/quicksearchbox/util/NamedTaskExecutor.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +/** Runs tasks that have a name tag. */ +interface NamedTaskExecutor { + /** + * Schedules a task for execution. Implementations should not throw + * [java.util.concurrent.RejectedExecutionException] if the task cannot be run. They should drop + * it silently instead. + */ + fun execute(task: NamedTask?) + + /** Stops any unstarted tasks from running. Implementations of this method must be idempotent. */ + fun cancelPendingTasks() + + /** + * Shuts down this executor, freeing any resources that it owns. The executor may not be used + * after calling this method. Implementations of this method must be idempotent. + */ + fun close() +} diff --git a/src/com/android/quicksearchbox/util/NoOpConsumer.java b/src/com/android/quicksearchbox/util/NoOpConsumer.java deleted file mode 100644 index bac138c..0000000 --- a/src/com/android/quicksearchbox/util/NoOpConsumer.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -import com.android.quicksearchbox.util.Consumer; - -/** - * A Consumer that does nothing with the objects it receives. - */ -public class NoOpConsumer<A> implements Consumer<A> { - public boolean consume(A result) { - // Tell the caller that we haven't taken ownership of this result. - return false; - } -} - diff --git a/src/com/android/quicksearchbox/util/NoOpConsumer.kt b/src/com/android/quicksearchbox/util/NoOpConsumer.kt new file mode 100644 index 0000000..36beb35 --- /dev/null +++ b/src/com/android/quicksearchbox/util/NoOpConsumer.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +/** A Consumer that does nothing with the objects it receives. */ +class NoOpConsumer<A> : Consumer<A> { + override fun consume(value: A): Boolean { + // Tell the caller that we haven't taken ownership of this result. + return false + } +} diff --git a/src/com/android/quicksearchbox/util/Now.java b/src/com/android/quicksearchbox/util/Now.java deleted file mode 100644 index 88328fd..0000000 --- a/src/com/android/quicksearchbox/util/Now.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -/** - * A {@link NowOrLater} object that is always ready now. - */ -public class Now<C> implements NowOrLater<C> { - - private final C mValue; - - public Now(C value) { - mValue = value; - } - - public void getLater(Consumer<? super C> consumer) { - consumer.consume(getNow()); - } - - public C getNow() { - return mValue; - } - - public boolean haveNow() { - return true; - } - -} diff --git a/src/com/android/quicksearchbox/util/Now.kt b/src/com/android/quicksearchbox/util/Now.kt new file mode 100644 index 0000000..fb7a82d --- /dev/null +++ b/src/com/android/quicksearchbox/util/Now.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +/** A [NowOrLater] object that is always ready now. */ +class Now<C>(override val now: C?) : NowOrLater<C?> { + override fun getLater(consumer: Consumer<in C?>?) { + consumer!!.consume(now) + } + + override fun haveNow(): Boolean { + return true + } +} diff --git a/src/com/android/quicksearchbox/util/NowOrLater.java b/src/com/android/quicksearchbox/util/NowOrLater.kt index 6029ef6..97a8ac7 100644 --- a/src/com/android/quicksearchbox/util/NowOrLater.java +++ b/src/com/android/quicksearchbox/util/NowOrLater.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2022 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. @@ -13,31 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.quicksearchbox.util; +package com.android.quicksearchbox.util /** * Interface for an object that may be constructed asynchronously. In cases when the object is ready * (on constructible) immediately, it provides synchronous access to it. Otherwise, the object can - * be sent to a {@link Consumer} later. + * be sent to a [Consumer] later. */ -public interface NowOrLater<C> { +interface NowOrLater<C> { + /** Indicates if the object is ready (or constructible synchronously). */ + fun haveNow(): Boolean - /** - * Indicates if the object is ready (or constructible synchronously). - */ - boolean haveNow(); - - /** - * Gets the object now. Should only be called if {@link #haveNow()} returns {@code true}, - * otherwise an {@link IllegalStateException} will be thrown. - */ - C getNow(); - - /** - * Request the object asynchronously. This can be called even if the object is ready now, in - * which case the callback may be made in context. The thread on which the consumer is called - * back depends on the implementation. - */ - void getLater(Consumer<? super C> consumer); + /** + * Gets the object now. Should only be called if [.haveNow] returns `true`, otherwise an + * [IllegalStateException] will be thrown. + */ + val now: C + /** + * Request the object asynchronously. This can be called even if the object is ready now, in which + * case the callback may be made in context. The thread on which the consumer is called back + * depends on the implementation. + */ + fun getLater(consumer: Consumer<in C>?) } diff --git a/src/com/android/quicksearchbox/util/NowOrLaterWrapper.java b/src/com/android/quicksearchbox/util/NowOrLaterWrapper.java deleted file mode 100644 index efe0901..0000000 --- a/src/com/android/quicksearchbox/util/NowOrLaterWrapper.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -/** - * {@link NowOrLater} class that converts from one type to another. - */ -public abstract class NowOrLaterWrapper<A, B> implements NowOrLater<B> { - - private final NowOrLater<A> mWrapped; - - public NowOrLaterWrapper(NowOrLater<A> wrapped) { - mWrapped = wrapped; - } - - public void getLater(final Consumer<? super B> consumer) { - mWrapped.getLater(new Consumer<A>(){ - public boolean consume(A value) { - return consumer.consume(get(value)); - }}); - } - - public B getNow() { - return get(mWrapped.getNow()); - } - - public boolean haveNow() { - return mWrapped.haveNow(); - } - - /** - * Perform the appropriate conversion. This will be called once for every call to - * {@link #getLater(Consumer)} or {@link #getNow()}. The thread that it's called on will depend - * on the behaviour of the wrapped object and the caller. - */ - public abstract B get(A value); - -} diff --git a/src/com/android/quicksearchbox/util/NowOrLaterWrapper.kt b/src/com/android/quicksearchbox/util/NowOrLaterWrapper.kt new file mode 100644 index 0000000..1f9de29 --- /dev/null +++ b/src/com/android/quicksearchbox/util/NowOrLaterWrapper.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +/** [NowOrLater] class that converts from one type to another. */ +abstract class NowOrLaterWrapper<A, B>(private val mWrapped: NowOrLater<A>) : NowOrLater<B> { + override fun getLater(consumer: Consumer<in B>?) { + mWrapped.getLater( + object : Consumer<A> { + override fun consume(value: A): Boolean { + return consumer!!.consume(get(value)) + } + } + ) + } + + override val now: B + get() = get(mWrapped.now) + + override fun haveNow(): Boolean { + return mWrapped.haveNow() + } + + /** + * Perform the appropriate conversion. This will be called once for every call to [.getLater] or + * [.getNow]. The thread that it's called on will depend on the behaviour of the wrapped object + * and the caller. + */ + abstract operator fun get(value: A): B +} diff --git a/src/com/android/quicksearchbox/util/PerNameExecutor.java b/src/com/android/quicksearchbox/util/PerNameExecutor.java deleted file mode 100644 index 3abd58f..0000000 --- a/src/com/android/quicksearchbox/util/PerNameExecutor.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - - -import java.util.HashMap; - -/** - * Uses a separate executor for each task name. - */ -public class PerNameExecutor implements NamedTaskExecutor { - - private final Factory<NamedTaskExecutor> mExecutorFactory; - private HashMap<String, NamedTaskExecutor> mExecutors; - - /** - * @param executorFactory Used to run the commands. - */ - public PerNameExecutor(Factory<NamedTaskExecutor> executorFactory) { - mExecutorFactory = executorFactory; - } - - public synchronized void cancelPendingTasks() { - if (mExecutors == null) return; - for (NamedTaskExecutor executor : mExecutors.values()) { - executor.cancelPendingTasks(); - } - } - - public synchronized void close() { - if (mExecutors == null) return; - for (NamedTaskExecutor executor : mExecutors.values()) { - executor.close(); - } - } - - public synchronized void execute(NamedTask task) { - if (mExecutors == null) { - mExecutors = new HashMap<String, NamedTaskExecutor>(); - } - String name = task.getName(); - NamedTaskExecutor executor = mExecutors.get(name); - if (executor == null) { - executor = mExecutorFactory.create(); - mExecutors.put(name, executor); - } - executor.execute(task); - } - -} diff --git a/src/com/android/quicksearchbox/util/PerNameExecutor.kt b/src/com/android/quicksearchbox/util/PerNameExecutor.kt new file mode 100644 index 0000000..cc0cf19 --- /dev/null +++ b/src/com/android/quicksearchbox/util/PerNameExecutor.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import kotlin.collections.HashMap + +/** + * Uses a separate executor for each task name. + * @param executorFactory Used to run the commands. + */ +class PerNameExecutor(private val mExecutorFactory: Factory<NamedTaskExecutor>) : + NamedTaskExecutor { + private var mExecutors: HashMap<String, NamedTaskExecutor>? = null + + @Synchronized + override fun cancelPendingTasks() { + if (mExecutors == null) return + for (executor in mExecutors!!.values) { + executor.cancelPendingTasks() + } + } + + @Synchronized + override fun close() { + if (mExecutors == null) return + for (executor in mExecutors!!.values) { + executor.close() + } + } + + @Synchronized + override fun execute(task: NamedTask?) { + if (mExecutors == null) { + mExecutors = HashMap<String, NamedTaskExecutor>() + } + val name: String? = task?.name + var executor: NamedTaskExecutor? = mExecutors?.get(name) + if (executor == null) { + executor = mExecutorFactory.create() + mExecutors?.put(name!!, executor) + } + executor.execute(task) + } +} diff --git a/src/com/android/quicksearchbox/util/PriorityThreadFactory.java b/src/com/android/quicksearchbox/util/PriorityThreadFactory.java deleted file mode 100644 index b75df0d..0000000 --- a/src/com/android/quicksearchbox/util/PriorityThreadFactory.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -import android.os.Process; - -import java.util.concurrent.ThreadFactory; - -/** - * A thread factory that creates threads with a given thread priority. - */ -public class PriorityThreadFactory implements ThreadFactory { - - private final int mPriority; - - /** - * Creates a new thread factory. - * - * @param priority The thread priority of the threads created by this factory. - * For values, see {@link Process}. - */ - public PriorityThreadFactory(int priority) { - mPriority = priority; - } - - public Thread newThread(Runnable r) { - return new Thread(r) { - @Override - public void run() { - Process.setThreadPriority(mPriority); - super.run(); - } - }; - } - -} diff --git a/src/com/android/quicksearchbox/util/PriorityThreadFactory.kt b/src/com/android/quicksearchbox/util/PriorityThreadFactory.kt new file mode 100644 index 0000000..bf4e8a3 --- /dev/null +++ b/src/com/android/quicksearchbox/util/PriorityThreadFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import android.os.Process +import java.util.concurrent.ThreadFactory + +/** + * A thread factory that creates threads with a given thread priority. + * @param priority The thread priority of the threads created by this factory. For values, see + * [Process]. + */ +class PriorityThreadFactory(private val mPriority: Int) : ThreadFactory { + override fun newThread(r: Runnable?): Thread { + return object : Thread(r) { + @Override + override fun run() { + Process.setThreadPriority(mPriority) + super.run() + } + } + } +} diff --git a/src/com/android/quicksearchbox/util/QuietlyCloseable.java b/src/com/android/quicksearchbox/util/QuietlyCloseable.kt index d442f8f..c6f5558 100644 --- a/src/com/android/quicksearchbox/util/QuietlyCloseable.java +++ b/src/com/android/quicksearchbox/util/QuietlyCloseable.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2022 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. @@ -13,16 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.quicksearchbox.util -package com.android.quicksearchbox.util; - -import java.io.Closeable; - -/** - * Interface for closeable objects whose close method doesn't throw IOExceptions. - */ -public interface QuietlyCloseable extends Closeable { - - void close(); +import java.io.Closeable +/** Interface for closeable objects whose close method doesn't throw IOExceptions. */ +interface QuietlyCloseable : Closeable { + override fun close() } diff --git a/src/com/android/quicksearchbox/util/SQLiteAsyncQuery.java b/src/com/android/quicksearchbox/util/SQLiteAsyncQuery.java deleted file mode 100644 index e4afecb..0000000 --- a/src/com/android/quicksearchbox/util/SQLiteAsyncQuery.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -import android.database.sqlite.SQLiteDatabase; - -/** - * Abstract helper base class for asynchronous SQLite queries. - * - * @param <A> The type of the result of the query. - */ -public abstract class SQLiteAsyncQuery<A> { - - /** - * Performs a query and computes some value from the result - * - * @param db A readable database. - * @return The result of the query. - */ - protected abstract A performQuery(SQLiteDatabase db); - - /** - * Runs the query against the database and passes the result to the consumer. - */ - public void run(SQLiteDatabase db, Consumer<A> consumer) { - A result = performQuery(db); - consumer.consume(result); - } -} diff --git a/src/com/android/quicksearchbox/util/SQLiteAsyncQuery.kt b/src/com/android/quicksearchbox/util/SQLiteAsyncQuery.kt new file mode 100644 index 0000000..d9cb85e --- /dev/null +++ b/src/com/android/quicksearchbox/util/SQLiteAsyncQuery.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import android.database.sqlite.SQLiteDatabase + +/** + * Abstract helper base class for asynchronous SQLite queries. + * + * @param <A> The type of the result of the query. </A> + */ +abstract class SQLiteAsyncQuery<A> { + /** + * Performs a query and computes some value from the result + * + * @param db A readable database. + * @return The result of the query. + */ + protected abstract fun performQuery(db: SQLiteDatabase?): A + + /** Runs the query against the database and passes the result to the consumer. */ + fun run(db: SQLiteDatabase?, consumer: Consumer<A>) { + val result = performQuery(db) + consumer.consume(result) + } +} diff --git a/src/com/android/quicksearchbox/util/SQLiteTransaction.java b/src/com/android/quicksearchbox/util/SQLiteTransaction.java deleted file mode 100644 index aa423cd..0000000 --- a/src/com/android/quicksearchbox/util/SQLiteTransaction.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2010 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.quicksearchbox.util; - -import android.database.sqlite.SQLiteDatabase; - -/** - * Abstract helper base class for SQLite write transactions. - */ -public abstract class SQLiteTransaction { - - /** - * Executes the statements that form the transaction. - * - * @param db A writable database. - * @return {@code true} if the transaction should be committed. - */ - protected abstract boolean performTransaction(SQLiteDatabase db); - - /** - * Runs the transaction against the database. The results are committed if - * {@link #performTransaction(SQLiteDatabase)} completes normally and returns {@code true}. - */ - public void run(SQLiteDatabase db) { - db.beginTransaction(); - try { - if (performTransaction(db)) { - db.setTransactionSuccessful(); - } - } finally { - db.endTransaction(); - } - } -} diff --git a/src/com/android/quicksearchbox/util/SQLiteTransaction.kt b/src/com/android/quicksearchbox/util/SQLiteTransaction.kt new file mode 100644 index 0000000..9932e2d --- /dev/null +++ b/src/com/android/quicksearchbox/util/SQLiteTransaction.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import android.database.sqlite.SQLiteDatabase + +/** Abstract helper base class for SQLite write transactions. */ +abstract class SQLiteTransaction { + /** + * Executes the statements that form the transaction. + * + * @param db A writable database. + * @return `true` if the transaction should be committed. + */ + protected abstract fun performTransaction(db: SQLiteDatabase?): Boolean + + /** + * Runs the transaction against the database. The results are committed if [.performTransaction] + * completes normally and returns `true`. + */ + fun run(db: SQLiteDatabase) { + db.beginTransaction() + try { + if (performTransaction(db)) { + db.setTransactionSuccessful() + } + } finally { + db.endTransaction() + } + } +} diff --git a/src/com/android/quicksearchbox/util/SingleThreadNamedTaskExecutor.java b/src/com/android/quicksearchbox/util/SingleThreadNamedTaskExecutor.java deleted file mode 100644 index be4012f..0000000 --- a/src/com/android/quicksearchbox/util/SingleThreadNamedTaskExecutor.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox.util; - -import android.util.Log; - -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; - -/** - * Executor that uses a single thread and an unbounded work queue. - */ -public class SingleThreadNamedTaskExecutor implements NamedTaskExecutor { - - private static final boolean DBG = false; - private static final String TAG = "QSB.SingleThreadNamedTaskExecutor"; - - private final LinkedBlockingQueue<NamedTask> mQueue; - private final Thread mWorker; - private volatile boolean mClosed = false; - - public SingleThreadNamedTaskExecutor(ThreadFactory threadFactory) { - mQueue = new LinkedBlockingQueue<NamedTask>(); - mWorker = threadFactory.newThread(new Worker()); - mWorker.start(); - } - - public void cancelPendingTasks() { - if (DBG) Log.d(TAG, "Cancelling " + mQueue.size() + " tasks: " + mWorker.getName()); - if (mClosed) { - throw new IllegalStateException("cancelPendingTasks() after close()"); - } - mQueue.clear(); - } - - public void close() { - mClosed = true; - mWorker.interrupt(); - mQueue.clear(); - } - - public void execute(NamedTask task) { - if (mClosed) { - throw new IllegalStateException("execute() after close()"); - } - mQueue.add(task); - } - - private class Worker implements Runnable { - public void run() { - try { - loop(); - } finally { - if (!mClosed) Log.w(TAG, "Worker exited before close"); - } - } - - private void loop() { - Thread currentThread = Thread.currentThread(); - String threadName = currentThread.getName(); - while (!mClosed) { - NamedTask task; - try { - task = mQueue.take(); - } catch (InterruptedException ex) { - continue; - } - currentThread.setName(threadName + " " + task.getName()); - try { - if (DBG) Log.d(TAG, "Running task " + task.getName()); - task.run(); - if (DBG) Log.d(TAG, "Task " + task.getName() + " complete"); - } catch (RuntimeException ex) { - Log.e(TAG, "Task " + task.getName() + " failed", ex); - } - } - } - } - - public static Factory<NamedTaskExecutor> factory(final ThreadFactory threadFactory) { - return new Factory<NamedTaskExecutor>() { - public NamedTaskExecutor create() { - return new SingleThreadNamedTaskExecutor(threadFactory); - } - }; - } - -} diff --git a/src/com/android/quicksearchbox/util/SingleThreadNamedTaskExecutor.kt b/src/com/android/quicksearchbox/util/SingleThreadNamedTaskExecutor.kt new file mode 100644 index 0000000..ffe0b6e --- /dev/null +++ b/src/com/android/quicksearchbox/util/SingleThreadNamedTaskExecutor.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import android.util.Log +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadFactory + +/** Executor that uses a single thread and an unbounded work queue. */ +class SingleThreadNamedTaskExecutor(threadFactory: ThreadFactory?) : NamedTaskExecutor { + private val mQueue: LinkedBlockingQueue<NamedTask> + private val mWorker: Thread + + @Volatile private var mClosed = false + override fun cancelPendingTasks() { + if (DBG) Log.d(TAG, "Cancelling " + mQueue.size.toString() + " tasks: " + mWorker.name) + if (mClosed) { + throw IllegalStateException("cancelPendingTasks() after close()") + } + mQueue.clear() + } + + override fun close() { + mClosed = true + mWorker.interrupt() + mQueue.clear() + } + + override fun execute(task: NamedTask?) { + if (mClosed) { + throw IllegalStateException("execute() after close()") + } + mQueue.add(task) + } + + private inner class Worker : Runnable { + override fun run() { + try { + loop() + } finally { + if (!mClosed) Log.w(TAG, "Worker exited before close") + } + } + + private fun loop() { + val currentThread: Thread = Thread.currentThread() + val threadName: String = currentThread.getName() + while (!mClosed) { + val task: NamedTask = + try { + mQueue.take() + } catch (ex: InterruptedException) { + continue + } + currentThread.setName(threadName + " " + task.name) + try { + if (DBG) Log.d(TAG, "Running task " + task.name) + task.run() + if (DBG) Log.d(TAG, "Task " + task.name + " complete") + } catch (ex: RuntimeException) { + Log.e(TAG, "Task " + task.name + " failed", ex) + } + } + } + } + + companion object { + private const val DBG = false + private const val TAG = "QSB.SingleThreadNamedTaskExecutor" + @JvmStatic + fun factory(threadFactory: ThreadFactory?): Factory<NamedTaskExecutor> { + return object : Factory<NamedTaskExecutor> { + override fun create(): NamedTaskExecutor { + return SingleThreadNamedTaskExecutor(threadFactory) + } + } + } + } + + init { + mQueue = LinkedBlockingQueue<NamedTask>() + mWorker = threadFactory!!.newThread(Worker()) + mWorker.start() + } +} diff --git a/src/com/android/quicksearchbox/util/Util.java b/src/com/android/quicksearchbox/util/Util.java deleted file mode 100644 index 373d2af..0000000 --- a/src/com/android/quicksearchbox/util/Util.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2009 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.quicksearchbox.util; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.net.Uri; -import android.util.Log; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * General utilities. - */ -public class Util { - - private static final String TAG = "QSB.Util"; - - public static <A> Set<A> setOfFirstN(List<A> list, int n) { - int end = Math.min(list.size(), n); - HashSet<A> set = new HashSet<A>(end); - for (int i = 0; i < end; i++) { - set.add(list.get(i)); - } - return set; - } - - public static Uri getResourceUri(Context packageContext, int res) { - try { - Resources resources = packageContext.getResources(); - return getResourceUri(resources, packageContext.getPackageName(), res); - } catch (Resources.NotFoundException e) { - Log.e(TAG, "Resource not found: " + res + " in " + packageContext.getPackageName()); - return null; - } - } - - public static Uri getResourceUri(Context context, ApplicationInfo appInfo, int res) { - try { - Resources resources = context.getPackageManager().getResourcesForApplication(appInfo); - return getResourceUri(resources, appInfo.packageName, res); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Resources not found for " + appInfo.packageName); - return null; - } catch (Resources.NotFoundException e) { - Log.e(TAG, "Resource not found: " + res + " in " + appInfo.packageName); - return null; - } - } - - private static Uri getResourceUri(Resources resources, String appPkg, int res) - throws Resources.NotFoundException { - String resPkg = resources.getResourcePackageName(res); - String type = resources.getResourceTypeName(res); - String name = resources.getResourceEntryName(res); - return makeResourceUri(appPkg, resPkg, type, name); - } - - private static Uri makeResourceUri(String appPkg, String resPkg, String type, String name) { - Uri.Builder uriBuilder = new Uri.Builder(); - uriBuilder.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE); - uriBuilder.encodedAuthority(appPkg); - uriBuilder.appendEncodedPath(type); - if (!appPkg.equals(resPkg)) { - uriBuilder.appendEncodedPath(resPkg + ":" + name); - } else { - uriBuilder.appendEncodedPath(name); - } - return uriBuilder.build(); - } -} diff --git a/src/com/android/quicksearchbox/util/Util.kt b/src/com/android/quicksearchbox/util/Util.kt new file mode 100644 index 0000000..78b9e5e --- /dev/null +++ b/src/com/android/quicksearchbox/util/Util.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 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.quicksearchbox.util + +import android.content.ContentResolver +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.res.Resources +import android.net.Uri +import android.util.Log + +/** General utilities. */ +object Util { + + private const val TAG = "QSB.Util" + + fun <A> setOfFirstN(list: List<A>, n: Int): Set<A> { + val end: Int = Math.min(list.size, n) + val set: HashSet<A> = hashSetOf() + for (i in 0 until end) { + set.add(list[i]) + } + return set + } + + fun getResourceUri(packageContext: Context?, res: Int): Uri? { + return try { + val resources: Resources? = packageContext?.getResources() + getResourceUri(resources, packageContext?.getPackageName(), res) + } catch (e: Resources.NotFoundException) { + Log.e(TAG, "Resource not found: " + res + " in " + packageContext?.getPackageName()) + null + } + } + + fun getResourceUri(context: Context?, appInfo: ApplicationInfo?, res: Int): Uri? { + return try { + val resources: Resources? = + context?.getPackageManager()?.getResourcesForApplication(appInfo!!) + getResourceUri(resources, appInfo?.packageName, res) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(TAG, "Resources not found for " + appInfo?.packageName) + null + } catch (e: Resources.NotFoundException) { + Log.e(TAG, "Resource not found: " + res + " in " + appInfo?.packageName) + null + } + } + + @Throws(Resources.NotFoundException::class) + private fun getResourceUri(resources: Resources?, appPkg: String?, res: Int): Uri { + val resPkg: String? = resources?.getResourcePackageName(res) + val type: String? = resources?.getResourceTypeName(res) + val name: String? = resources?.getResourceEntryName(res) + return makeResourceUri(appPkg, resPkg, type, name) + } + + private fun makeResourceUri(appPkg: String?, resPkg: String?, type: String?, name: String?): Uri { + val uriBuilder: Uri.Builder = Uri.Builder() + uriBuilder.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + uriBuilder.encodedAuthority(appPkg) + uriBuilder.appendEncodedPath(type) + if (appPkg != resPkg) { + uriBuilder.appendEncodedPath("$resPkg:$name") + } else { + uriBuilder.appendEncodedPath(name) + } + return uriBuilder.build() + } +} diff --git a/tests/src/com/android/quicksearchbox/tests/CrashingIconProvider.java b/tests/src/com/android/quicksearchbox/tests/CrashingIconProvider.java index c2162e9..b42ff93 100644 --- a/tests/src/com/android/quicksearchbox/tests/CrashingIconProvider.java +++ b/tests/src/com/android/quicksearchbox/tests/CrashingIconProvider.java @@ -22,6 +22,8 @@ import android.net.Uri; import android.os.ParcelFileDescriptor; import android.util.Log; +import java.util.Arrays; + /** * A content provider that crashes when something is requested. */ @@ -46,7 +48,10 @@ public class CrashingIconProvider extends ContentProvider { @Override public int delete(Uri uri, String selection, String[] selectionArgs) { - if (DBG) Log.d(TAG, "delete(" + uri + ", " + selection + ", " + selectionArgs + ")"); + if (DBG) { + Log.d(TAG, "delete(" + uri + ", " + selection + ", " + + Arrays.toString(selectionArgs) + ")"); + } throw new UnsupportedOperationException(); } |