Merge "Add Initial SearchUIManager"

This commit is contained in:
Hyunyoung Song
2020-12-15 23:17:45 +00:00
committed by Android (Google) Code Review
31 changed files with 2990 additions and 0 deletions

View File

@@ -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<android.app.search.Query> 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<android.app.search.SearchAction> 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<android.app.search.SearchContext> 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<java.util.List<android.app.search.SearchTarget>>);
}
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<android.app.search.SearchSessionId> 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<android.app.search.SearchTarget> 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<java.lang.String> 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<android.app.search.SearchTargetEvent> 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<java.lang.String>, 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 {
@@ -1806,6 +1938,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";
@@ -9590,6 +9723,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<java.util.List<android.app.search.SearchTarget>>);
}
}
package android.service.settings.suggestions {
public final class Suggestion implements android.os.Parcelable {

View File

@@ -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.net.Uri[]>, 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):

View File

@@ -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<SearchUiManager>() {
@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<AppPredictionManager>() {
@Override

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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<Query> CREATOR =
new Creator<Query>() {
public Query createFromParcel(Parcel parcel) {
return new Query(parcel);
}
public Query[] newArray(int size) {
return new Query[size];
}
};
}

View File

@@ -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<SearchAction> CREATOR =
new Parcelable.Creator<SearchAction>() {
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);
}
}
}

View File

@@ -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;

View File

@@ -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<SearchContext> CREATOR =
new Parcelable.Creator<SearchContext>() {
public SearchContext createFromParcel(Parcel parcel) {
return new SearchContext(parcel);
}
public SearchContext[] newArray(int size) {
return new SearchContext[size];
}
};
}

View File

@@ -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.
*
* <p>
* Usage: <pre> {@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();
* }
*
* }</pre>
*
* @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.
* <p>
* 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<List<SearchTarget>> 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<List<SearchTarget>> mCallback;
private final Executor mExecutor;
CallbackWrapper(@NonNull Executor callbackExecutor,
@NonNull Consumer<List<SearchTarget>> 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);
}
}
}
}

View File

@@ -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;

View File

@@ -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<SearchSessionId> CREATOR =
new Creator<SearchSessionId>() {
public SearchSessionId createFromParcel(Parcel parcel) {
return new SearchSessionId(parcel);
}
public SearchSessionId[] newArray(int size) {
return new SearchSessionId[size];
}
};
}

View File

@@ -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;

View File

@@ -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<SearchTarget> CREATOR =
new Parcelable.Creator<SearchTarget>() {
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);
}
}
}

View File

@@ -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;

View File

@@ -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<String> 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<String> 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<String> 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<SearchTargetEvent> CREATOR =
new Creator<SearchTargetEvent>() {
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<String> 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<String> 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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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.
*
* <p><b>NOTE: </b> 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}.

View File

@@ -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);
}

View File

@@ -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.
*
* <p>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<List<SearchTarget>> 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<List<SearchTarget>>,
IBinder.DeathRecipient {
private ISearchCallback mCallback;
private final Consumer<CallbackWrapper> mOnBinderDied;
CallbackWrapper(ISearchCallback callback,
@Nullable Consumer<CallbackWrapper> 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<SearchTarget> 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);
}
}
}
}

View File

@@ -5034,6 +5034,11 @@
<permission android:name="android.permission.MANAGE_APP_PREDICTIONS"
android:protectionLevel="signature|appPredictor" />
<!-- @SystemApi Allows an application to manage the search ui service.
@hide <p>Not for use by third-party applications.</p> -->
<permission android:name="android.permission.MANAGE_SEARCH_UI"
android:protectionLevel="signature" />
<!-- Allows an app to set the theme overlay in /vendor/overlay
being used.
@hide <p>Not for use by third-party applications.</p> -->

View File

@@ -3741,6 +3741,16 @@
-->
<string name="config_defaultContentSuggestionsService" translatable="false"></string>
<!-- The package name for the system's search ui service.
This service returns search results when provided with an input string.
This service must be trusted, as it can be activated without explicit consent of the user.
If no service with the specified name exists on the device, on device search wil be
disabled.
Example: "com.android.intelliegence/.SearchUiService"
-->
<string name="config_defaultSearchUiService" translatable="false"></string>
<string name="config_defaultMusicRecognitionService" translatable="false"></string>
<!-- The package name for the default retail demo app.

View File

@@ -3477,6 +3477,7 @@
<java-symbol type="string" name="config_defaultAugmentedAutofillService" />
<java-symbol type="string" name="config_defaultAppPredictionService" />
<java-symbol type="string" name="config_defaultContentSuggestionsService" />
<java-symbol type="string" name="config_defaultSearchUiService" />
<java-symbol type="string" name="config_defaultMusicRecognitionService" />
<java-symbol type="string" name="config_defaultAttentionService" />
<java-symbol type="string" name="config_defaultSystemCaptionsService" />

View File

@@ -27,6 +27,7 @@ filegroup {
":services.print-sources",
":services.profcollect-sources",
":services.restrictions-sources",
":services.searchui-sources",
":services.startop.iorap-sources",
":services.systemcaptions-sources",
":services.usage-sources",
@@ -72,6 +73,7 @@ java_library {
"services.print",
"services.profcollect",
"services.restrictions",
"services.searchui",
"services.startop",
"services.systemcaptions",
"services.usage",

View File

@@ -323,6 +323,8 @@ public final class SystemServer implements Dumpable {
"com.android.server.appprediction.AppPredictionManagerService";
private static final String CONTENT_SUGGESTIONS_SERVICE_CLASS =
"com.android.server.contentsuggestions.ContentSuggestionsManagerService";
private static final String SEARCH_UI_MANAGER_SERVICE_CLASS =
"com.android.server.searchui.SearchUiManagerService";
private static final String DEVICE_IDLE_CONTROLLER_CLASS =
"com.android.server.DeviceIdleController";
private static final String BLOB_STORE_MANAGER_SERVICE_CLASS =
@@ -1571,6 +1573,12 @@ public final class SystemServer implements Dumpable {
Slog.d(TAG, "ContentSuggestionsService not defined by OEM");
}
// Search UI manager service
// TODO: add deviceHasConfigString(context, R.string.config_defaultSearchUiService)
t.traceBegin("StartSearchUiService");
mSystemServiceManager.startService(SEARCH_UI_MANAGER_SERVICE_CLASS);
t.traceEnd();
t.traceBegin("InitConnectivityModuleConnector");
try {
ConnectivityModuleConnector.getInstance().init(context);

View File

@@ -0,0 +1,13 @@
filegroup {
name: "services.searchui-sources",
srcs: ["java/**/*.java"],
path: "java",
visibility: ["//frameworks/base/services"],
}
java_library_static {
name: "services.searchui",
defaults: ["platform_service_defaults"],
srcs: [":services.searchui-sources"],
libs: ["services.core"],
}

View File

@@ -0,0 +1,112 @@
/*
* 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 com.android.server.searchui;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
import android.os.IBinder;
import android.service.search.ISearchUiService;
import android.text.format.DateUtils;
import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService;
/**
* Proxy to the {@link android.service.searchui.SearchUiService} implementation in another
* process.
*/
public class RemoteSearchUiService extends
AbstractMultiplePendingRequestsRemoteService<RemoteSearchUiService,
ISearchUiService> {
private static final String TAG = "RemoteSearchUiService";
private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS;
private final RemoteSearchUiServiceCallbacks mCallback;
public RemoteSearchUiService(Context context, String serviceInterface,
ComponentName componentName, int userId,
RemoteSearchUiServiceCallbacks callback, boolean bindInstantServiceAllowed,
boolean verbose) {
super(context, serviceInterface, componentName, userId, callback,
context.getMainThreadHandler(),
bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0,
verbose, /* initialCapacity= */ 1);
mCallback = callback;
}
@Override
protected ISearchUiService getServiceInterface(IBinder service) {
return ISearchUiService.Stub.asInterface(service);
}
@Override
protected long getTimeoutIdleBindMillis() {
return PERMANENT_BOUND_TIMEOUT_MS;
}
@Override
protected long getRemoteRequestMillis() {
return TIMEOUT_REMOTE_REQUEST_MILLIS;
}
/**
* Schedules a request to bind to the remote service.
*/
public void reconnect() {
super.scheduleBind();
}
/**
* Schedule async request on remote service.
*/
public void scheduleOnResolvedService(@NonNull AsyncRequest<ISearchUiService> request) {
scheduleAsyncRequest(request);
}
/**
* Execute async request on remote service immediately instead of sending it to Handler queue.
*/
public void executeOnResolvedService(@NonNull AsyncRequest<ISearchUiService> request) {
executeAsyncRequest(request);
}
/**
* Failure callback
*/
public interface RemoteSearchUiServiceCallbacks
extends VultureCallback<RemoteSearchUiService> {
/**
* Notifies a the failure or timeout of a remote call.
*/
void onFailureOrTimeout(boolean timedOut);
/**
* Notifies change in connected state of the remote service.
*/
void onConnectedStateChanged(boolean connected);
}
@Override // from AbstractRemoteService
protected void handleOnConnectedStateChanged(boolean connected) {
if (mCallback != null) {
mCallback.onConnectedStateChanged(connected);
}
}
}

View File

@@ -0,0 +1,173 @@
/*
* 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 com.android.server.searchui;
import static android.Manifest.permission.MANAGE_SEARCH_UI;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.content.Context.SEARCH_UI_SERVICE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.search.ISearchCallback;
import android.app.search.ISearchUiManager;
import android.app.search.Query;
import android.app.search.SearchContext;
import android.app.search.SearchSessionId;
import android.app.search.SearchTargetEvent;
import android.content.Context;
import android.os.Binder;
import android.os.IBinder;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.util.Slog;
import com.android.server.LocalServices;
import com.android.server.infra.AbstractMasterSystemService;
import com.android.server.infra.FrameworkResourcesServiceNameResolver;
import com.android.server.wm.ActivityTaskManagerInternal;
import java.io.FileDescriptor;
import java.util.function.Consumer;
/**
* A service used to return search targets given a query.
*/
public class SearchUiManagerService extends
AbstractMasterSystemService<SearchUiManagerService, SearchUiPerUserService> {
private static final String TAG = SearchUiManagerService.class.getSimpleName();
private static final boolean DEBUG = false;
private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes
private ActivityTaskManagerInternal mActivityTaskManagerInternal;
public SearchUiManagerService(Context context) {
super(context, new FrameworkResourcesServiceNameResolver(context,
com.android.internal.R.string.config_defaultSearchUiService), null,
PACKAGE_UPDATE_POLICY_NO_REFRESH | PACKAGE_RESTART_POLICY_NO_REFRESH);
mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
}
@Override
protected SearchUiPerUserService newServiceLocked(int resolvedUserId, boolean disabled) {
return new SearchUiPerUserService(this, mLock, resolvedUserId);
}
@Override
public void onStart() {
publishBinderService(SEARCH_UI_SERVICE, new SearchUiManagerStub());
}
@Override
protected void enforceCallingPermissionForManagement() {
getContext().enforceCallingPermission(MANAGE_SEARCH_UI, TAG);
}
@Override // from AbstractMasterSystemService
protected void onServicePackageUpdatedLocked(@UserIdInt int userId) {
final SearchUiPerUserService service = peekServiceForUserLocked(userId);
if (service != null) {
service.onPackageUpdatedLocked();
}
}
@Override // from AbstractMasterSystemService
protected void onServicePackageRestartedLocked(@UserIdInt int userId) {
final SearchUiPerUserService service = peekServiceForUserLocked(userId);
if (service != null) {
service.onPackageRestartedLocked();
}
}
@Override
protected int getMaximumTemporaryServiceDurationMs() {
return MAX_TEMP_SERVICE_DURATION_MS;
}
private class SearchUiManagerStub extends ISearchUiManager.Stub {
@Override
public void createSearchSession(@NonNull SearchContext context,
@NonNull SearchSessionId sessionId, @NonNull IBinder token) {
runForUserLocked("createSearchSession", sessionId, (service) ->
service.onCreateSearchSessionLocked(context, sessionId, token));
}
@Override
public void notifyEvent(@NonNull SearchSessionId sessionId, @NonNull Query query,
@NonNull SearchTargetEvent event) {
runForUserLocked("notifyEvent", sessionId,
(service) -> service.notifyLocked(sessionId, query, event));
}
@Override
public void query(@NonNull SearchSessionId sessionId,
@NonNull Query query,
ISearchCallback callback) {
runForUserLocked("query", sessionId,
(service) -> service.queryLocked(sessionId, query, callback));
}
@Override
public void destroySearchSession(@NonNull SearchSessionId sessionId) {
runForUserLocked("destroySearchSession", sessionId,
(service) -> service.onDestroyLocked(sessionId));
}
public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
@Nullable FileDescriptor err,
@NonNull String[] args, @Nullable ShellCallback callback,
@NonNull ResultReceiver resultReceiver) {
new SearchUiManagerServiceShellCommand(SearchUiManagerService.this)
.exec(this, in, out, err, args, callback, resultReceiver);
}
private void runForUserLocked(@NonNull final String func,
@NonNull final SearchSessionId sessionId,
@NonNull final Consumer<SearchUiPerUserService> c) {
ActivityManagerInternal am = LocalServices.getService(ActivityManagerInternal.class);
final int userId = am.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
sessionId.getUserId(), false, ALLOW_NON_FULL, null, null);
if (DEBUG) {
Slog.d(TAG, "runForUserLocked:" + func + " from pid=" + Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid());
}
if (!(mServiceNameResolver.isTemporary(userId)
|| mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid()))) {
String msg = "Permission Denial: " + func + " from pid=" + Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid();
Slog.w(TAG, msg);
throw new SecurityException(msg);
}
final long origId = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
final SearchUiPerUserService service = getServiceForUserLocked(userId);
c.accept(service);
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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 com.android.server.searchui;
import android.annotation.NonNull;
import android.os.ShellCommand;
import java.io.PrintWriter;
/**
* The shell command implementation for the SearchUiManagerService.
*/
public class SearchUiManagerServiceShellCommand extends ShellCommand {
private static final String TAG =
SearchUiManagerServiceShellCommand.class.getSimpleName();
private final SearchUiManagerService mService;
public SearchUiManagerServiceShellCommand(@NonNull SearchUiManagerService service) {
mService = service;
}
@Override
public int onCommand(String cmd) {
if (cmd == null) {
return handleDefaultCommands(cmd);
}
final PrintWriter pw = getOutPrintWriter();
switch (cmd) {
case "set": {
final String what = getNextArgRequired();
switch (what) {
case "temporary-service": {
final int userId = Integer.parseInt(getNextArgRequired());
String serviceName = getNextArg();
if (serviceName == null) {
mService.resetTemporaryService(userId);
pw.println("SearchUiService temporarily reset. ");
return 0;
}
final int duration = Integer.parseInt(getNextArgRequired());
mService.setTemporaryService(userId, serviceName, duration);
pw.println("SearchUiService temporarily set to " + serviceName
+ " for " + duration + "ms");
break;
}
}
}
break;
default:
return handleDefaultCommands(cmd);
}
return 0;
}
@Override
public void onHelp() {
try (PrintWriter pw = getOutPrintWriter()) {
pw.println("SearchUiManagerService commands:");
pw.println(" help");
pw.println(" Prints this help text.");
pw.println("");
pw.println(" set temporary-service USER_ID [COMPONENT_NAME DURATION]");
pw.println(" Temporarily (for DURATION ms) changes the service implemtation.");
pw.println(" To reset, call with just the USER_ID argument.");
pw.println("");
}
}
}

View File

@@ -0,0 +1,374 @@
/*
* 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 com.android.server.searchui;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppGlobals;
import android.app.search.ISearchCallback;
import android.app.search.Query;
import android.app.search.SearchContext;
import android.app.search.SearchSessionId;
import android.app.search.SearchTargetEvent;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ServiceInfo;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.service.search.ISearchUiService;
import android.service.search.SearchUiService;
import android.util.ArrayMap;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.infra.AbstractRemoteService;
import com.android.server.infra.AbstractPerUserSystemService;
/**
* Per-user instance of {@link SearchUiManagerService}.
*/
public class SearchUiPerUserService extends
AbstractPerUserSystemService<SearchUiPerUserService, SearchUiManagerService>
implements RemoteSearchUiService.RemoteSearchUiServiceCallbacks {
private static final String TAG = SearchUiPerUserService.class.getSimpleName();
@Nullable
@GuardedBy("mLock")
private RemoteSearchUiService mRemoteService;
/**
* When {@code true}, remote service died but service state is kept so it's restored after
* the system re-binds to it.
*/
@GuardedBy("mLock")
private boolean mZombie;
@GuardedBy("mLock")
private final ArrayMap<SearchSessionId, SearchSessionInfo> mSessionInfos =
new ArrayMap<>();
protected SearchUiPerUserService(SearchUiManagerService master,
Object lock, int userId) {
super(master, lock, userId);
}
@Override // from PerUserSystemService
protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
throws NameNotFoundException {
ServiceInfo si;
try {
si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
PackageManager.GET_META_DATA, mUserId);
} catch (RemoteException e) {
throw new NameNotFoundException("Could not get service for " + serviceComponent);
}
// TODO(b/111701043): must check that either the service is from a system component,
// or it matches a service set by shell cmd (so it can be used on CTS tests and when
// OEMs are implementing the real service and also verify the proper permissions
return si;
}
@GuardedBy("mLock")
@Override // from PerUserSystemService
protected boolean updateLocked(boolean disabled) {
final boolean enabledChanged = super.updateLocked(disabled);
if (enabledChanged) {
if (!isEnabledLocked()) {
// Clear the remote service for the next call
updateRemoteServiceLocked();
}
}
return enabledChanged;
}
/**
* Notifies the service of a new search session.
*/
@GuardedBy("mLock")
public void onCreateSearchSessionLocked(@NonNull SearchContext context,
@NonNull SearchSessionId sessionId, @NonNull IBinder token) {
final boolean serviceExists = resolveService(sessionId,
s -> s.onCreateSearchSession(context, sessionId));
if (serviceExists && !mSessionInfos.containsKey(sessionId)) {
final SearchSessionInfo sessionInfo = new SearchSessionInfo(
sessionId, context, token, () -> {
synchronized (mLock) {
onDestroyLocked(sessionId);
}
});
if (sessionInfo.linkToDeath()) {
mSessionInfos.put(sessionId, sessionInfo);
} else {
// destroy the session if calling process is already dead
onDestroyLocked(sessionId);
}
}
}
/**
* Records an app target event to the service.
*/
@GuardedBy("mLock")
public void notifyLocked(@NonNull SearchSessionId sessionId, @NonNull Query query,
@NonNull SearchTargetEvent event) {
final SearchSessionInfo sessionInfo = mSessionInfos.get(sessionId);
if (sessionInfo == null) return;
resolveService(sessionId,
s -> s.onNotifyEvent(sessionId, query, event));
}
/**
* Requests the service to return search results of an input query.
*/
@GuardedBy("mLock")
public void queryLocked(@NonNull SearchSessionId sessionId,
@NonNull Query input, @NonNull ISearchCallback callback) {
final SearchSessionInfo sessionInfo = mSessionInfos.get(sessionId);
if (sessionInfo == null) return;
resolveService(sessionId,
s -> s.onQuery(sessionId, input, callback));
}
/**
* Notifies the service of the end of an existing search session.
*/
@GuardedBy("mLock")
public void onDestroyLocked(@NonNull SearchSessionId sessionId) {
if (isDebug()) {
Slog.d(TAG, "onDestroyLocked(): sessionId=" + sessionId);
}
final SearchSessionInfo sessionInfo = mSessionInfos.remove(sessionId);
if (sessionInfo == null) return;
resolveService(sessionId,
s -> s.onDestroy(sessionId));
sessionInfo.destroy();
}
@Override
public void onFailureOrTimeout(boolean timedOut) {
if (isDebug()) {
Slog.d(TAG, "onFailureOrTimeout(): timed out=" + timedOut);
}
// Do nothing, we are just proxying to the search ui service
}
@Override
public void onConnectedStateChanged(boolean connected) {
if (isDebug()) {
Slog.d(TAG, "onConnectedStateChanged(): connected=" + connected);
}
if (connected) {
synchronized (mLock) {
if (mZombie) {
// Validation check - shouldn't happen
if (mRemoteService == null) {
Slog.w(TAG, "Cannot resurrect sessions because remote service is null");
return;
}
mZombie = false;
resurrectSessionsLocked();
}
}
}
}
@Override
public void onServiceDied(RemoteSearchUiService service) {
if (isDebug()) {
Slog.w(TAG, "onServiceDied(): service=" + service);
}
synchronized (mLock) {
mZombie = true;
}
updateRemoteServiceLocked();
}
@GuardedBy("mLock")
private void updateRemoteServiceLocked() {
if (mRemoteService != null) {
mRemoteService.destroy();
mRemoteService = null;
}
}
void onPackageUpdatedLocked() {
if (isDebug()) {
Slog.v(TAG, "onPackageUpdatedLocked()");
}
destroyAndRebindRemoteService();
}
void onPackageRestartedLocked() {
if (isDebug()) {
Slog.v(TAG, "onPackageRestartedLocked()");
}
destroyAndRebindRemoteService();
}
private void destroyAndRebindRemoteService() {
if (mRemoteService == null) {
return;
}
if (isDebug()) {
Slog.d(TAG, "Destroying the old remote service.");
}
mRemoteService.destroy();
mRemoteService = null;
synchronized (mLock) {
mZombie = true;
}
mRemoteService = getRemoteServiceLocked();
if (mRemoteService != null) {
if (isDebug()) {
Slog.d(TAG, "Rebinding to the new remote service.");
}
mRemoteService.reconnect();
}
}
/**
* Called after the remote service connected, it's used to restore state from a 'zombie'
* service (i.e., after it died).
*/
private void resurrectSessionsLocked() {
final int numSessions = mSessionInfos.size();
if (isDebug()) {
Slog.d(TAG, "Resurrecting remote service (" + mRemoteService + ") on "
+ numSessions + " sessions.");
}
for (SearchSessionInfo sessionInfo : mSessionInfos.values()) {
sessionInfo.resurrectSessionLocked(this, sessionInfo.mToken);
}
}
@GuardedBy("mLock")
@Nullable
protected boolean resolveService(
@NonNull final SearchSessionId sessionId,
@NonNull final AbstractRemoteService.AsyncRequest<ISearchUiService> cb) {
final RemoteSearchUiService service = getRemoteServiceLocked();
if (service != null) {
service.executeOnResolvedService(cb);
}
return service != null;
}
@GuardedBy("mLock")
@Nullable
private RemoteSearchUiService getRemoteServiceLocked() {
if (mRemoteService == null) {
final String serviceName = getComponentNameLocked();
if (serviceName == null) {
if (mMaster.verbose) {
Slog.v(TAG, "getRemoteServiceLocked(): not set");
}
return null;
}
ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName);
mRemoteService = new RemoteSearchUiService(getContext(),
SearchUiService.SERVICE_INTERFACE, serviceComponent, mUserId, this,
mMaster.isBindInstantServiceAllowed(), mMaster.verbose);
}
return mRemoteService;
}
private static final class SearchSessionInfo {
private static final boolean DEBUG = true; // Do not submit with true
@NonNull
private final SearchSessionId mSessionId;
@NonNull
private final SearchContext mSearchContext;
@NonNull
final IBinder mToken;
@NonNull
final IBinder.DeathRecipient mDeathRecipient;
private final RemoteCallbackList<ISearchCallback> mCallbacks =
new RemoteCallbackList<ISearchCallback>() {
@Override
public void onCallbackDied(ISearchCallback callback) {
if (DEBUG) {
Slog.d(TAG, "Binder died for session Id=" + mSessionId
+ " and callback=" + callback.asBinder());
}
if (mCallbacks.getRegisteredCallbackCount() == 0) {
destroy();
}
}
};
SearchSessionInfo(
@NonNull final SearchSessionId id,
@NonNull final SearchContext context,
@NonNull final IBinder token,
@NonNull final IBinder.DeathRecipient deathRecipient) {
if (DEBUG) {
Slog.d(TAG, "Creating SearchSessionInfo for session Id=" + id);
}
mSessionId = id;
mSearchContext = context;
mToken = token;
mDeathRecipient = deathRecipient;
}
boolean linkToDeath() {
try {
mToken.linkToDeath(mDeathRecipient, 0);
} catch (RemoteException e) {
if (DEBUG) {
Slog.w(TAG, "Caller is dead before session can be started, sessionId: "
+ mSessionId);
}
return false;
}
return true;
}
void destroy() {
if (DEBUG) {
Slog.d(TAG, "Removing all callbacks for session Id=" + mSessionId
+ " and " + mCallbacks.getRegisteredCallbackCount() + " callbacks.");
}
if (mToken != null) {
mToken.unlinkToDeath(mDeathRecipient, 0);
}
mCallbacks.kill();
}
void resurrectSessionLocked(SearchUiPerUserService service, IBinder token) {
int callbackCount = mCallbacks.getRegisteredCallbackCount();
if (DEBUG) {
Slog.d(TAG, "Resurrecting remote service (" + service.getRemoteServiceLocked()
+ ") for session Id=" + mSessionId + " and "
+ callbackCount + " callbacks.");
}
service.onCreateSearchSessionLocked(mSearchContext, mSessionId, token);
}
}
}