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