From 21e60a9d43b4130b0e87aec2ddd2e096116f8fe5 Mon Sep 17 00:00:00 2001 From: Hyunyoung Song Date: Fri, 20 Nov 2020 20:27:54 -0800 Subject: [PATCH] Add Initial SearchUIManager Bug:162234997 Test: builds, locally ran cts tests CTS tests: ag/13129468 [Meeting notes with Svet/Sunny/Winson] go/aaplus-search-api [Design diagrams] go/aaplus-search-supl Change-Id: I4589cd411e124dc9d7ea132a71651f6b9f9656ae --- core/api/system-current.txt | 146 ++++++ core/api/system-lint-baseline.txt | 10 + .../android/app/SystemServiceRegistry.java | 11 + .../android/app/search/ISearchCallback.aidl | 27 ++ .../android/app/search/ISearchUiManager.aidl | 40 ++ core/java/android/app/search/Query.aidl | 19 + core/java/android/app/search/Query.java | 102 +++++ .../java/android/app/search/SearchAction.java | 344 ++++++++++++++ .../android/app/search/SearchContext.aidl | 19 + .../android/app/search/SearchContext.java | 122 +++++ .../android/app/search/SearchSession.java | 224 +++++++++ .../android/app/search/SearchSessionId.aidl | 19 + .../android/app/search/SearchSessionId.java | 99 ++++ .../java/android/app/search/SearchTarget.aidl | 19 + .../java/android/app/search/SearchTarget.java | 425 ++++++++++++++++++ .../android/app/search/SearchTargetEvent.aidl | 19 + .../android/app/search/SearchTargetEvent.java | 267 +++++++++++ .../android/app/search/SearchUiManager.java | 50 +++ core/java/android/content/Context.java | 12 + .../service/search/ISearchUiService.aidl | 41 ++ .../service/search/SearchUiService.java | 193 ++++++++ core/res/AndroidManifest.xml | 5 + core/res/res/values/config.xml | 10 + core/res/res/values/symbols.xml | 1 + services/Android.bp | 2 + .../java/com/android/server/SystemServer.java | 8 + services/searchui/Android.bp | 13 + .../searchui/RemoteSearchUiService.java | 112 +++++ .../searchui/SearchUiManagerService.java | 173 +++++++ .../SearchUiManagerServiceShellCommand.java | 84 ++++ .../searchui/SearchUiPerUserService.java | 374 +++++++++++++++ 31 files changed, 2990 insertions(+) create mode 100644 core/java/android/app/search/ISearchCallback.aidl create mode 100644 core/java/android/app/search/ISearchUiManager.aidl create mode 100644 core/java/android/app/search/Query.aidl create mode 100644 core/java/android/app/search/Query.java create mode 100644 core/java/android/app/search/SearchAction.java create mode 100644 core/java/android/app/search/SearchContext.aidl create mode 100644 core/java/android/app/search/SearchContext.java create mode 100644 core/java/android/app/search/SearchSession.java create mode 100644 core/java/android/app/search/SearchSessionId.aidl create mode 100644 core/java/android/app/search/SearchSessionId.java create mode 100644 core/java/android/app/search/SearchTarget.aidl create mode 100644 core/java/android/app/search/SearchTarget.java create mode 100644 core/java/android/app/search/SearchTargetEvent.aidl create mode 100644 core/java/android/app/search/SearchTargetEvent.java create mode 100644 core/java/android/app/search/SearchUiManager.java create mode 100644 core/java/android/service/search/ISearchUiService.aidl create mode 100644 core/java/android/service/search/SearchUiService.java create mode 100644 services/searchui/Android.bp create mode 100644 services/searchui/java/com/android/server/searchui/RemoteSearchUiService.java create mode 100644 services/searchui/java/com/android/server/searchui/SearchUiManagerService.java create mode 100644 services/searchui/java/com/android/server/searchui/SearchUiManagerServiceShellCommand.java create mode 100644 services/searchui/java/com/android/server/searchui/SearchUiPerUserService.java diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 6085faca862d3..7bd9cd7509dcc 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -129,6 +129,7 @@ package android { field public static final String MANAGE_ONE_TIME_PERMISSION_SESSIONS = "android.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS"; field public static final String MANAGE_ROLE_HOLDERS = "android.permission.MANAGE_ROLE_HOLDERS"; field public static final String MANAGE_ROLLBACKS = "android.permission.MANAGE_ROLLBACKS"; + field public static final String MANAGE_SEARCH_UI = "android.permission.MANAGE_SEARCH_UI"; field public static final String MANAGE_SENSOR_PRIVACY = "android.permission.MANAGE_SENSOR_PRIVACY"; field public static final String MANAGE_SOUND_TRIGGER = "android.permission.MANAGE_SOUND_TRIGGER"; field public static final String MANAGE_SUBSCRIPTION_PLANS = "android.permission.MANAGE_SUBSCRIPTION_PLANS"; @@ -1385,6 +1386,137 @@ package android.app.role { } +package android.app.search { + + public final class Query implements android.os.Parcelable { + ctor public Query(@NonNull String, long, @Nullable android.os.Bundle); + method public int describeContents(); + method @Nullable public android.os.Bundle getExtras(); + method @NonNull public String getInput(); + method @NonNull public long getTimestamp(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public final class SearchAction implements android.os.Parcelable { + method public int describeContents(); + method @Nullable public CharSequence getContentDescription(); + method @Nullable public android.os.Bundle getExtras(); + method @Nullable public android.graphics.drawable.Icon getIcon(); + method @NonNull public String getId(); + method @Nullable public android.content.Intent getIntent(); + method @Nullable public android.app.PendingIntent getPendingIntent(); + method @Nullable public CharSequence getSubtitle(); + method @NonNull public CharSequence getTitle(); + method @Nullable public android.os.UserHandle getUserHandle(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class SearchAction.Builder { + ctor public SearchAction.Builder(@NonNull String, @NonNull String); + method @NonNull public android.app.search.SearchAction build(); + method @NonNull public android.app.search.SearchAction.Builder setContentDescription(@Nullable CharSequence); + method @NonNull public android.app.search.SearchAction.Builder setExtras(@Nullable android.os.Bundle); + method @NonNull public android.app.search.SearchAction.Builder setIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.search.SearchAction.Builder setIntent(@Nullable android.content.Intent); + method @NonNull public android.app.search.SearchAction.Builder setPendingIntent(@Nullable android.app.PendingIntent); + method @NonNull public android.app.search.SearchAction.Builder setSubtitle(@Nullable CharSequence); + method @NonNull public android.app.search.SearchAction.Builder setUserHandle(@Nullable android.os.UserHandle); + } + + public final class SearchContext implements android.os.Parcelable { + ctor public SearchContext(int, int, @Nullable android.os.Bundle); + method public int describeContents(); + method @Nullable public android.os.Bundle getExtras(); + method @Nullable public String getPackageName(); + method @NonNull public int getResultTypes(); + method @NonNull public int getTimeoutMillis(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public final class SearchSession implements java.lang.AutoCloseable { + method public void close(); + method public void destroy(); + method protected void finalize(); + method public void notifyEvent(@NonNull android.app.search.Query, @NonNull android.app.search.SearchTargetEvent); + method @Nullable public void query(@NonNull android.app.search.Query, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer>); + } + + public final class SearchSessionId implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public final class SearchTarget implements android.os.Parcelable { + method public int describeContents(); + method @Nullable public android.appwidget.AppWidgetProviderInfo getAppWidgetProviderInfo(); + method @Nullable public android.os.Bundle getExtras(); + method @NonNull public String getId(); + method @NonNull public String getLayoutType(); + method @NonNull public String getPackageName(); + method @NonNull public String getParentId(); + method public int getResultType(); + method public float getScore(); + method @Nullable public android.app.search.SearchAction getSearchAction(); + method @Nullable public android.content.pm.ShortcutInfo getShortcutInfo(); + method @Nullable public android.net.Uri getSliceUri(); + method @NonNull public android.os.UserHandle getUserHandle(); + method public boolean shouldHide(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class SearchTarget.Builder { + ctor public SearchTarget.Builder(int, @NonNull String, @NonNull String); + method @NonNull public android.app.search.SearchTarget build(); + method @NonNull public android.app.search.SearchTarget.Builder setAppWidgetProviderInfo(@NonNull android.appwidget.AppWidgetProviderInfo); + method @NonNull public android.app.search.SearchTarget.Builder setExtras(@Nullable android.os.Bundle); + method @NonNull public android.app.search.SearchTarget.Builder setPackageName(@NonNull String); + method @NonNull public android.app.search.SearchTarget.Builder setParentId(@NonNull String); + method @NonNull public android.app.search.SearchTarget.Builder setScore(float); + method @NonNull public android.app.search.SearchTarget.Builder setSearchAction(@Nullable android.app.search.SearchAction); + method @NonNull public android.app.search.SearchTarget.Builder setShortcutInfo(@NonNull android.content.pm.ShortcutInfo); + method @NonNull public android.app.search.SearchTarget.Builder setShouldHide(boolean); + method @NonNull public android.app.search.SearchTarget.Builder setSliceUri(@NonNull android.net.Uri); + method @NonNull public android.app.search.SearchTarget.Builder setUserHandle(@NonNull android.os.UserHandle); + } + + public final class SearchTargetEvent implements android.os.Parcelable { + method public int describeContents(); + method public int getAction(); + method public int getFlags(); + method @Nullable public String getLaunchLocation(); + method @NonNull public String getTargetId(); + method @NonNull public java.util.List getTargetIds(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int ACTION_DRAGNDROP = 7; // 0x7 + field public static final int ACTION_LAUNCH_KEYBOARD_FOCUS = 6; // 0x6 + field public static final int ACTION_LAUNCH_TOUCH = 5; // 0x5 + field public static final int ACTION_LONGPRESS = 4; // 0x4 + field public static final int ACTION_SURFACE_INVISIBLE = 8; // 0x8 + field public static final int ACTION_SURFACE_VISIBLE = 1; // 0x1 + field public static final int ACTION_TAP = 3; // 0x3 + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + field public static final int FLAG_IME_SHOWN = 1; // 0x1 + } + + public static final class SearchTargetEvent.Builder { + ctor public SearchTargetEvent.Builder(@NonNull String, int); + ctor public SearchTargetEvent.Builder(@NonNull java.util.List, int); + method @NonNull public android.app.search.SearchTargetEvent build(); + method @NonNull public android.app.search.SearchTargetEvent.Builder setFlags(int); + method @NonNull public android.app.search.SearchTargetEvent.Builder setLaunchLocation(@Nullable String); + } + + public final class SearchUiManager { + method @NonNull public android.app.search.SearchSession createSearchSession(@NonNull android.app.search.SearchContext); + } + +} + package android.app.time { public final class TimeManager { @@ -1805,6 +1937,7 @@ package android.content { field public static final String PERMISSION_SERVICE = "permission"; field public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; field public static final String ROLLBACK_SERVICE = "rollback"; + field public static final String SEARCH_UI_SERVICE = "search_ui"; field public static final String SECURE_ELEMENT_SERVICE = "secure_element"; field public static final String STATS_MANAGER = "stats"; field public static final String STATUS_BAR_SERVICE = "statusbar"; @@ -9589,6 +9722,19 @@ package android.service.resolver { } +package android.service.search { + + public abstract class SearchUiService extends android.app.Service { + ctor public SearchUiService(); + method @NonNull public final android.os.IBinder onBind(@NonNull android.content.Intent); + method public void onCreateSearchSession(@NonNull android.app.search.SearchContext, @NonNull android.app.search.SearchSessionId); + method @MainThread public abstract void onDestroy(@NonNull android.app.search.SearchSessionId); + method @MainThread public abstract void onNotifyEvent(@NonNull android.app.search.SearchSessionId, @NonNull android.app.search.Query, @NonNull android.app.search.SearchTargetEvent); + method @MainThread public abstract void onQuery(@NonNull android.app.search.SearchSessionId, @NonNull android.app.search.Query, @NonNull java.util.function.Consumer>); + } + +} + package android.service.settings.suggestions { public final class Suggestion implements android.os.Parcelable { diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt index a3fb06cbd92fd..8b3cee403bca4 100644 --- a/core/api/system-lint-baseline.txt +++ b/core/api/system-lint-baseline.txt @@ -19,6 +19,10 @@ GenericException: android.service.autofill.augmented.FillWindow#finalize(): +IntentBuilderName: android.app.search.SearchAction#getIntent(): + + + KotlinKeyword: android.app.Notification#when: @@ -248,3 +252,9 @@ SamShouldBeLast: android.view.accessibility.AccessibilityManager#addTouchExplora SamShouldBeLast: android.webkit.WebChromeClient#onShowFileChooser(android.webkit.WebView, android.webkit.ValueCallback, android.webkit.WebChromeClient.FileChooserParams): + + +UserHandleName: android.app.search.SearchAction.Builder#setUserHandle(android.os.UserHandle): + Method taking UserHandle should be named `doFooAsUser` or `queryFooForUser`, was `setUserHandle` +UserHandleName: android.app.search.SearchTarget.Builder#setUserHandle(android.os.UserHandle): + diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 5326bf37fad26..4afbfbf7143cf 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -32,6 +32,7 @@ import android.app.job.JobSchedulerFrameworkInitializer; import android.app.prediction.AppPredictionManager; import android.app.role.RoleControllerManager; import android.app.role.RoleManager; +import android.app.search.SearchUiManager; import android.app.slice.SliceManager; import android.app.time.TimeManager; import android.app.timedetector.TimeDetector; @@ -1172,6 +1173,16 @@ public final class SystemServiceRegistry { return null; }}); + registerService(Context.SEARCH_UI_SERVICE, SearchUiManager.class, + new CachedServiceFetcher() { + @Override + public SearchUiManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = ServiceManager.getService(Context.SEARCH_UI_SERVICE); + return b == null ? null : new SearchUiManager(ctx); + } + }); + registerService(Context.APP_PREDICTION_SERVICE, AppPredictionManager.class, new CachedServiceFetcher() { @Override diff --git a/core/java/android/app/search/ISearchCallback.aidl b/core/java/android/app/search/ISearchCallback.aidl new file mode 100644 index 0000000000000..a10bffc306a09 --- /dev/null +++ b/core/java/android/app/search/ISearchCallback.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.search; + +import android.content.pm.ParceledListSlice; + +/** + * @hide + */ +oneway interface ISearchCallback { + + void onResult(in ParceledListSlice result); +} diff --git a/core/java/android/app/search/ISearchUiManager.aidl b/core/java/android/app/search/ISearchUiManager.aidl new file mode 100644 index 0000000000000..a298a2cf3dfaf --- /dev/null +++ b/core/java/android/app/search/ISearchUiManager.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.search; + +import android.app.search.Query; +import android.app.search.SearchTarget; +import android.app.search.SearchTargetEvent; +import android.app.search.SearchSessionId; +import android.app.search.SearchContext; +import android.app.search.ISearchCallback; +import android.content.pm.ParceledListSlice; + +/** + * @hide + */ +interface ISearchUiManager { + + void createSearchSession(in SearchContext context, in SearchSessionId sessionId, + in IBinder token); + + void query(in SearchSessionId sessionId, in Query input, in ISearchCallback callback); + + void notifyEvent(in SearchSessionId sessionId, in Query input, in SearchTargetEvent event); + + void destroySearchSession(in SearchSessionId sessionId); +} diff --git a/core/java/android/app/search/Query.aidl b/core/java/android/app/search/Query.aidl new file mode 100644 index 0000000000000..6c13d9884243a --- /dev/null +++ b/core/java/android/app/search/Query.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2020, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.search; + +parcelable Query; \ No newline at end of file diff --git a/core/java/android/app/search/Query.java b/core/java/android/app/search/Query.java new file mode 100644 index 0000000000000..447ca314b99b2 --- /dev/null +++ b/core/java/android/app/search/Query.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.search; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +@SystemApi +public final class Query implements Parcelable { + + /** + * Query string typed from the client. + */ + @NonNull + private final String mInput; + + /** + * The timestamp that the query string was typed. If this object was created for the + * {@link SearchSession#query}, the client expects first consumer to be returned + * within mTimestamp + {@link SearchContext#mTimeoutMillis} + */ + private final long mTimestamp; + + @Nullable + private final Bundle mExtras; + + public Query(@NonNull String input, + long timestamp, + @Nullable Bundle extras) { + mInput = input; + mTimestamp = timestamp; + mExtras = extras; + } + + private Query(Parcel parcel) { + mInput = parcel.readString(); + mTimestamp = parcel.readLong(); + mExtras = parcel.readBundle(); + } + + @NonNull + public String getInput() { + return mInput; + } + + @NonNull + public long getTimestamp() { + return mTimestamp; + } + + @Nullable + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mInput); + dest.writeLong(mTimestamp); + dest.writeBundle(mExtras); + } + + /** + * @see Creator + */ + @NonNull + public static final Creator CREATOR = + new Creator() { + public Query createFromParcel(Parcel parcel) { + return new Query(parcel); + } + + public Query[] newArray(int size) { + return new Query[size]; + } + }; +} diff --git a/core/java/android/app/search/SearchAction.java b/core/java/android/app/search/SearchAction.java new file mode 100644 index 0000000000000..158f9f3846036 --- /dev/null +++ b/core/java/android/app/search/SearchAction.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.search; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.UserHandle; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * Represents a searchable action info that can be called from another process + * or within the client process. + * + * @hide + */ +@SystemApi +public final class SearchAction implements Parcelable { + + private static final String TAG = "SearchAction"; + + @NonNull + private String mId; + + @Nullable + private final Icon mIcon; + + @NonNull + private final CharSequence mTitle; + + @Nullable + private final CharSequence mSubtitle; + + @Nullable + private final CharSequence mContentDescription; + + @Nullable + private final PendingIntent mPendingIntent; + + @Nullable + private final Intent mIntent; + + @Nullable + private final UserHandle mUserHandle; + + @Nullable + private Bundle mExtras; + + SearchAction(Parcel in) { + mId = in.readString(); + mIcon = Icon.CREATOR.createFromParcel(in); + mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mSubtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mPendingIntent = PendingIntent.CREATOR.createFromParcel(in); + mIntent = Intent.CREATOR.createFromParcel(in); + mUserHandle = in.readTypedObject(UserHandle.CREATOR); + mExtras = in.readBundle(); + } + + private SearchAction( + @NonNull String id, + @Nullable Icon icon, + @NonNull CharSequence title, + @Nullable CharSequence subtitle, + @Nullable CharSequence contentDescription, + @Nullable PendingIntent pendingIntent, + @Nullable Intent intent, + @Nullable UserHandle userHandle, + @Nullable Bundle extras) { + mId = Objects.requireNonNull(id); + mIcon = icon; + mTitle = Objects.requireNonNull(title); + mSubtitle = subtitle; + mContentDescription = contentDescription; + mPendingIntent = pendingIntent; + mIntent = intent; + mUserHandle = userHandle; + mExtras = extras; + + if (mPendingIntent == null && mIntent == null) { + throw new IllegalStateException("At least one type of intent should be available."); + } + if (mPendingIntent != null && mIntent != null) { + throw new IllegalStateException("Only one type of intent should be available."); + } + } + + /** + * Returns the unique id of this object. + */ + public @NonNull String getId() { + return mId; + } + + /** + * Returns an icon representing the action. + */ + public @Nullable Icon getIcon() { + return mIcon; + } + + /** + * Returns a title representing the action. + */ + public @NonNull CharSequence getTitle() { + return mTitle; + } + + /** + * Returns a subtitle representing the action. + */ + public @Nullable CharSequence getSubtitle() { + return mSubtitle; + } + + /** + * Returns a content description representing the action. + */ + public @Nullable CharSequence getContentDescription() { + return mContentDescription; + } + + /** + * Returns the action intent. + */ + public @Nullable PendingIntent getPendingIntent() { + return mPendingIntent; + } + + /** + * Returns the intent. + */ + public @Nullable Intent getIntent() { + return mIntent; + } + + /** + * Returns the user handle. + */ + public @Nullable UserHandle getUserHandle() { + return mUserHandle; + } + + /** + * Returns the extra bundle for this object. + */ + public @Nullable Bundle getExtras() { + return mExtras; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SearchAction)) return false; + SearchAction that = (SearchAction) o; + return mId.equals(that.mId) && mTitle.equals(that.mTitle); + } + + @Override + public int hashCode() { + return Objects.hash(mId, mTitle); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeString(mId); + out.writeTypedObject(mIcon, flags); + TextUtils.writeToParcel(mTitle, out, flags); + TextUtils.writeToParcel(mSubtitle, out, flags); + TextUtils.writeToParcel(mContentDescription, out, flags); + out.writeTypedObject(mPendingIntent, flags); + out.writeTypedObject(mIntent, flags); + out.writeTypedObject(mUserHandle, flags); + out.writeBundle(mExtras); + } + + @Override + public String toString() { + String str = "id=" + mId + + " title=" + mTitle + + " contentDescription=" + mContentDescription + + " subtitle=" + mSubtitle + + " icon=" + mIcon + + " pendingIntent=" + (mPendingIntent == null ? "" : mPendingIntent.getIntent()) + + " intent=" + mIntent + + " userHandle=" + mUserHandle; + return str; + } + + public static final @android.annotation.NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SearchAction createFromParcel(Parcel in) { + return new SearchAction(in); + } + public SearchAction[] newArray(int size) { + return new SearchAction[size]; + } + }; + + /** + * A builder for search action object. + * + * @hide + */ + @SystemApi + public static final class Builder { + @NonNull + private String mId; + + @Nullable + private Icon mIcon; + + @NonNull + private CharSequence mTitle; + + @Nullable + private CharSequence mSubtitle; + + @Nullable + private CharSequence mContentDescription; + + @Nullable + private PendingIntent mPendingIntent; + + @Nullable + private Intent mIntent; + + @Nullable + private UserHandle mUserHandle; + + @Nullable + private Bundle mExtras; + + public Builder(@NonNull String id, @NonNull String title) { + mId = Objects.requireNonNull(id); + mTitle = Objects.requireNonNull(title); + } + + /** + * Sets the subtitle. + */ + @NonNull + public SearchAction.Builder setIcon( + @Nullable Icon icon) { + mIcon = icon; + return this; + } + + /** + * Sets the subtitle. + */ + @NonNull + public SearchAction.Builder setSubtitle( + @Nullable CharSequence subtitle) { + mSubtitle = subtitle; + return this; + } + + /** + * Sets the content description. + */ + @NonNull + public SearchAction.Builder setContentDescription( + @Nullable CharSequence contentDescription) { + mContentDescription = contentDescription; + return this; + } + + /** + * Sets the pending intent. + */ + @NonNull + public SearchAction.Builder setPendingIntent(@Nullable PendingIntent pendingIntent) { + mPendingIntent = pendingIntent; + return this; + } + + /** + * Sets the user handle. + */ + @NonNull + public SearchAction.Builder setUserHandle(@Nullable UserHandle userHandle) { + mUserHandle = userHandle; + return this; + } + + /** + * Sets the intent. + */ + @NonNull + public SearchAction.Builder setIntent(@Nullable Intent intent) { + mIntent = intent; + return this; + } + + /** + * Sets the extra. + */ + @NonNull + public SearchAction.Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds a new SearchAction instance. + * + * @throws IllegalStateException if no target is set + */ + @NonNull + public SearchAction build() { + return new SearchAction(mId, mIcon, mTitle, mSubtitle, mContentDescription, + mPendingIntent, mIntent, mUserHandle, mExtras); + } + } +} diff --git a/core/java/android/app/search/SearchContext.aidl b/core/java/android/app/search/SearchContext.aidl new file mode 100644 index 0000000000000..c9442b869a055 --- /dev/null +++ b/core/java/android/app/search/SearchContext.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2020, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.search; + +parcelable SearchContext; \ No newline at end of file diff --git a/core/java/android/app/search/SearchContext.java b/core/java/android/app/search/SearchContext.java new file mode 100644 index 0000000000000..9bf766dc86680 --- /dev/null +++ b/core/java/android/app/search/SearchContext.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.search; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +@SystemApi +public final class SearchContext implements Parcelable { + + /** + * Result types the client UI is expecting the service to return. + */ + private final int mResultTypes; + + /** + * Timeout constraint imposed from the client UI for the first search result. + */ + private final int mTimeoutMillis; + + /** + * Send other client UI configurations in extras. + */ + @Nullable + private final Bundle mExtras; + + /** + * Package name of the client. + */ + @Nullable + private String mPackageName; + + public SearchContext(int resultTypes, + int queryTimeoutMillis, + @Nullable Bundle extras) { + mResultTypes = resultTypes; + mTimeoutMillis = queryTimeoutMillis; + mExtras = extras; + } + + private SearchContext(Parcel parcel) { + mResultTypes = parcel.readInt(); + mTimeoutMillis = parcel.readInt(); + mPackageName = parcel.readString(); + mExtras = parcel.readBundle(); + } + + @Nullable + public String getPackageName() { + return mPackageName; + } + + /** + * @hide + */ + public void setPackageName(@Nullable String packageName) { + mPackageName = packageName; + } + + @NonNull + public int getTimeoutMillis() { + return mTimeoutMillis; + } + + @Nullable + public Bundle getExtras() { + return mExtras; + } + + @NonNull + public int getResultTypes() { + return mResultTypes; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mResultTypes); + dest.writeInt(mTimeoutMillis); + dest.writeString(mPackageName); + dest.writeBundle(mExtras); + } + + /** + * @see Parcelable.Creator + */ + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SearchContext createFromParcel(Parcel parcel) { + return new SearchContext(parcel); + } + + public SearchContext[] newArray(int size) { + return new SearchContext[size]; + } + }; +} diff --git a/core/java/android/app/search/SearchSession.java b/core/java/android/app/search/SearchSession.java new file mode 100644 index 0000000000000..7bd88d9852fce --- /dev/null +++ b/core/java/android/app/search/SearchSession.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.search; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.search.ISearchCallback.Stub; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import dalvik.system.CloseGuard; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +/** + * Client API to share information about the search UI state and execute query. + * + *

+ * Usage:

 {@code
+ *
+ * class MyActivity {
+ *
+ *    void onCreate() {
+ *         mSearchSession.createSearchSession(searchContext)
+ *    }
+ *
+ *    void afterTextChanged(...) {
+ *        mSearchSession.query(...);
+ *    }
+ *
+ *    void onTouch(...) OR
+ *    void onStateTransitionStarted(...) OR
+ *    void onResume(...) OR
+ *    void onStop(...) {
+ *        mSearchSession.notifyEvent(event);
+ *    }
+ *
+ *    void onDestroy() {
+ *        mSearchSession.destroy();
+ *    }
+ *
+ * }
+ * + * @hide + */ +@SystemApi +public final class SearchSession implements AutoCloseable{ + + private static final String TAG = SearchSession.class.getSimpleName(); + private static final boolean DEBUG = false; + + private final android.app.search.ISearchUiManager mInterface; + private final CloseGuard mCloseGuard = CloseGuard.get(); + private final AtomicBoolean mIsClosed = new AtomicBoolean(false); + + private final SearchSessionId mSessionId; + private final IBinder mToken = new Binder(); + + /** + * Creates a new search ui client. + *

+ * The caller should call {@link SearchSession#destroy()} to dispose the client once it + * no longer used. + * + * @param context the {@link Context} of the user of this {@link SearchSession}. + * @param searchContext the search context. + */ + // b/175668315 Create weak reference child objects to not leak context. + SearchSession(@NonNull Context context, @NonNull SearchContext searchContext) { + IBinder b = ServiceManager.getService(Context.SEARCH_UI_SERVICE); + mInterface = android.app.search.ISearchUiManager.Stub.asInterface(b); + mSessionId = new SearchSessionId( + context.getPackageName() + ":" + UUID.randomUUID().toString(), context.getUserId()); + // b/175527717 whitelist possible clients of this API + searchContext.setPackageName(context.getPackageName()); + try { + mInterface.createSearchSession(searchContext, mSessionId, mToken); + } catch (RemoteException e) { + Log.e(TAG, "Failed to search session", e); + e.rethrowFromSystemServer(); + } + + mCloseGuard.open("close"); + } + + /** + * Notifies the search service of an search target event. + * + * @param query input object associated with the event. + * @param event The {@link SearchTargetEvent} that represents the search target event. + */ + public void notifyEvent(@NonNull Query query, @NonNull SearchTargetEvent event) { + if (mIsClosed.get()) { + throw new IllegalStateException("This client has already been destroyed."); + } + + try { + mInterface.notifyEvent(mSessionId, query, event); + } catch (RemoteException e) { + Log.e(TAG, "Failed to notify event", e); + e.rethrowFromSystemServer(); + } + } + + /** + * Calls consumer with list of {@link SearchTarget}s based on the input query. + * + * @param input query object to be used for the request. + * @param callbackExecutor The callback executor to use when calling the callback. + * @param callback The callback to return the list of search targets. + */ + @Nullable + public void query(@NonNull Query input, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull Consumer> callback) { + if (mIsClosed.get()) { + throw new IllegalStateException("This client has already been destroyed."); + } + + try { + + mInterface.query(mSessionId, input, new CallbackWrapper(callbackExecutor, callback)); + } catch (RemoteException e) { + Log.e(TAG, "Failed to sort targets", e); + e.rethrowFromSystemServer(); + } + } + + /** + * Destroys the client and unregisters the callback. Any method on this class after this call + * will throw {@link IllegalStateException}. + */ + public void destroy() { + if (!mIsClosed.getAndSet(true)) { + mCloseGuard.close(); + + // Do destroy; + try { + mInterface.destroySearchSession(mSessionId); + } catch (RemoteException e) { + Log.e(TAG, "Failed to notify search target event", e); + e.rethrowFromSystemServer(); + } + } else { + throw new IllegalStateException("This client has already been destroyed."); + } + } + + @Override + protected void finalize() { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + if (!mIsClosed.get()) { + destroy(); + } + } finally { + try { + super.finalize(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + } + } + + @Override + public void close() { + try { + finalize(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + } + + static class CallbackWrapper extends Stub { + + private final Consumer> mCallback; + private final Executor mExecutor; + + CallbackWrapper(@NonNull Executor callbackExecutor, + @NonNull Consumer> callback) { + mCallback = callback; + mExecutor = callbackExecutor; + } + + @Override + public void onResult(ParceledListSlice result) { + final long identity = Binder.clearCallingIdentity(); + try { + if (DEBUG) { + Log.d(TAG, "CallbackWrapper.onResult result=" + result.getList()); + } + mExecutor.execute(() -> mCallback.accept(result.getList())); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } +} diff --git a/core/java/android/app/search/SearchSessionId.aidl b/core/java/android/app/search/SearchSessionId.aidl new file mode 100644 index 0000000000000..d39013668047a --- /dev/null +++ b/core/java/android/app/search/SearchSessionId.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2020, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.search; + +parcelable SearchSessionId; \ No newline at end of file diff --git a/core/java/android/app/search/SearchSessionId.java b/core/java/android/app/search/SearchSessionId.java new file mode 100644 index 0000000000000..03bf1a124b0b6 --- /dev/null +++ b/core/java/android/app/search/SearchSessionId.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.search; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * The id for an search session. See {@link SearchSession}. + * + * @hide + */ +@SystemApi +public final class SearchSessionId implements Parcelable { + + private final String mId; + private final int mUserId; + + /** + * Creates a new id for a search session. + * + * @hide + */ + public SearchSessionId(@NonNull final String id, final int userId) { + mId = id; + mUserId = userId; + } + + private SearchSessionId(Parcel p) { + mId = p.readString(); + mUserId = p.readInt(); + } + + /** + * @hide + */ + public int getUserId() { + return mUserId; + } + + @Override + public boolean equals(@Nullable Object o) { + if (!getClass().equals(o != null ? o.getClass() : null)) return false; + + SearchSessionId other = (SearchSessionId) o; + return mId.equals(other.mId) && mUserId == other.mUserId; + } + + @Override + public @NonNull String toString() { + return mId + "," + mUserId; + } + + @Override + public int hashCode() { + return Objects.hash(mId, mUserId); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mId); + dest.writeInt(mUserId); + } + + public static final @NonNull Creator CREATOR = + new Creator() { + public SearchSessionId createFromParcel(Parcel parcel) { + return new SearchSessionId(parcel); + } + + public SearchSessionId[] newArray(int size) { + return new SearchSessionId[size]; + } + }; +} diff --git a/core/java/android/app/search/SearchTarget.aidl b/core/java/android/app/search/SearchTarget.aidl new file mode 100644 index 0000000000000..0d3d5d334f600 --- /dev/null +++ b/core/java/android/app/search/SearchTarget.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2020, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.search; + +parcelable SearchTarget; \ No newline at end of file diff --git a/core/java/android/app/search/SearchTarget.java b/core/java/android/app/search/SearchTarget.java new file mode 100644 index 0000000000000..cac22d81c67b7 --- /dev/null +++ b/core/java/android/app/search/SearchTarget.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.search; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.appwidget.AppWidgetProviderInfo; +import android.content.pm.ShortcutInfo; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.UserHandle; + +import java.util.Objects; + +/** + * A representation of a searchable item info. + * + * @hide + */ +@SystemApi +public final class SearchTarget implements Parcelable { + + + @NonNull + private final int mResultType; + + /** + * Constant to express how the group of {@link SearchTarget} should be laid out. + */ + @NonNull + private final String mLayoutType; + + @NonNull + private final String mId; + + @Nullable + private String mParentId; + + private final float mScore; + + private final boolean mShouldHide; + + @NonNull + private final String mPackageName; + @NonNull + private final UserHandle mUserHandle; + @Nullable + private final SearchAction mSearchAction; + @Nullable + private final ShortcutInfo mShortcutInfo; + @Nullable + private final AppWidgetProviderInfo mAppWidgetProviderInfo; + @Nullable + private final Uri mSliceUri; + @Nullable + private final Bundle mExtras; + + private SearchTarget(Parcel parcel) { + mResultType = parcel.readInt(); + mLayoutType = parcel.readString(); + + mId = parcel.readString(); + mParentId = parcel.readString(); + mScore = parcel.readFloat(); + mShouldHide = parcel.readBoolean(); + + mPackageName = parcel.readString(); + mUserHandle = UserHandle.of(parcel.readInt()); + mSearchAction = parcel.readTypedObject(SearchAction.CREATOR); + mShortcutInfo = parcel.readTypedObject(ShortcutInfo.CREATOR); + mAppWidgetProviderInfo = parcel.readTypedObject(AppWidgetProviderInfo.CREATOR); + mSliceUri = parcel.readTypedObject(Uri.CREATOR); + mExtras = parcel.readBundle(getClass().getClassLoader()); + } + + private SearchTarget( + int resultType, + @NonNull String layoutType, + @NonNull String id, + @Nullable String parentId, + float score, boolean shouldHide, + @NonNull String packageName, + @NonNull UserHandle userHandle, + @Nullable SearchAction action, + @Nullable ShortcutInfo shortcutInfo, + @Nullable Uri sliceUri, + @Nullable AppWidgetProviderInfo appWidgetProviderInfo, + @Nullable Bundle extras) { + mResultType = resultType; + mLayoutType = Objects.requireNonNull(layoutType); + mId = Objects.requireNonNull(id); + mParentId = parentId; + mScore = score; + mShouldHide = shouldHide; + mPackageName = Objects.requireNonNull(packageName); + mUserHandle = Objects.requireNonNull(userHandle); + mSearchAction = action; + mShortcutInfo = shortcutInfo; + mAppWidgetProviderInfo = appWidgetProviderInfo; + mSliceUri = sliceUri; + mExtras = extras; + + int published = 0; + if (mSearchAction != null) published++; + if (mShortcutInfo != null) published++; + if (mAppWidgetProviderInfo != null) published++; + if (mSliceUri != null) published++; + if (published > 1) { + throw new IllegalStateException("Only one of SearchAction, ShortcutInfo," + + " AppWidgetProviderInfo, SliceUri can be assigned in a SearchTarget."); + } + } + + /** + * Retrieves the result type. + */ + public int getResultType() { + return mResultType; + } + + /** + * Retrieves the layout type. + */ + @NonNull + public String getLayoutType() { + return mLayoutType; + } + + /** + * Retrieves the id of the target. + */ + @NonNull + public String getId() { + return mId; + } + + /** + * Retrieves the parent id of the target. + */ + @NonNull + public String getParentId() { + return mParentId; + } + + /** + * Retrieves the score of the target. + */ + public float getScore() { + return mScore; + } + + /** + * TODO: add comment + */ + public boolean shouldHide() { + return mShouldHide; + } + + /** + * Retrieves the package name of the target. + */ + @NonNull + public String getPackageName() { + return mPackageName; + } + + /** + * Retrieves the user handle of the target. + */ + @NonNull + public UserHandle getUserHandle() { + return mUserHandle; + } + + /** + * Retrieves the shortcut info of the target. + */ + @Nullable + public ShortcutInfo getShortcutInfo() { + return mShortcutInfo; + } + + /** + * Return widget provider info. + */ + @Nullable + public AppWidgetProviderInfo getAppWidgetProviderInfo() { + return mAppWidgetProviderInfo; + } + + /** + * Return slice uri. + */ + @Nullable + public Uri getSliceUri() { + return mSliceUri; + } + + /** + * Return search action. + */ + @Nullable + public SearchAction getSearchAction() { + return mSearchAction; + } + + /** + * Return extra bundle. + */ + @Nullable + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeInt(mResultType); + parcel.writeString(mLayoutType); + parcel.writeString(mId); + parcel.writeString(mParentId); + parcel.writeFloat(mScore); + parcel.writeBoolean(mShouldHide); + parcel.writeString(mPackageName); + parcel.writeInt(mUserHandle.getIdentifier()); + parcel.writeTypedObject(mSearchAction, flags); + parcel.writeTypedObject(mShortcutInfo, flags); + parcel.writeTypedObject(mAppWidgetProviderInfo, flags); + parcel.writeTypedObject(mSliceUri, flags); + parcel.writeBundle(mExtras); + } + + /** + * @see Parcelable.Creator + */ + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SearchTarget createFromParcel(Parcel parcel) { + return new SearchTarget(parcel); + } + + public SearchTarget[] newArray(int size) { + return new SearchTarget[size]; + } + }; + + /** + * A builder for search target object. + * + * @hide + */ + @SystemApi + public static final class Builder { + private int mResultType; + @NonNull + private String mLayoutType; + @NonNull + private String mId; + @Nullable + private String mParentId; + private float mScore; + private boolean mShouldHide; + @NonNull + private String mPackageName; + @NonNull + private UserHandle mUserHandle; + @Nullable + private SearchAction mSearchAction; + @Nullable + private ShortcutInfo mShortcutInfo; + @Nullable + private Uri mSliceUri; + @Nullable + private AppWidgetProviderInfo mAppWidgetProviderInfo; + @Nullable + private Bundle mExtras; + + public Builder(int resultType, + @NonNull String layoutType, + @NonNull String id) { + mId = id; + mLayoutType = Objects.requireNonNull(layoutType); + mResultType = resultType; + mScore = 1f; + mShouldHide = false; + } + + /** + * Sets the parent id. + */ + @NonNull + public Builder setParentId(@NonNull String parentId) { + mParentId = Objects.requireNonNull(parentId); + return this; + } + + /** + * Sets the package name. + */ + @NonNull + public Builder setPackageName(@NonNull String packageName) { + mPackageName = Objects.requireNonNull(packageName); + return this; + } + + /** + * Sets the user handle. + */ + @NonNull + public Builder setUserHandle(@NonNull UserHandle userHandle) { + mUserHandle = Objects.requireNonNull(userHandle); + return this; + } + + /** + * Sets the shortcut info. + */ + @NonNull + public Builder setShortcutInfo(@NonNull ShortcutInfo shortcutInfo) { + mShortcutInfo = Objects.requireNonNull(shortcutInfo); + if (mPackageName != null && !mPackageName.equals(shortcutInfo.getPackage())) { + throw new IllegalStateException("SearchTarget packageName is different from " + + "shortcut's packageName"); + } + mPackageName = shortcutInfo.getPackage(); + return this; + } + + /** + * Sets the app widget provider info. + */ + @NonNull + public Builder setAppWidgetProviderInfo( + @NonNull AppWidgetProviderInfo appWidgetProviderInfo) { + mAppWidgetProviderInfo = Objects.requireNonNull(appWidgetProviderInfo); + if (mPackageName != null + && !mPackageName.equals(appWidgetProviderInfo.provider.getPackageName())) { + throw new IllegalStateException("SearchTarget packageName is different from " + + "appWidgetProviderInfo's packageName"); + } + return this; + } + + /** + * Sets the slice URI. + */ + @NonNull + public Builder setSliceUri(@NonNull Uri sliceUri) { + // TODO: add packageName check + mSliceUri = sliceUri; + return this; + } + + /** + * TODO: add comment + */ + @NonNull + public Builder setSearchAction(@Nullable SearchAction remoteAction) { + // TODO: add packageName check + mSearchAction = remoteAction; + return this; + } + + /** + * TODO: add comment + */ + @NonNull + public Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** + * TODO: add comment + */ + @NonNull + public Builder setScore(float score) { + mScore = score; + return this; + } + + /** + * TODO: add comment + */ + @NonNull + public Builder setShouldHide(boolean shouldHide) { + mShouldHide = shouldHide; + return this; + } + + /** + * Builds a new SearchTarget instance. + * + * @throws IllegalStateException if no target is set + */ + @NonNull + public SearchTarget build() { + return new SearchTarget(mResultType, mLayoutType, mId, mParentId, mScore, mShouldHide, + mPackageName, mUserHandle, + mSearchAction, mShortcutInfo, mSliceUri, mAppWidgetProviderInfo, + mExtras); + } + } +} diff --git a/core/java/android/app/search/SearchTargetEvent.aidl b/core/java/android/app/search/SearchTargetEvent.aidl new file mode 100644 index 0000000000000..a7a8ea23c0298 --- /dev/null +++ b/core/java/android/app/search/SearchTargetEvent.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2020, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.search; + +parcelable SearchTargetEvent; diff --git a/core/java/android/app/search/SearchTargetEvent.java b/core/java/android/app/search/SearchTargetEvent.java new file mode 100644 index 0000000000000..f478dc3c8fc77 --- /dev/null +++ b/core/java/android/app/search/SearchTargetEvent.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.search; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A representation of an app target event. + * + * @hide + */ +@SystemApi +public final class SearchTargetEvent implements Parcelable { + + /** + * @hide + */ + @IntDef(prefix = {"ACTION_"}, value = { + ACTION_SURFACE_VISIBLE, + ACTION_TAP, + ACTION_LONGPRESS, + ACTION_LAUNCH_TOUCH, + ACTION_LAUNCH_KEYBOARD_FOCUS, + ACTION_DRAGNDROP, + ACTION_SURFACE_INVISIBLE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ActionType {} + + /** + * @hide + */ + @IntDef(prefix = {"FLAG_"}, value = { + FLAG_IME_SHOWN + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FlagType {} + + @NonNull + private final List mTargetIds; + @Nullable + private final String mLocation; + @ActionType + private final int mAction; + @FlagType + private int mFlags; + + /** + * IME was shown when event happened. + */ + public static final int FLAG_IME_SHOWN = 1 << 0; + + + /** + * Search container was opened. + */ + public static final int ACTION_SURFACE_VISIBLE = 1; + + /** + * Constants that define tapping without closing the search surface. + */ + public static final int ACTION_TAP = 3; + + /** + * Constants that define long pressing without closing the search surface. + */ + public static final int ACTION_LONGPRESS = 4; + + /** + * Constants that define tapping on the touch target to launch. + */ + public static final int ACTION_LAUNCH_TOUCH = 5; + + /** + * Constants that define tapping on the soft keyboard confirm or search to launch. + */ + public static final int ACTION_LAUNCH_KEYBOARD_FOCUS = 6; + + /** + * Searcheable item was draged and dropped to another surface. + */ + public static final int ACTION_DRAGNDROP = 7; + + /** + * Search container was closed. + */ + public static final int ACTION_SURFACE_INVISIBLE = 8; + + private SearchTargetEvent(@NonNull List targetIds, + @Nullable String location, + @ActionType int actionType, + @FlagType int flags) { + mTargetIds = Objects.requireNonNull(targetIds); + mLocation = location; + mAction = actionType; + mFlags = flags; + } + + private SearchTargetEvent(Parcel parcel) { + mTargetIds = new ArrayList<>(); + parcel.readStringList(mTargetIds); + mLocation = parcel.readString(); + mAction = parcel.readInt(); + mFlags = parcel.readInt(); + } + + /** + * Returns the primary search target with interaction. + */ + @NonNull + public String getTargetId() { + return mTargetIds.get(0); + } + + /** + * Returns the list of search targets with visualization change. + */ + @NonNull + public List getTargetIds() { + return mTargetIds; + } + + /** + * Returns the launch location. + */ + @Nullable + public String getLaunchLocation() { + return mLocation; + } + + /** + * Returns the action type. + */ + @ActionType + public int getAction() { + return mAction; + } + + public int getFlags() { + return mFlags; + } + + @Override + public int hashCode() { + return mTargetIds.get(0).hashCode() + mAction; + } + + @Override + public boolean equals(@Nullable Object o) { + if (!getClass().equals(o != null ? o.getClass() : null)) return false; + + SearchTargetEvent other = (SearchTargetEvent) o; + return mTargetIds.equals(other.mTargetIds) + && mAction == other.mAction + && mFlags == other.mFlags + && mLocation == null ? other.mLocation == null : mLocation.equals(other.mLocation); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeStringList(mTargetIds); + dest.writeString(mLocation); + dest.writeInt(mAction); + dest.writeInt(mFlags); + } + + public static final @NonNull Creator CREATOR = + new Creator() { + public SearchTargetEvent createFromParcel(Parcel parcel) { + return new SearchTargetEvent(parcel); + } + + public SearchTargetEvent[] newArray(int size) { + return new SearchTargetEvent[size]; + } + }; + + /** + * A builder for search target events. + * + * @hide + */ + @SystemApi + public static final class Builder { + @NonNull + private List mTargetIds; + @Nullable + private String mLocation; + @ActionType + private int mAction; + private int mFlags; + + /** + * @param id The target id that is associated with this event. + * @param actionType The event type + */ + public Builder(@NonNull String id, @ActionType int actionType) { + mTargetIds = new ArrayList<>(); + mTargetIds.add(id); + mAction = actionType; + } + + /** + * @param ids The target ids that is associated with this event. + * @param actionType The event type + */ + public Builder(@NonNull List ids, @ActionType int actionType) { + mTargetIds = ids; + mAction = actionType; + } + + /** + * Sets the launch location. + */ + @NonNull + public Builder setLaunchLocation(@Nullable String location) { + mLocation = location; + return this; + } + + /** + * Sets the launch location. + */ + @NonNull + public Builder setFlags(int flags) { + mFlags = flags; + return this; + } + + /** + * Builds a new event instance. + */ + @NonNull + public SearchTargetEvent build() { + return new SearchTargetEvent(mTargetIds, mLocation, mAction, mFlags); + } + } +} diff --git a/core/java/android/app/search/SearchUiManager.java b/core/java/android/app/search/SearchUiManager.java new file mode 100644 index 0000000000000..636bfe15df030 --- /dev/null +++ b/core/java/android/app/search/SearchUiManager.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.search; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; + +import java.util.Objects; + +/** + * Class that provides methods to create search ui session clients. + * + * @hide + */ +@SystemApi +public final class SearchUiManager { + + private final Context mContext; + + /** + * @hide + */ + public SearchUiManager(Context context) { + mContext = Objects.requireNonNull(context); + } + + /** + * Creates a new search session. + */ + @NonNull + public SearchSession createSearchSession( + @NonNull SearchContext searchContext) { + return new SearchSession(mContext, searchContext); + } +} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index f3e1a87ea271a..30d53a8cba8dc 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4528,6 +4528,18 @@ public abstract class Context { @SystemApi public static final String APP_PREDICTION_SERVICE = "app_prediction"; + /** + * Official published name of the search ui service. + * + *

NOTE: this service is optional; callers of + * {@code Context.getSystemServiceName(SEARCH_UI_SERVICE)} should check for {@code null}. + * + * @hide + * @see #getSystemService(String) + */ + @SystemApi + public static final String SEARCH_UI_SERVICE = "search_ui"; + /** * Use with {@link #getSystemService(String)} to access the * {@link com.android.server.voiceinteraction.SoundTriggerService}. diff --git a/core/java/android/service/search/ISearchUiService.aidl b/core/java/android/service/search/ISearchUiService.aidl new file mode 100644 index 0000000000000..aae66ca0b08ac --- /dev/null +++ b/core/java/android/service/search/ISearchUiService.aidl @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.search; + +import android.app.search.Query; +import android.app.search.SearchTarget; +import android.app.search.SearchTargetEvent; +import android.app.search.SearchSessionId; +import android.app.search.SearchContext; +import android.app.search.ISearchCallback; +import android.content.pm.ParceledListSlice; + +/** + * Interface from the system to a search service. + * + * @hide + */ +oneway interface ISearchUiService { + + void onCreateSearchSession(in SearchContext context, in SearchSessionId sessionId); + + void onQuery(in SearchSessionId sessionId, in Query input, in ISearchCallback callback); + + void onNotifyEvent(in SearchSessionId sessionId, in Query input, in SearchTargetEvent event); + + void onDestroy(in SearchSessionId sessionId); +} diff --git a/core/java/android/service/search/SearchUiService.java b/core/java/android/service/search/SearchUiService.java new file mode 100644 index 0000000000000..d0e41956523a7 --- /dev/null +++ b/core/java/android/service/search/SearchUiService.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.search; + +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.CallSuper; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.Service; +import android.app.search.ISearchCallback; +import android.app.search.Query; +import android.app.search.SearchContext; +import android.app.search.SearchSessionId; +import android.app.search.SearchTarget; +import android.app.search.SearchTargetEvent; +import android.content.Intent; +import android.content.pm.ParceledListSlice; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.service.search.ISearchUiService.Stub; +import android.util.Slog; + +import java.util.List; +import java.util.function.Consumer; + +/** + * A service used to share the lifecycle of search UI (open, close, interaction) + * and also to return search result on a query. + * + * @hide + */ +@SystemApi +public abstract class SearchUiService extends Service { + + private static final boolean DEBUG = false; + private static final String TAG = "SearchUiService"; + + /** + * The {@link Intent} that must be declared as handled by the service. + * + *

The service must also require the {@link android.permission#MANAGE_SEARCH_UI} + * permission. + * + * @hide + */ + public static final String SERVICE_INTERFACE = + "android.service.search.SearchUiService"; + + private Handler mHandler; + + private final android.service.search.ISearchUiService mInterface = new Stub() { + + @Override + public void onCreateSearchSession(SearchContext context, SearchSessionId sessionId) { + mHandler.sendMessage( + obtainMessage(SearchUiService::onCreateSearchSession, + SearchUiService.this, context, sessionId)); + } + + @Override + public void onQuery(SearchSessionId sessionId, Query input, + ISearchCallback callback) { + mHandler.sendMessage( + obtainMessage(SearchUiService::onQuery, + SearchUiService.this, sessionId, input, + new CallbackWrapper(callback, null))); + } + + @Override + public void onNotifyEvent(SearchSessionId sessionId, Query query, SearchTargetEvent event) { + mHandler.sendMessage( + obtainMessage(SearchUiService::onNotifyEvent, + SearchUiService.this, sessionId, query, event)); + } + + @Override + public void onDestroy(SearchSessionId sessionId) { + mHandler.sendMessage( + obtainMessage(SearchUiService::doDestroy, + SearchUiService.this, sessionId)); + } + }; + + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + mHandler = new Handler(Looper.getMainLooper(), null, true); + } + + @Override + @NonNull + public final IBinder onBind(@NonNull Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + return mInterface.asBinder(); + } + Slog.w(TAG, "Tried to bind to wrong intent (should be " + + SERVICE_INTERFACE + ": " + intent); + return null; + } + + /** + * Creates a new search session. + */ + public void onCreateSearchSession(@NonNull SearchContext context, + @NonNull SearchSessionId sessionId) {} + + /** + * Called by the client to request search results using a query string. + */ + @MainThread + public abstract void onQuery(@NonNull SearchSessionId sessionId, + @NonNull Query query, + @NonNull Consumer> callback); + + /** + * Called by a client to indicate an interaction (tap, long press, drag, etc) on target(s). + */ + @MainThread + public abstract void onNotifyEvent(@NonNull SearchSessionId sessionId, + @NonNull Query query, + @NonNull SearchTargetEvent event); + + private void doDestroy(@NonNull SearchSessionId sessionId) { + super.onDestroy(); + onDestroy(sessionId); + } + + /** + * Destroys a search session. + */ + @MainThread + public abstract void onDestroy(@NonNull SearchSessionId sessionId); + + private static final class CallbackWrapper implements Consumer>, + IBinder.DeathRecipient { + + private ISearchCallback mCallback; + private final Consumer mOnBinderDied; + + CallbackWrapper(ISearchCallback callback, + @Nullable Consumer onBinderDied) { + mCallback = callback; + mOnBinderDied = onBinderDied; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to link to death: " + e); + } + } + + @Override + public void accept(List searchTargets) { + try { + if (mCallback != null) { + if (DEBUG) { + Slog.d(TAG, "CallbackWrapper.accept searchTargets=" + searchTargets); + } + mCallback.onResult(new ParceledListSlice(searchTargets)); + } + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result:" + e); + } + } + + @Override + public void binderDied() { + mCallback.asBinder().unlinkToDeath(this, 0); + mCallback = null; + if (mOnBinderDied != null) { + mOnBinderDied.accept(this); + } + } + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index ef7edc2f2dd21..4f9a8de367b00 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -5034,6 +5034,11 @@ + + + diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index fab65d05e69ff..945b586ddc558 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3741,6 +3741,16 @@ --> + + +