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
This commit is contained in:
Fan Zhang
2017-08-25 17:22:31 -07:00
parent ddbc7fb0aa
commit 723bf37ab2
11 changed files with 485 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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" />

View File

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

View File

@@ -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.
}
}

View File

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