diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 27788dcbaa1..c21eaba3377 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -255,6 +255,8 @@ android:value="com.android.settings.wifi.WifiSettings" /> + + + + + + + + + + + android:exported="true" + android:grantUriPermissions="true"> + + + + + + + indexProvider.updateIndex(SettingsActivity.this, false /* force */)); + } + private void doUpdateTilesList() { PackageManager pm = getPackageManager(); final UserManager um = UserManager.get(this); diff --git a/src/com/android/settings/overlay/FeatureFactory.java b/src/com/android/settings/overlay/FeatureFactory.java index 7cf437f3439..110d204ed93 100644 --- a/src/com/android/settings/overlay/FeatureFactory.java +++ b/src/com/android/settings/overlay/FeatureFactory.java @@ -30,6 +30,7 @@ import com.android.settings.enterprise.EnterprisePrivacyFeatureProvider; import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.gestures.AssistGestureFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProvider; +import com.android.settings.search.DeviceIndexFeatureProvider; import com.android.settings.search.SearchFeatureProvider; import com.android.settings.security.SecurityFeatureProvider; import com.android.settings.slices.SlicesFeatureProvider; @@ -106,6 +107,8 @@ public abstract class FeatureFactory { public abstract AccountFeatureProvider getAccountFeatureProvider(); + public abstract DeviceIndexFeatureProvider getDeviceIndexFeatureProvider(); + public static final class FactoryNotFoundException extends RuntimeException { public FactoryNotFoundException(Throwable throwable) { super("Unable to create factory. Did you misconfigure Proguard?", throwable); diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.java b/src/com/android/settings/overlay/FeatureFactoryImpl.java index 5fc8627dded..c521eb80d53 100644 --- a/src/com/android/settings/overlay/FeatureFactoryImpl.java +++ b/src/com/android/settings/overlay/FeatureFactoryImpl.java @@ -41,6 +41,8 @@ import com.android.settings.gestures.AssistGestureFeatureProvider; import com.android.settings.gestures.AssistGestureFeatureProviderImpl; import com.android.settings.localepicker.LocaleFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProviderImpl; +import com.android.settings.search.DeviceIndexFeatureProvider; +import com.android.settings.search.DeviceIndexFeatureProviderImpl; import com.android.settings.search.SearchFeatureProvider; import com.android.settings.search.SearchFeatureProviderImpl; import com.android.settings.security.SecurityFeatureProvider; @@ -75,6 +77,7 @@ public class FeatureFactoryImpl extends FeatureFactory { private BluetoothFeatureProvider mBluetoothFeatureProvider; private SlicesFeatureProvider mSlicesFeatureProvider; private AccountFeatureProvider mAccountFeatureProvider; + private DeviceIndexFeatureProviderImpl mDeviceIndexFeatureProvider; @Override public SupportFeatureProvider getSupportFeatureProvider(Context context) { @@ -208,4 +211,12 @@ public class FeatureFactoryImpl extends FeatureFactory { } return mAccountFeatureProvider; } + + @Override + public DeviceIndexFeatureProvider getDeviceIndexFeatureProvider() { + if (mDeviceIndexFeatureProvider == null) { + mDeviceIndexFeatureProvider = new DeviceIndexFeatureProviderImpl(); + } + return mDeviceIndexFeatureProvider; + } } diff --git a/src/com/android/settings/search/DeviceIndexFeatureProvider.java b/src/com/android/settings/search/DeviceIndexFeatureProvider.java new file mode 100644 index 00000000000..690943ee7a4 --- /dev/null +++ b/src/com/android/settings/search/DeviceIndexFeatureProvider.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018 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.settings.search; + +import static com.android.settings.slices.SliceDeepLinkSpringBoard.INTENT; +import static com.android.settings.slices.SliceDeepLinkSpringBoard.SETTINGS; + +import android.app.slice.SliceManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.provider.Settings; +import android.util.Log; + +import com.android.settings.slices.SettingsSliceProvider; + +public interface DeviceIndexFeatureProvider { + + // TODO: Remove this and index all action and intent slices through search index. + String[] ACTIONS_TO_INDEX = new String[]{ + Settings.ACTION_WIFI_SETTINGS, + }; + + String TAG = "DeviceIndex"; + + String INDEX_VERSION = "settings:index_version"; + + // Increment when new items are added to ensure they get pushed to the device index. + int VERSION = 1; + + boolean isIndexingEnabled(); + + void index(Context context, CharSequence title, Uri sliceUri, Uri launchUri); + + default void updateIndex(Context context, boolean force) { + if (!isIndexingEnabled()) return; + + if (!force && Settings.Secure.getInt(context.getContentResolver(), INDEX_VERSION, -1) + == VERSION) { + // No need to update. + return; + } + + PackageManager pm = context.getPackageManager(); + for (String action : ACTIONS_TO_INDEX) { + Intent intent = new Intent(action); + intent.setPackage(context.getPackageName()); + ResolveInfo activity = pm.resolveActivity(intent, PackageManager.GET_META_DATA); + if (activity == null) { + Log.e(TAG, "Unable to resolve " + action); + continue; + } + String sliceUri = activity.activityInfo.metaData + .getString(SliceManager.SLICE_METADATA_KEY); + if (sliceUri != null) { + Log.d(TAG, "Intent: " + createDeepLink(intent.toUri(Intent.URI_ANDROID_APP_SCHEME))); + index(context, activity.activityInfo.loadLabel(pm), + Uri.parse(sliceUri), + Uri.parse(createDeepLink(intent.toUri(Intent.URI_ANDROID_APP_SCHEME)))); + } else { + Log.e(TAG, "No slice uri found for " + activity.activityInfo.name); + } + } + + Settings.Secure.putInt(context.getContentResolver(), INDEX_VERSION, VERSION); + } + + static String createDeepLink(String s) { + return new Uri.Builder().scheme(SETTINGS) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendQueryParameter(INTENT, s) + .build() + .toString(); + } +} diff --git a/src/com/android/settings/search/DeviceIndexFeatureProviderImpl.java b/src/com/android/settings/search/DeviceIndexFeatureProviderImpl.java new file mode 100644 index 00000000000..4564fe67d04 --- /dev/null +++ b/src/com/android/settings/search/DeviceIndexFeatureProviderImpl.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 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.settings.search; + +import android.content.Context; +import android.net.Uri; + +public class DeviceIndexFeatureProviderImpl implements DeviceIndexFeatureProvider { + + @Override + public boolean isIndexingEnabled() { + return false; + } + + @Override + public void index(Context context, CharSequence title, Uri sliceUri, Uri launchUri) { + // Not enabled by default. + } +} diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index 802f1e4a86d..8b3bdbd2392 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -17,27 +17,26 @@ package com.android.settings.slices; import android.app.PendingIntent; - -import android.content.ContentResolver; +import android.app.slice.SliceManager; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Icon; import android.net.Uri; import android.net.wifi.WifiManager; -import android.provider.SettingsSlicesContract; import android.support.annotation.VisibleForTesting; import android.util.Log; import com.android.settings.R; import com.android.settingslib.utils.ThreadUtils; +import java.net.URISyntaxException; import java.util.Map; import java.util.WeakHashMap; import androidx.slice.Slice; import androidx.slice.SliceProvider; -import androidx.slice.builders.SliceAction; import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; /** * A {@link SliceProvider} for Settings to enabled inline results in system apps. @@ -106,6 +105,17 @@ public class SettingsSliceProvider extends SliceProvider { return true; } + @Override + public Uri onMapIntentToUri(Intent intent) { + try { + return getContext().getSystemService(SliceManager.class).mapIntentToUri( + SliceDeepLinkSpringBoard.parse( + intent.getData(), getContext().getPackageName())); + } catch (URISyntaxException e) { + return null; + } + } + @Override public Slice onBindSlice(Uri sliceUri) { String path = sliceUri.getPath(); diff --git a/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java b/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java new file mode 100644 index 00000000000..fcb452516bc --- /dev/null +++ b/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 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.settings.slices; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import java.net.URISyntaxException; + +public class SliceDeepLinkSpringBoard extends Activity { + + private static final String TAG = "DeeplinkSpringboard"; + public static final String INTENT = "intent"; + public static final String SETTINGS = "settings"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Uri uri = getIntent().getData(); + if (uri == null) { + Log.e(TAG, "No data found"); + finish(); + return; + } + try { + Intent intent = parse(uri, getPackageName()); + startActivity(intent); + finish(); + } catch (URISyntaxException e) { + Log.e(TAG, "Error decoding uri", e); + finish(); + } + } + + public static Intent parse(Uri uri, String pkg) throws URISyntaxException { + Intent intent = Intent.parseUri(uri.getQueryParameter(INTENT), + Intent.URI_ANDROID_APP_SCHEME); + // Start with some really strict constraints and loosen them if we need to. + // Don't allow components. + intent.setComponent(null); + // Clear out the extras. + if (intent.getExtras() != null) { + intent.getExtras().clear(); + } + // Make sure this points at Settings. + intent.setPackage(pkg); + return intent; + } +} diff --git a/tests/robotests/src/com/android/settings/search/DeviceIndexFeatureProviderTest.java b/tests/robotests/src/com/android/settings/search/DeviceIndexFeatureProviderTest.java new file mode 100644 index 00000000000..25acc63cf02 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/DeviceIndexFeatureProviderTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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.settings.search; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; + +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; + +@RunWith(SettingsRobolectricTestRunner.class) +public class DeviceIndexFeatureProviderTest { + + private DeviceIndexFeatureProvider mProvider; + private Activity mActivity; + + @Before + public void setUp() { + FakeFeatureFactory.setupForTest(); + mActivity = Robolectric.buildActivity(Activity.class).create().visible().get(); + mProvider = spy(new DeviceIndexFeatureProviderImpl()); + } + + @Test + public void verifyDisabled() { + when(mProvider.isIndexingEnabled()).thenReturn(false); + + mProvider.updateIndex(mActivity, false); + verify(mProvider, never()).index(any(), any(), any(), any()); + } + + @Test + public void verifyIndexing() { + when(mProvider.isIndexingEnabled()).thenReturn(true); + + mProvider.updateIndex(mActivity, false); + verify(mProvider, atLeastOnce()).index(any(), any(), any(), any()); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java index 601164032d5..8945af924e7 100644 --- a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java +++ b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java @@ -33,6 +33,7 @@ import com.android.settings.localepicker.LocaleFeatureProvider; import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.SupportFeatureProvider; import com.android.settings.overlay.SurveyFeatureProvider; +import com.android.settings.search.DeviceIndexFeatureProvider; import com.android.settings.search.SearchFeatureProvider; import com.android.settings.security.SecurityFeatureProvider; import com.android.settings.slices.SlicesFeatureProvider; @@ -63,6 +64,7 @@ public class FakeFeatureFactory extends FeatureFactory { public final SlicesFeatureProvider slicesFeatureProvider; public SearchFeatureProvider searchFeatureProvider; public final AccountFeatureProvider mAccountFeatureProvider; + public final DeviceIndexFeatureProvider deviceIndexFeatureProvider; /** * Call this in {@code @Before} method of the test class to use fake factory. @@ -101,6 +103,7 @@ public class FakeFeatureFactory extends FeatureFactory { bluetoothFeatureProvider = mock(BluetoothFeatureProvider.class); slicesFeatureProvider = mock(SlicesFeatureProvider.class); mAccountFeatureProvider = mock(AccountFeatureProvider.class); + deviceIndexFeatureProvider = mock(DeviceIndexFeatureProvider.class); } @Override @@ -182,4 +185,9 @@ public class FakeFeatureFactory extends FeatureFactory { public AccountFeatureProvider getAccountFeatureProvider() { return mAccountFeatureProvider; } + + @Override + public DeviceIndexFeatureProvider getDeviceIndexFeatureProvider() { + return deviceIndexFeatureProvider; + } }