Snap for 10453563 from 1c5d34d5176f212b7abc0192fc724f88c75de622 to mainline-permission-release

Change-Id: I1ed59c597c8c36347db0377c33feae629a297470
diff --git a/Android.bp b/Android.bp
index 6f26bf0..cf84788 100644
--- a/Android.bp
+++ b/Android.bp
@@ -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 @@
 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
diff --git a/BUILD b/BUILD
deleted file mode 100644
index 76b0c0c..0000000
--- a/BUILD
+++ /dev/null
@@ -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",
-    ],
-)
diff --git a/OWNERS b/OWNERS
index ed1d60a..8bf72f2 100644
--- a/OWNERS
+++ b/OWNERS
@@ -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/SourceResult.java b/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.kt
similarity index 65%
copy from src/com/android/quicksearchbox/SourceResult.java
copy to src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.kt
index 20ea48f..5e00434 100644
--- a/src/com/android/quicksearchbox/SourceResult.java
+++ b/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.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,8 @@
  * 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();
-
-}
+/** 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.java b/src/com/android/quicksearchbox/ResultFilter.java
deleted file mode 100644
index 76d88b6..0000000
--- a/src/com/android/quicksearchbox/ResultFilter.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;
-
-/**
- * {@link SuggestionFilter} that accepts only results (not web suggestions).
- */
-public class ResultFilter implements SuggestionFilter {
-
-    public ResultFilter() {
-    }
-
-    public boolean accept(Suggestion s) {
-        return !s.isWebSearchSuggestion();
-    }
-
-}
diff --git a/src/com/android/quicksearchbox/SourceResult.java b/src/com/android/quicksearchbox/ResultFilter.kt
similarity index 65%
copy from src/com/android/quicksearchbox/SourceResult.java
copy to src/com/android/quicksearchbox/ResultFilter.kt
index 20ea48f..27b03dd 100644
--- a/src/com/android/quicksearchbox/SourceResult.java
+++ b/src/com/android/quicksearchbox/ResultFilter.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,11 @@
  * 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();
-
+/** [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/util/Factory.java b/src/com/android/quicksearchbox/SourceResult.kt
similarity index 71%
copy from src/com/android/quicksearchbox/util/Factory.java
copy to src/com/android/quicksearchbox/SourceResult.kt
index 8aebe5c..b5baae3 100644
--- a/src/com/android/quicksearchbox/util/Factory.java
+++ b/src/com/android/quicksearchbox/SourceResult.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,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.quicksearchbox
 
-package com.android.quicksearchbox.util;
-
-public interface Factory<A> {
-
-    A create();
-
+/** 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/SuggestionExtras.kt b/src/com/android/quicksearchbox/SuggestionExtras.kt
new file mode 100644
index 0000000..583b7b5
--- /dev/null
+++ b/src/com/android/quicksearchbox/SuggestionExtras.kt
@@ -0,0 +1,30 @@
+/*
+ * 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 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>
+
+  /** @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/SuggestionNonFormatter.java b/src/com/android/quicksearchbox/SuggestionNonFormatter.java
deleted file mode 100644
index d7dc0bd..0000000
--- a/src/com/android/quicksearchbox/SuggestionNonFormatter.java
+++ /dev/null
@@ -1,34 +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;
-
-
-/**
- * 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;
-    }
-
-}
diff --git a/src/com/android/quicksearchbox/SuggestionNonFormatter.kt b/src/com/android/quicksearchbox/SuggestionNonFormatter.kt
new file mode 100644
index 0000000..d473d74
--- /dev/null
+++ b/src/com/android/quicksearchbox/SuggestionNonFormatter.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
+
+/** 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
similarity index 62%
rename from src/com/android/quicksearchbox/util/Consumer.java
rename to 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
similarity index 79%
rename from src/com/android/quicksearchbox/util/Factory.java
rename to 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.java
deleted file mode 100644
index fa11267..0000000
--- a/src/com/android/quicksearchbox/util/NamedTask.java
+++ /dev/null
@@ -1,26 +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;
-
-/**
- * A task that has a name.
- */
-public interface NamedTask extends Runnable {
-
-    String getName();
-
-}
diff --git a/src/com/android/quicksearchbox/util/Factory.java b/src/com/android/quicksearchbox/util/NamedTask.kt
similarity index 75%
copy from src/com/android/quicksearchbox/util/Factory.java
copy to src/com/android/quicksearchbox/util/NamedTask.kt
index 8aebe5c..5d6b1a5 100644
--- a/src/com/android/quicksearchbox/util/Factory.java
+++ b/src/com/android/quicksearchbox/util/NamedTask.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,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;
-
-public interface Factory<A> {
-
-    A create();
-
+/** 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/SourceResult.java b/src/com/android/quicksearchbox/util/NoOpConsumer.kt
similarity index 62%
copy from src/com/android/quicksearchbox/SourceResult.java
copy to src/com/android/quicksearchbox/util/NoOpConsumer.kt
index 20ea48f..36beb35 100644
--- a/src/com/android/quicksearchbox/SourceResult.java
+++ b/src/com/android/quicksearchbox/util/NoOpConsumer.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.
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.quicksearchbox;
+package com.android.quicksearchbox.util
 
-/**
- * The result of getting suggestions from a single source.
- */
-public interface SourceResult extends SuggestionCursor {
-
-    Source getSource();
-
+/** 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/Factory.java b/src/com/android/quicksearchbox/util/Now.kt
similarity index 62%
copy from src/com/android/quicksearchbox/util/Factory.java
copy to src/com/android/quicksearchbox/util/Now.kt
index 8aebe5c..fb7a82d 100644
--- a/src/com/android/quicksearchbox/util/Factory.java
+++ b/src/com/android/quicksearchbox/util/Now.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.
@@ -14,10 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.quicksearchbox.util;
+package com.android.quicksearchbox.util
 
-public interface Factory<A> {
+/** 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)
+  }
 
-    A create();
-
+  override fun haveNow(): Boolean {
+    return true
+  }
 }
diff --git a/src/com/android/quicksearchbox/util/NowOrLater.java b/src/com/android/quicksearchbox/util/NowOrLater.java
deleted file mode 100644
index 6029ef6..0000000
--- a/src/com/android/quicksearchbox/util/NowOrLater.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;
-
-/**
- * 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.
- */
-public interface NowOrLater<C> {
-
-    /**
-     * 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);
-
-}
diff --git a/src/com/android/quicksearchbox/util/NowOrLater.kt b/src/com/android/quicksearchbox/util/NowOrLater.kt
new file mode 100644
index 0000000..97a8ac7
--- /dev/null
+++ b/src/com/android/quicksearchbox/util/NowOrLater.kt
@@ -0,0 +1,39 @@
+/*
+ * 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
+
+/**
+ * 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 [Consumer] later.
+ */
+interface NowOrLater<C> {
+  /** Indicates if the object is ready (or constructible synchronously). */
+  fun haveNow(): Boolean
+
+  /**
+   * 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.java
deleted file mode 100644
index d442f8f..0000000
--- a/src/com/android/quicksearchbox/util/QuietlyCloseable.java
+++ /dev/null
@@ -1,28 +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.Closeable;
-
-/**
- * Interface for closeable objects whose close method doesn't throw IOExceptions.
- */
-public interface QuietlyCloseable extends Closeable {
-
-    void close();
-
-}
diff --git a/src/com/android/quicksearchbox/SourceResult.java b/src/com/android/quicksearchbox/util/QuietlyCloseable.kt
similarity index 67%
rename from src/com/android/quicksearchbox/SourceResult.java
rename to src/com/android/quicksearchbox/util/QuietlyCloseable.kt
index 20ea48f..c6f5558 100644
--- a/src/com/android/quicksearchbox/SourceResult.java
+++ b/src/com/android/quicksearchbox/util/QuietlyCloseable.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,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.quicksearchbox.util
 
-package com.android.quicksearchbox;
+import java.io.Closeable
 
-/**
- * The result of getting suggestions from a single source.
- */
-public interface SourceResult extends SuggestionCursor {
-
-    Source getSource();
-
+/** 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.os.ParcelFileDescriptor;
 import android.util.Log;
 
+import java.util.Arrays;
+
 /**
  * A content provider that crashes when something is requested.
  */
@@ -46,7 +48,10 @@
 
     @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();
     }