diff --git a/AndroidManifest.xml b/AndroidManifest.xml index af565d6a02f..9a365dae951 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3196,6 +3196,9 @@ android:exported="true" android:permission="android.permission.DUMP" /> + + 100 101 102 - \ No newline at end of file + 103 + diff --git a/src/com/android/settings/AirplaneModeEnabler.java b/src/com/android/settings/AirplaneModeEnabler.java index 11f1a283220..f14409624d3 100644 --- a/src/com/android/settings/AirplaneModeEnabler.java +++ b/src/com/android/settings/AirplaneModeEnabler.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.os.SystemProperties; import android.os.UserHandle; @@ -51,7 +52,7 @@ public class AirplaneModeEnabler { void onAirplaneModeChanged(boolean isAirplaneModeOn); } - private Handler mHandler = new Handler() { + private Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { @@ -62,7 +63,8 @@ public class AirplaneModeEnabler { } }; - private ContentObserver mAirplaneModeObserver = new ContentObserver(new Handler()) { + private ContentObserver mAirplaneModeObserver = new ContentObserver( + new Handler(Looper.getMainLooper())) { @Override public void onChange(boolean selfChange) { onAirplaneModeChanged(); diff --git a/src/com/android/settings/network/AirplaneModePreferenceController.java b/src/com/android/settings/network/AirplaneModePreferenceController.java index b4851e6b560..7d32ece3012 100644 --- a/src/com/android/settings/network/AirplaneModePreferenceController.java +++ b/src/com/android/settings/network/AirplaneModePreferenceController.java @@ -54,6 +54,7 @@ public class AirplaneModePreferenceController extends TogglePreferenceController public AirplaneModePreferenceController(Context context, String key) { super(context, key); mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + mAirplaneModeEnabler = new AirplaneModeEnabler(mContext, mMetricsFeatureProvider, this); } public void setFragment(Fragment hostFragment) { @@ -81,7 +82,6 @@ public class AirplaneModePreferenceController extends TogglePreferenceController super.displayPreference(screen); if (isAvailable()) { mAirplaneModePreference = (SwitchPreference) screen.findPreference(getPreferenceKey()); - mAirplaneModeEnabler = new AirplaneModeEnabler(mContext, mMetricsFeatureProvider, this); } } diff --git a/src/com/android/settings/search/DatabaseIndexingManager.java b/src/com/android/settings/search/DatabaseIndexingManager.java index 0c02b6757bb..8a18efae545 100644 --- a/src/com/android/settings/search/DatabaseIndexingManager.java +++ b/src/com/android/settings/search/DatabaseIndexingManager.java @@ -59,6 +59,7 @@ import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; +import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.indexing.IndexData; import com.android.settings.search.indexing.IndexDataConverter; import com.android.settings.search.indexing.PreIndexData; diff --git a/src/com/android/settings/search/DeviceIndexFeatureProvider.java b/src/com/android/settings/search/DeviceIndexFeatureProvider.java index 2273ffd0e1d..37978df1699 100644 --- a/src/com/android/settings/search/DeviceIndexFeatureProvider.java +++ b/src/com/android/settings/search/DeviceIndexFeatureProvider.java @@ -17,70 +17,57 @@ 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.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.net.Uri; +import android.os.Build; import android.provider.Settings; -import android.util.Log; +import com.android.settings.R; import com.android.settings.slices.SettingsSliceProvider; +import java.util.List; +import java.util.Objects; + 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, - Settings.ACTION_BATTERY_SAVER_SETTINGS, - Settings.ACTION_BLUETOOTH_SETTINGS, - "android.intent.action.POWER_USAGE_SUMMARY", - Settings.ACTION_SOUND_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 = 2; + String VERSION = Build.FINGERPRINT; boolean isIndexingEnabled(); - void index(Context context, CharSequence title, Uri sliceUri, Uri launchUri); + void index(Context context, CharSequence title, Uri sliceUri, Uri launchUri, + List keywords); default void updateIndex(Context context, boolean force) { if (!isIndexingEnabled()) return; - if (!force && Settings.Secure.getInt(context.getContentResolver(), INDEX_VERSION, -1) - == VERSION) { + if (!force && Objects.equals( + Settings.Secure.getString(context.getContentResolver(), INDEX_VERSION), 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); - } - } + ComponentName jobComponent = new ComponentName(context.getPackageName(), + DeviceIndexUpdateJobService.class.getName()); + int jobId = context.getResources().getInteger(R.integer.device_index_update); + // Schedule a job so that we know it'll be able to complete, but try to run as + // soon as possible. + context.getSystemService(JobScheduler.class).schedule( + new JobInfo.Builder(jobId, jobComponent) + .setPersisted(true) + .setMinimumLatency(1) + .setOverrideDeadline(1) + .build()); - Settings.Secure.putInt(context.getContentResolver(), INDEX_VERSION, VERSION); + Settings.Secure.putString(context.getContentResolver(), INDEX_VERSION, VERSION); } static String createDeepLink(String s) { diff --git a/src/com/android/settings/search/DeviceIndexFeatureProviderImpl.java b/src/com/android/settings/search/DeviceIndexFeatureProviderImpl.java index 4564fe67d04..7a11bd46789 100644 --- a/src/com/android/settings/search/DeviceIndexFeatureProviderImpl.java +++ b/src/com/android/settings/search/DeviceIndexFeatureProviderImpl.java @@ -17,6 +17,8 @@ package com.android.settings.search; import android.content.Context; import android.net.Uri; +import java.util.List; + public class DeviceIndexFeatureProviderImpl implements DeviceIndexFeatureProvider { @Override @@ -25,7 +27,8 @@ public class DeviceIndexFeatureProviderImpl implements DeviceIndexFeatureProvide } @Override - public void index(Context context, CharSequence title, Uri sliceUri, Uri launchUri) { + public void index(Context context, CharSequence title, Uri sliceUri, Uri launchUri, + List keywords) { // Not enabled by default. } } diff --git a/src/com/android/settings/search/DeviceIndexUpdateJobService.java b/src/com/android/settings/search/DeviceIndexUpdateJobService.java new file mode 100644 index 00000000000..573dcdf5c8b --- /dev/null +++ b/src/com/android/settings/search/DeviceIndexUpdateJobService.java @@ -0,0 +1,172 @@ +/* + * 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 android.app.slice.Slice.HINT_LARGE; +import static android.app.slice.Slice.HINT_TITLE; +import static android.app.slice.SliceItem.FORMAT_TEXT; + +import static com.android.settings.search.DeviceIndexFeatureProvider.createDeepLink; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.content.ContentResolver; +import android.content.Intent; +import android.net.Uri; +import android.net.Uri.Builder; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.slices.SettingsSliceProvider; +import com.android.settings.slices.SliceDeepLinkSpringBoard; + +import java.util.Collection; +import java.util.concurrent.CountDownLatch; + +import androidx.slice.Slice; +import androidx.slice.SliceItem; +import androidx.slice.SliceManager; +import androidx.slice.SliceManager.SliceCallback; +import androidx.slice.SliceMetadata; +import androidx.slice.core.SliceQuery; +import androidx.slice.widget.ListContent; + +public class DeviceIndexUpdateJobService extends JobService { + + private static final String TAG = "DeviceIndexUpdate"; + private static final boolean DEBUG = false; + @VisibleForTesting + protected boolean mRunningJob; + + @Override + public boolean onStartJob(JobParameters params) { + if (DEBUG) Log.d(TAG, "onStartJob"); + mRunningJob = true; + Thread thread = new Thread(() -> updateIndex(params)); + thread.setPriority(Thread.MIN_PRIORITY); + thread.start(); + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + if (DEBUG) Log.d(TAG, "onStopJob " + mRunningJob); + if (mRunningJob) { + mRunningJob = false; + return true; + } + return false; + } + + @VisibleForTesting + protected void updateIndex(JobParameters params) { + if (DEBUG) Log.d(TAG, "Starting index"); + DeviceIndexFeatureProvider indexProvider = FeatureFactory.getFactory( + this).getDeviceIndexFeatureProvider(); + SliceManager manager = getSliceManager(); + Uri baseUri = new Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .build(); + Collection slices = manager.getSliceDescendants(baseUri); + if (DEBUG) Log.d(TAG, "Indexing " + slices.size() + " slices"); + + for (Uri slice : slices) { + if (!mRunningJob) { + return; + } + Slice loadedSlice = bindSliceSynchronous(manager, slice); + // TODO: Get Title APIs on SliceMetadata and use that. + SliceMetadata metaData = getMetadata(loadedSlice); + CharSequence title = findTitle(loadedSlice, metaData); + if (title != null) { + if (DEBUG) Log.d(TAG, "Indexing: " + slice + " " + title + " " + loadedSlice); + indexProvider.index(this, title, slice, Uri.parse(createDeepLink( + new Intent(SliceDeepLinkSpringBoard.ACTION_VIEW_SLICE) + .setPackage(getPackageName()) + .putExtra(SliceDeepLinkSpringBoard.EXTRA_SLICE, slice.toString()) + .toUri(Intent.URI_ANDROID_APP_SCHEME))), + metaData.getSliceKeywords()); + } + } + if (DEBUG) Log.d(TAG, "Done indexing"); + jobFinished(params, false); + } + + protected SliceManager getSliceManager() { + return SliceManager.getInstance(this); + } + + protected SliceMetadata getMetadata(Slice loadedSlice) { + return SliceMetadata.from(this, loadedSlice); + } + + protected CharSequence findTitle(Slice loadedSlice, SliceMetadata metaData) { + ListContent content = new ListContent(this, loadedSlice); + SliceItem headerItem = content.getHeaderItem(); + if (headerItem == null) { + if (content.getRowItems().size() != 0) { + headerItem = content.getRowItems().get(0); + } else { + return null; + } + } + // Look for a title, then large text, then any text at all. + SliceItem title = SliceQuery.find(headerItem, FORMAT_TEXT, HINT_TITLE, null); + if (title != null) { + return title.getText(); + } + title = SliceQuery.find(headerItem, FORMAT_TEXT, HINT_LARGE, null); + if (title != null) { + return title.getText(); + } + title = SliceQuery.find(headerItem, FORMAT_TEXT); + if (title != null) { + return title.getText(); + } + return null; + } + + protected Slice bindSliceSynchronous(SliceManager manager, Uri slice) { + final Slice[] returnSlice = new Slice[1]; + CountDownLatch latch = new CountDownLatch(1); + SliceCallback callback = new SliceCallback() { + @Override + public void onSliceUpdated(Slice s) { + try { + SliceMetadata m = SliceMetadata.from(DeviceIndexUpdateJobService.this, s); + if (m.getLoadingState() == SliceMetadata.LOADED_ALL) { + returnSlice[0] = s; + latch.countDown(); + manager.unregisterSliceCallback(slice, this); + } + } catch (Exception e) { + Log.w(TAG, slice + " cannot be indexed", e); + returnSlice[0] = s; + } + } + }; + // Register a callback until we get a loaded slice. + manager.registerSliceCallback(slice, callback); + // Trigger the first bind in case no loading is needed. + callback.onSliceUpdated(manager.bindSlice(slice)); + try { + latch.await(); + } catch (InterruptedException e) { + } + return returnSlice[0]; + } +} diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index f4496057e0b..b33308c06e7 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.graphics.drawable.Icon; import android.net.Uri; import android.net.wifi.WifiManager; +import android.os.StrictMode; import android.provider.Settings; import android.provider.SettingsSlicesContract; import android.support.annotation.VisibleForTesting; @@ -148,6 +149,11 @@ public class SettingsSliceProvider extends SliceProvider { @Override public Slice onBindSlice(Uri sliceUri) { + // TODO: Remove this when all slices are not breaking strict mode + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .permitAll() + .build()); + String path = sliceUri.getPath(); // If adding a new Slice, do not directly match Slice URIs. // Use {@link SlicesDatabaseAccessor}. @@ -277,7 +283,8 @@ public class SettingsSliceProvider extends SliceProvider { * {@link SliceData} is loaded from {@link SlicesDatabaseHelper.Tables#TABLE_SLICES_INDEX}. */ private Slice getSliceStub(Uri uri) { - return new ListBuilder(getContext(), uri).build(); + // TODO: Switch back to ListBuilder when slice loading states are fixed. + return new Slice.Builder(uri).build(); } // TODO (b/70622039) remove this when the proper wifi slice is enabled. diff --git a/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java b/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java index fcb452516bc..d02431c8f77 100644 --- a/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java +++ b/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java @@ -27,6 +27,8 @@ public class SliceDeepLinkSpringBoard extends Activity { private static final String TAG = "DeeplinkSpringboard"; public static final String INTENT = "intent"; public static final String SETTINGS = "settings"; + public static final String ACTION_VIEW_SLICE = "com.android.settings.action.VIEW_SLICE"; + public static final String EXTRA_SLICE = "slice"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -39,7 +41,14 @@ public class SliceDeepLinkSpringBoard extends Activity { } try { Intent intent = parse(uri, getPackageName()); - startActivity(intent); + if (ACTION_VIEW_SLICE.equals(intent.getAction())) { + // This shouldn't matter since the slice is shown instead of the device + // index caring about the launch uri. + Uri slice = Uri.parse(intent.getStringExtra(EXTRA_SLICE)); + Log.e(TAG, "Slice intent launched: " + slice); + } else { + startActivity(intent); + } finish(); } catch (URISyntaxException e) { Log.e(TAG, "Error decoding uri", e); diff --git a/tests/robotests/src/com/android/settings/search/DeviceIndexFeatureProviderTest.java b/tests/robotests/src/com/android/settings/search/DeviceIndexFeatureProviderTest.java index 25acc63cf02..26c2830a01c 100644 --- a/tests/robotests/src/com/android/settings/search/DeviceIndexFeatureProviderTest.java +++ b/tests/robotests/src/com/android/settings/search/DeviceIndexFeatureProviderTest.java @@ -16,12 +16,14 @@ package com.android.settings.search; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; 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 android.app.job.JobScheduler; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.SettingsRobolectricTestRunner; @@ -40,7 +42,7 @@ public class DeviceIndexFeatureProviderTest { @Before public void setUp() { FakeFeatureFactory.setupForTest(); - mActivity = Robolectric.buildActivity(Activity.class).create().visible().get(); + mActivity = spy(Robolectric.buildActivity(Activity.class).create().visible().get()); mProvider = spy(new DeviceIndexFeatureProviderImpl()); } @@ -49,14 +51,16 @@ public class DeviceIndexFeatureProviderTest { when(mProvider.isIndexingEnabled()).thenReturn(false); mProvider.updateIndex(mActivity, false); - verify(mProvider, never()).index(any(), any(), any(), any()); + verify(mProvider, never()).index(any(), any(), any(), any(), any()); } @Test public void verifyIndexing() { + JobScheduler jobScheduler = mock(JobScheduler.class); when(mProvider.isIndexingEnabled()).thenReturn(true); + when(mActivity.getSystemService(JobScheduler.class)).thenReturn(jobScheduler); mProvider.updateIndex(mActivity, false); - verify(mProvider, atLeastOnce()).index(any(), any(), any(), any()); + verify(jobScheduler).schedule(any()); } } diff --git a/tests/robotests/src/com/android/settings/search/DeviceIndexUpdateJobServiceTest.java b/tests/robotests/src/com/android/settings/search/DeviceIndexUpdateJobServiceTest.java new file mode 100644 index 00000000000..ec16893de24 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/DeviceIndexUpdateJobServiceTest.java @@ -0,0 +1,146 @@ +/* + * 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.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.ContentResolver; +import android.net.Uri; + +import com.android.settings.slices.SettingsSliceProvider; +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.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.Robolectric; + +import java.util.ArrayList; +import java.util.List; + +import androidx.slice.Slice; +import androidx.slice.SliceManager; +import androidx.slice.SliceMetadata; + +@RunWith(SettingsRobolectricTestRunner.class) +public class DeviceIndexUpdateJobServiceTest { + private static final Uri BASE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .build(); + + private Activity mActivity; + private DeviceIndexUpdateJobService mJob; + private SliceManager mSliceManager; + + @Before + public void setup() { + FakeFeatureFactory.setupForTest(); + mActivity = spy(Robolectric.buildActivity(Activity.class).create().visible().get()); + mJob = spy(new DeviceIndexUpdateJobService()); + mSliceManager = mock(SliceManager.class); + + doReturn(mActivity.getPackageName()).when(mJob).getPackageName(); + doReturn(mSliceManager).when(mJob).getSliceManager(); + doNothing().when(mJob).jobFinished(null, false); + } + + @Test + public void testGetsSlices() { + setSlices(); + + mJob.updateIndex(null); + verify(mSliceManager).getSliceDescendants(eq(BASE_URI)); + } + + @Test + public void testIndexesSlices() { + setSlices(genSlice("path1"), genSlice("path2")); + + mJob.mRunningJob = true; + mJob.updateIndex(null); + verify(mSliceManager).getSliceDescendants(eq(BASE_URI)); + + DeviceIndexFeatureProvider indexFeatureProvider = FakeFeatureFactory.getFactory(mActivity) + .getDeviceIndexFeatureProvider(); + verify(indexFeatureProvider, times(2)).index(any(), any(), any(), any(), any()); + } + + @Test + public void testDoNotIndexWithoutTitle() { + Slice testSlice = genSlice("path2"); + setSlices(genSlice("path1"), testSlice); + doReturn(null).when(mJob).findTitle(testSlice, mJob.getMetadata(testSlice)); + + mJob.mRunningJob = true; + mJob.updateIndex(null); + verify(mSliceManager).getSliceDescendants(eq(BASE_URI)); + + DeviceIndexFeatureProvider indexFeatureProvider = FakeFeatureFactory.getFactory(mActivity) + .getDeviceIndexFeatureProvider(); + verify(indexFeatureProvider, times(1)).index(any(), any(), any(), any(), any()); + } + + @Test + public void testStopIndexing() { + Slice testSlice = genSlice("path1"); + setSlices(testSlice, genSlice("path2")); + mJob.mRunningJob = true; + + doAnswer(invocation -> { + // Stop running after the first iteration + mJob.mRunningJob = false; + return testSlice; + }).when(mJob).bindSliceSynchronous(mSliceManager, testSlice.getUri()); + + mJob.updateIndex(null); + verify(mSliceManager).getSliceDescendants(eq(BASE_URI)); + + DeviceIndexFeatureProvider indexFeatureProvider = FakeFeatureFactory.getFactory(mActivity) + .getDeviceIndexFeatureProvider(); + verify(indexFeatureProvider, times(1)).index(any(), any(), any(), any(), any()); + } + + private Slice genSlice(String path) { + return new Slice.Builder(BASE_URI.buildUpon().path(path).build()).build(); + } + + private void setSlices(Slice... slice) { + List mUris = new ArrayList<>(); + for (Slice slouse : slice) { + SliceMetadata m = mock(SliceMetadata.class); + mUris.add(slouse.getUri()); + doReturn(slouse).when(mJob).bindSliceSynchronous(mSliceManager, slouse.getUri()); + doReturn(m).when(mJob).getMetadata(slouse); + doReturn(slouse.getUri().getPath()).when(mJob).findTitle(slouse, m); + } + when(mSliceManager.getSliceDescendants(BASE_URI)).thenReturn(mUris); + } + +} \ No newline at end of file