From 723bf37ab2e4b535a6061c02df03fcec57006e37 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Fri, 25 Aug 2017 17:22:31 -0700 Subject: [PATCH] Initial API for providing suggestions. This is a WIP system api that will be - Implemented by one unbundled app (SettingsIntelligence) using system-sdk, - and consumed by Settings app. Note: The bundled app does not have permission to read/write any user settings. Nor does it have permission to call any private API to do anything unsafe. Test: builds Test: instrumentation test Bug: 65065268 Change-Id: Ib190c0e4c167deb3c6197b8a5b39d442d804770b --- Android.mk | 1 + api/system-current.txt | 30 ++++ .../suggestions/ISuggestionService.aidl | 20 +++ .../settings/suggestions/Suggestion.aidl | 20 +++ .../settings/suggestions/Suggestion.java | 152 ++++++++++++++++++ .../suggestions/SuggestionService.java | 71 ++++++++ core/res/AndroidManifest.xml | 5 + core/tests/coretests/AndroidManifest.xml | 5 + .../suggestions/MockSuggestionService.java | 38 +++++ .../suggestions/SuggestionServiceTest.java | 53 ++++++ .../settings/suggestions/SuggestionTest.java | 90 +++++++++++ 11 files changed, 485 insertions(+) create mode 100644 core/java/android/service/settings/suggestions/ISuggestionService.aidl create mode 100644 core/java/android/service/settings/suggestions/Suggestion.aidl create mode 100644 core/java/android/service/settings/suggestions/Suggestion.java create mode 100644 core/java/android/service/settings/suggestions/SuggestionService.java create mode 100644 core/tests/coretests/src/android/service/settings/suggestions/MockSuggestionService.java create mode 100644 core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java create mode 100644 core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java diff --git a/Android.mk b/Android.mk index 76ca9b928aa21..09e596ffa0423 100644 --- a/Android.mk +++ b/Android.mk @@ -305,6 +305,7 @@ LOCAL_SRC_FILES += \ core/java/android/service/notification/IStatusBarNotificationHolder.aidl \ core/java/android/service/notification/IConditionListener.aidl \ core/java/android/service/notification/IConditionProvider.aidl \ + core/java/android/service/settings/suggestions/ISuggestionService.aidl \ core/java/android/service/vr/IPersistentVrStateCallbacks.aidl \ core/java/android/service/vr/IVrListener.aidl \ core/java/android/service/vr/IVrManager.aidl \ diff --git a/api/system-current.txt b/api/system-current.txt index 4a76bfbda1d01..c03d70cb8f793 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -57,6 +57,7 @@ package android { field public static final java.lang.String BIND_RESOLVER_RANKER_SERVICE = "android.permission.BIND_RESOLVER_RANKER_SERVICE"; field public static final java.lang.String BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE = "android.permission.BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE"; field public static final java.lang.String BIND_SCREENING_SERVICE = "android.permission.BIND_SCREENING_SERVICE"; + field public static final java.lang.String BIND_SETTINGS_SUGGESTIONS_SERVICE = "android.permission.BIND_SETTINGS_SUGGESTIONS_SERVICE"; field public static final java.lang.String BIND_TELECOM_CONNECTION_SERVICE = "android.permission.BIND_TELECOM_CONNECTION_SERVICE"; field public static final java.lang.String BIND_TEXT_SERVICE = "android.permission.BIND_TEXT_SERVICE"; field public static final java.lang.String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT"; @@ -40819,6 +40820,35 @@ package android.service.restrictions { } +package android.service.settings.suggestions { + + public final class Suggestion implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getId(); + method public android.app.PendingIntent getPendingIntent(); + method public java.lang.CharSequence getSummary(); + method public java.lang.CharSequence getTitle(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class Suggestion.Builder { + ctor public Suggestion.Builder(java.lang.String); + method public android.service.settings.suggestions.Suggestion build(); + method public android.service.settings.suggestions.Suggestion.Builder setPendingIntent(android.app.PendingIntent); + method public android.service.settings.suggestions.Suggestion.Builder setSummary(java.lang.CharSequence); + method public android.service.settings.suggestions.Suggestion.Builder setTitle(java.lang.CharSequence); + } + + public abstract class SuggestionService extends android.app.Service { + ctor public SuggestionService(); + method public android.os.IBinder onBind(android.content.Intent); + method public abstract java.util.List onGetSuggestions(); + method public abstract void onSuggestionDismissed(android.service.settings.suggestions.Suggestion); + } + +} + package android.service.textservice { public abstract class SpellCheckerService extends android.app.Service { diff --git a/core/java/android/service/settings/suggestions/ISuggestionService.aidl b/core/java/android/service/settings/suggestions/ISuggestionService.aidl new file mode 100644 index 0000000000000..423c507257c3c --- /dev/null +++ b/core/java/android/service/settings/suggestions/ISuggestionService.aidl @@ -0,0 +1,20 @@ +package android.service.settings.suggestions; + +import android.service.settings.suggestions.Suggestion; + +import java.util.List; + +/** @hide */ +interface ISuggestionService { + + /** + * Return all available suggestions. + */ + List getSuggestions() = 1; + + /** + * Dismiss a suggestion. The suggestion will not be included in future {@link #getSuggestions) + * calls. + */ + void dismissSuggestion(in Suggestion suggestion) = 2; +} \ No newline at end of file diff --git a/core/java/android/service/settings/suggestions/Suggestion.aidl b/core/java/android/service/settings/suggestions/Suggestion.aidl new file mode 100644 index 0000000000000..b26f12c5f861f --- /dev/null +++ b/core/java/android/service/settings/suggestions/Suggestion.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @hide */ +package android.service.settings.suggestions; + +parcelable Suggestion; diff --git a/core/java/android/service/settings/suggestions/Suggestion.java b/core/java/android/service/settings/suggestions/Suggestion.java new file mode 100644 index 0000000000000..f27cc2eb04121 --- /dev/null +++ b/core/java/android/service/settings/suggestions/Suggestion.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.settings.suggestions; + +import android.annotation.SystemApi; +import android.app.PendingIntent; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Data object that has information about a device suggestion. + * + * @hide + */ +@SystemApi +public final class Suggestion implements Parcelable { + + private final String mId; + private final CharSequence mTitle; + private final CharSequence mSummary; + private final PendingIntent mPendingIntent; + + /** + * Gets the id for the suggestion object. + */ + public String getId() { + return mId; + } + + /** + * Title of the suggestion that is shown to the user. + */ + public CharSequence getTitle() { + return mTitle; + } + + /** + * Optional summary describing what this suggestion controls. + */ + public CharSequence getSummary() { + return mSummary; + } + + /** + * The Intent to launch when the suggestion is activated. + */ + public PendingIntent getPendingIntent() { + return mPendingIntent; + } + + private Suggestion(Builder builder) { + mId = builder.mId; + mTitle = builder.mTitle; + mSummary = builder.mSummary; + mPendingIntent = builder.mPendingIntent; + } + + private Suggestion(Parcel in) { + mId = in.readString(); + mTitle = in.readCharSequence(); + mSummary = in.readCharSequence(); + mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader()); + } + + public static final Creator CREATOR = new Creator() { + @Override + public Suggestion createFromParcel(Parcel in) { + return new Suggestion(in); + } + + @Override + public Suggestion[] newArray(int size) { + return new Suggestion[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + dest.writeCharSequence(mTitle); + dest.writeCharSequence(mSummary); + dest.writeParcelable(mPendingIntent, flags); + } + + /** + * Builder class for {@link Suggestion}. + */ + public static class Builder { + private final String mId; + private CharSequence mTitle; + private CharSequence mSummary; + private PendingIntent mPendingIntent; + + public Builder(String id) { + if (TextUtils.isEmpty(id)) { + throw new IllegalArgumentException("Suggestion id cannot be empty"); + } + mId = id; + } + + /** + * Sets suggestion title + */ + public Builder setTitle(CharSequence title) { + mTitle = title; + return this; + } + + /** + * Sets suggestion summary + */ + public Builder setSummary(CharSequence summary) { + mSummary = summary; + return this; + } + + /** + * Sets suggestion intent + */ + public Builder setPendingIntent(PendingIntent pendingIntent) { + mPendingIntent = pendingIntent; + return this; + } + + /** + * Builds an immutable {@link Suggestion} object. + */ + public Suggestion build() { + return new Suggestion(this /* builder */); + } + } +} diff --git a/core/java/android/service/settings/suggestions/SuggestionService.java b/core/java/android/service/settings/suggestions/SuggestionService.java new file mode 100644 index 0000000000000..2a4c84c2a1cf6 --- /dev/null +++ b/core/java/android/service/settings/suggestions/SuggestionService.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.settings.suggestions; + +import android.annotation.SystemApi; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +import java.util.List; + +/** + * This is the base class for implementing suggestion service. A suggestion service is responsible + * to provide a collection of {@link Suggestion}s for the current user when queried. + * + * @hide + */ +@SystemApi +public abstract class SuggestionService extends Service { + + private static final String TAG = "SuggestionService"; + private static final boolean DEBUG = false; + + @Override + public IBinder onBind(Intent intent) { + return new ISuggestionService.Stub() { + @Override + public List getSuggestions() { + if (DEBUG) { + Log.d(TAG, "getSuggestions() " + getPackageName()); + } + return onGetSuggestions(); + } + + @Override + public void dismissSuggestion(Suggestion suggestion) { + if (DEBUG) { + Log.d(TAG, "dismissSuggestion() " + getPackageName()); + } + onSuggestionDismissed(suggestion); + } + }; + } + + /** + * Return all available suggestions. + */ + public abstract List onGetSuggestions(); + + /** + * Dismiss a suggestion. The suggestion will not be included in future + * {@link #onGetSuggestions()} calls. + * @param suggestion + */ + public abstract void onSuggestionDismissed(Suggestion suggestion); +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d6f67f6eb1a69..8122eb7de9b9c 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3144,6 +3144,11 @@ + + + diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index ab9912a438d4c..ac5d224c90309 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -1264,6 +1264,11 @@ + + diff --git a/core/tests/coretests/src/android/service/settings/suggestions/MockSuggestionService.java b/core/tests/coretests/src/android/service/settings/suggestions/MockSuggestionService.java new file mode 100644 index 0000000000000..c158b9f0bf718 --- /dev/null +++ b/core/tests/coretests/src/android/service/settings/suggestions/MockSuggestionService.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.settings.suggestions; + +import java.util.ArrayList; +import java.util.List; + +public class MockSuggestionService extends SuggestionService { + + @Override + public List onGetSuggestions() { + final List data = new ArrayList<>(); + + data.add(new Suggestion.Builder("test") + .setTitle("title") + .setSummary("summary") + .build()); + return data; + } + + @Override + public void onSuggestionDismissed(Suggestion suggestion) { + } +} diff --git a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java new file mode 100644 index 0000000000000..bc88231844178 --- /dev/null +++ b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.settings.suggestions; + +import android.content.Intent; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.rule.ServiceTestRule; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeoutException; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SuggestionServiceTest { + + @Rule + public ServiceTestRule mServiceTestRule; + private Intent mMockServiceIntent; + + @Before + public void setUp() { + mServiceTestRule = new ServiceTestRule(); + mMockServiceIntent = new Intent( + InstrumentationRegistry.getTargetContext(), + MockSuggestionService.class); + } + + @Test + public void canStartService() throws TimeoutException { + mServiceTestRule.startService(mMockServiceIntent); + // Do nothing after starting service. + } +} diff --git a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java new file mode 100644 index 0000000000000..5548e48ffddab --- /dev/null +++ b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.settings.suggestions; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Parcel; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SuggestionTest { + private static final String TEST_ID = "id"; + private static final String TEST_TITLE = "title"; + private static final String TEST_SUMMARY = "summary"; + private PendingIntent mTestIntent; + + + @Before + public void setUp() { + final Context context = InstrumentationRegistry.getContext(); + mTestIntent = PendingIntent.getActivity(context, 0 /* requestCode */, + new Intent(), 0 /* flags */); + } + + @Test + public void buildSuggestion_allFieldsShouldBeSet() { + final Suggestion suggestion = new Suggestion.Builder(TEST_ID) + .setTitle(TEST_TITLE) + .setSummary(TEST_SUMMARY) + .setPendingIntent(mTestIntent) + .build(); + + assertThat(suggestion.getId()).isEqualTo(TEST_ID); + assertThat(suggestion.getTitle()).isEqualTo(TEST_TITLE); + assertThat(suggestion.getSummary()).isEqualTo(TEST_SUMMARY); + assertThat(suggestion.getPendingIntent()).isEqualTo(mTestIntent); + } + + @Test(expected = IllegalArgumentException.class) + public void buildSuggestion_emptyKey_shouldCrash() { + new Suggestion.Builder(null) + .setTitle(TEST_TITLE) + .setSummary(TEST_SUMMARY) + .setPendingIntent(mTestIntent) + .build(); + } + + @Test + public void buildSuggestion_fromParcelable() { + final Parcel parcel = Parcel.obtain(); + final Suggestion oldSuggestion = new Suggestion.Builder(TEST_ID) + .setTitle(TEST_TITLE) + .setSummary(TEST_SUMMARY) + .setPendingIntent(mTestIntent) + .build(); + + oldSuggestion.writeToParcel(parcel, 0 /* flags */); + parcel.setDataPosition(0); + final Suggestion newSuggestion = Suggestion.CREATOR.createFromParcel(parcel); + + assertThat(newSuggestion.getId()).isEqualTo(TEST_ID); + assertThat(newSuggestion.getTitle()).isEqualTo(TEST_TITLE); + assertThat(newSuggestion.getSummary()).isEqualTo(TEST_SUMMARY); + assertThat(newSuggestion.getPendingIntent()).isEqualTo(mTestIntent); + } +}