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
diff --git a/Android.mk b/Android.mk
index 76ca9b9..09e596f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -305,6 +305,7 @@
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 4a76bfb..c03d70c 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -57,6 +57,7 @@
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.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<android.service.settings.suggestions.Suggestion> 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<android.service.settings.suggestions.Suggestion> 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 0000000..423c507
--- /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<Suggestion> 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 0000000..b26f12c
--- /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 0000000..f27cc2e
--- /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<Suggestion> CREATOR = new Creator<Suggestion>() {
+ @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 0000000..2a4c84c
--- /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<Suggestion> 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<Suggestion> 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 d6f67f6..8122eb7 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3144,6 +3144,11 @@
<permission android:name="android.permission.READ_SEARCH_INDEXABLES"
android:protectionLevel="signature|privileged" />
+ <!-- @SystemApi Internal permission to allows an application to bind to suggestion service.
+ @hide -->
+ <permission android:name="android.permission.BIND_SETTINGS_SUGGESTIONS_SERVICE"
+ android:protectionLevel="signature" />
+
<!-- @SystemApi Allows applications to set a live wallpaper.
@hide XXX Change to signature once the picker is moved to its
own apk as Ghod Intended. -->
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index ab9912a..ac5d224 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1264,6 +1264,11 @@
</intent-filter>
</service>
+ <service
+ android:name="android.service.settings.suggestions.MockSuggestionService"
+ android:permission="android.permission.BIND_SETTINGS_SUGGESTIONS_SERVICE"
+ android:exported="true"/>
+
<provider android:name="android.app.activity.LocalProvider"
android:authorities="com.android.frameworks.coretests.LocalProvider">
<meta-data android:name="com.android.frameworks.coretests.string" android:value="foo" />
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 0000000..c158b9f
--- /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<Suggestion> onGetSuggestions() {
+ final List<Suggestion> 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 0000000..bc88231
--- /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 0000000..5548e48
--- /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);
+ }
+}