From 1a35c133d23422d09d56f1edcd6b222d83b39170 Mon Sep 17 00:00:00 2001 From: Zhizhi Liu Date: Thu, 18 Jan 2018 11:06:10 -0800 Subject: [PATCH] Migrate some Suggestions related classes to SettingsLib(I). Copy classes to SettingsLib from Settings so the code can be shared with TvSettings. Test: mmma SettingsLib/tests/robotests Change-Id: If2d7db9d998af9d262a5e225c3b7a5cacb053542 --- .../suggestions/SuggestionController.java | 174 +++++++++++++++++++++ .../suggestions/SuggestionControllerMixin.java | 140 +++++++++++++++++ .../settingslib/suggestions/SuggestionLoader.java | 54 +++++++ .../suggestions/ShadowSuggestionController.java | 61 ++++++++ .../suggestions/SuggestionControllerMixinTest.java | 132 ++++++++++++++++ 5 files changed, 561 insertions(+) create mode 100644 packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java create mode 100644 packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java create mode 100644 packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java create mode 100644 packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/ShadowSuggestionController.java create mode 100644 packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/SuggestionControllerMixinTest.java diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java new file mode 100644 index 000000000000..f740f7c01ce1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java @@ -0,0 +1,174 @@ +/* + * 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 com.android.settingslib.suggestions; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.RemoteException; +import android.service.settings.suggestions.ISuggestionService; +import android.service.settings.suggestions.Suggestion; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import java.util.List; + +/** + * A controller class to access suggestion data. + */ +public class SuggestionController { + + /** + * Callback interface when service is connected/disconnected. + */ + public interface ServiceConnectionListener { + /** + * Called when service is connected. + */ + void onServiceConnected(); + + /** + * Called when service is disconnected. + */ + void onServiceDisconnected(); + } + + private static final String TAG = "SuggestionController"; + private static final boolean DEBUG = false; + + private final Context mContext; + private final Intent mServiceIntent; + + private ServiceConnection mServiceConnection; + private ISuggestionService mRemoteService; + private ServiceConnectionListener mConnectionListener; + + /** + * Create a new controller instance. + * + * @param context caller context + * @param service The component name for service. + * @param listener listener to receive service connected/disconnected event. + */ + public SuggestionController(Context context, ComponentName service, + ServiceConnectionListener listener) { + mContext = context.getApplicationContext(); + mConnectionListener = listener; + mServiceIntent = new Intent().setComponent(service); + mServiceConnection = createServiceConnection(); + } + + /** + * Start the controller. + */ + public void start() { + mContext.bindServiceAsUser(mServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE, + android.os.Process.myUserHandle()); + } + + /** + * Stop the controller. + */ + public void stop() { + if (mRemoteService != null) { + mRemoteService = null; + mContext.unbindService(mServiceConnection); + } + } + + /** + * Get setting suggestions. + */ + @Nullable + @WorkerThread + public List getSuggestions() { + if (!isReady()) { + return null; + } + try { + return mRemoteService.getSuggestions(); + } catch (NullPointerException e) { + Log.w(TAG, "mRemote service detached before able to query", e); + return null; + } catch (RemoteException e) { + Log.w(TAG, "Error when calling getSuggestion()", e); + return null; + } + } + + public void dismissSuggestions(Suggestion suggestion) { + if (!isReady()) { + Log.w(TAG, "SuggestionController not ready, cannot dismiss " + suggestion.getId()); + return; + } + try { + mRemoteService.dismissSuggestion(suggestion); + } catch (RemoteException e) { + Log.w(TAG, "Error when calling dismissSuggestion()", e); + } + } + + public void launchSuggestion(Suggestion suggestion) { + if (!isReady()) { + Log.w(TAG, "SuggestionController not ready, cannot launch " + suggestion.getId()); + return; + } + + try { + mRemoteService.launchSuggestion(suggestion); + } catch (RemoteException e) { + Log.w(TAG, "Error when calling launchSuggestion()", e); + } + } + + /** + * Whether or not the manager is ready + */ + private boolean isReady() { + return mRemoteService != null; + } + + /** + * Create a new {@link ServiceConnection} object to handle service connect/disconnect event. + */ + private ServiceConnection createServiceConnection() { + return new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (DEBUG) { + Log.d(TAG, "Service is connected"); + } + mRemoteService = ISuggestionService.Stub.asInterface(service); + if (mConnectionListener != null) { + mConnectionListener.onServiceConnected(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + if (mConnectionListener != null) { + mRemoteService = null; + mConnectionListener.onServiceDisconnected(); + } + } + }; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java new file mode 100644 index 000000000000..46fc32fa43cc --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java @@ -0,0 +1,140 @@ +/* + * 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 com.android.settingslib.suggestions; + +import android.app.LoaderManager; +import android.arch.lifecycle.OnLifecycleEvent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Loader; +import android.os.Bundle; +import android.service.settings.suggestions.Suggestion; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.util.List; + +/** + * Manages IPC communication to SettingsIntelligence for suggestion related services. + */ +public class SuggestionControllerMixin implements SuggestionController.ServiceConnectionListener, + android.arch.lifecycle.LifecycleObserver, LoaderManager.LoaderCallbacks> { + + public interface SuggestionControllerHost { + /** + * Called when suggestion data fetching is ready. + */ + void onSuggestionReady(List data); + + /** + * Returns {@link LoaderManager} associated with the host. If host is not attached to + * activity then return null. + */ + @Nullable + LoaderManager getLoaderManager(); + } + + private static final String TAG = "SuggestionCtrlMixin"; + private static final boolean DEBUG = false; + + private final Context mContext; + private final SuggestionController mSuggestionController; + private final SuggestionControllerHost mHost; + + private boolean mSuggestionLoaded; + + public SuggestionControllerMixin(Context context, SuggestionControllerHost host, + Lifecycle lifecycle, ComponentName componentName) { + mContext = context.getApplicationContext(); + mHost = host; + mSuggestionController = new SuggestionController(mContext, componentName, + this /* serviceConnectionListener */); + if (lifecycle != null) { + lifecycle.addObserver(this); + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + public void onStart() { + if (DEBUG) { + Log.d(TAG, "SuggestionController started"); + } + mSuggestionController.start(); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop() { + if (DEBUG) { + Log.d(TAG, "SuggestionController stopped."); + } + mSuggestionController.stop(); + } + + @Override + public void onServiceConnected() { + final LoaderManager loaderManager = mHost.getLoaderManager(); + if (loaderManager != null) { + loaderManager.restartLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS, + null /* args */, this /* callback */); + } + } + + @Override + public void onServiceDisconnected() { + if (DEBUG) { + Log.d(TAG, "SuggestionService disconnected"); + } + final LoaderManager loaderManager = mHost.getLoaderManager(); + if (loaderManager != null) { + loaderManager.destroyLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS); + } + } + + @Override + public Loader> onCreateLoader(int id, Bundle args) { + if (id == SuggestionLoader.LOADER_ID_SUGGESTIONS) { + mSuggestionLoaded = false; + return new SuggestionLoader(mContext, mSuggestionController); + } + throw new IllegalArgumentException("This loader id is not supported " + id); + } + + @Override + public void onLoadFinished(Loader> loader, List data) { + mSuggestionLoaded = true; + mHost.onSuggestionReady(data); + } + + @Override + public void onLoaderReset(Loader> loader) { + mSuggestionLoaded = false; + } + + public boolean isSuggestionLoaded() { + return mSuggestionLoaded; + } + + public void dismissSuggestion(Suggestion suggestion) { + mSuggestionController.dismissSuggestions(suggestion); + } + + public void launchSuggestion(Suggestion suggestion) { + mSuggestionController.launchSuggestion(suggestion); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java new file mode 100644 index 000000000000..9c1af1edc778 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java @@ -0,0 +1,54 @@ +/* + * 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 com.android.settingslib.suggestions; + +import android.content.Context; +import android.service.settings.suggestions.Suggestion; +import android.util.Log; + +import com.android.settingslib.utils.AsyncLoader; + +import java.util.List; + +public class SuggestionLoader extends AsyncLoader> { + + public static final int LOADER_ID_SUGGESTIONS = 42; + private static final String TAG = "SuggestionLoader"; + + private final SuggestionController mSuggestionController; + + public SuggestionLoader(Context context, SuggestionController controller) { + super(context); + mSuggestionController = controller; + } + + @Override + protected void onDiscardResult(List result) { + + } + + @Override + public List loadInBackground() { + final List data = mSuggestionController.getSuggestions(); + if (data == null) { + Log.d(TAG, "data is null"); + } else { + Log.d(TAG, "data size " + data.size()); + } + return data; + } +} \ No newline at end of file diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/ShadowSuggestionController.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/ShadowSuggestionController.java new file mode 100644 index 000000000000..61bc83b8f72e --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/ShadowSuggestionController.java @@ -0,0 +1,61 @@ +/* + * 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 com.android.settingslib.suggestions; + +import android.service.settings.suggestions.Suggestion; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +import java.util.List; + +@Implements(SuggestionController.class) +public class ShadowSuggestionController { + + public static boolean sStartCalled; + public static boolean sStopCalled; + public static boolean sGetSuggestionCalled; + + public static List sSuggestions; + + public static void reset() { + sStartCalled = false; + sStopCalled = false; + sGetSuggestionCalled = false; + sSuggestions = null; + } + + @Implementation + public void start() { + sStartCalled = true; + } + + @Implementation + public void stop() { + sStopCalled = true; + } + + public static void setSuggestion(List suggestions) { + sSuggestions = suggestions; + } + + @Implementation + public List getSuggestions() { + sGetSuggestionCalled = true; + return sSuggestions; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/SuggestionControllerMixinTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/SuggestionControllerMixinTest.java new file mode 100644 index 000000000000..ed1c405f8a81 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/SuggestionControllerMixinTest.java @@ -0,0 +1,132 @@ +/* + * 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 com.android.settingslib.suggestions; + +import static android.arch.lifecycle.Lifecycle.Event.ON_START; +import static android.arch.lifecycle.Lifecycle.Event.ON_STOP; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.LoaderManager; +import android.arch.lifecycle.LifecycleOwner; +import android.content.ComponentName; +import android.content.Context; + +import com.android.settingslib.TestConfig; +import com.android.settingslib.SettingsLibRobolectricTestRunner; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(SettingsLibRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION, + shadows = { + ShadowSuggestionController.class + }) +public class SuggestionControllerMixinTest { + + @Mock + private SuggestionControllerMixin.SuggestionControllerHost mHost; + + private Context mContext; + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + private SuggestionControllerMixin mMixin; + private ComponentName mComponentName; + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + mComponentName = new ComponentName( + "com.android.settings.intelligence", + "com.android.settings.intelligence.suggestions.SuggestionService"); + } + + @After + public void tearDown() { + ShadowSuggestionController.reset(); + } + + @Test + public void goThroughLifecycle_onStartStop_shouldStartStopController() { + mMixin = new SuggestionControllerMixin(mContext, mHost, mLifecycle, mComponentName); + + mLifecycle.handleLifecycleEvent(ON_START); + assertThat(ShadowSuggestionController.sStartCalled).isTrue(); + + mLifecycle.handleLifecycleEvent(ON_STOP); + assertThat(ShadowSuggestionController.sStopCalled).isTrue(); + } + + @Test + public void onServiceConnected_shouldGetSuggestion() { + final LoaderManager loaderManager = mock(LoaderManager.class); + when(mHost.getLoaderManager()).thenReturn(loaderManager); + + mMixin = new SuggestionControllerMixin(mContext, mHost, mLifecycle, mComponentName); + mMixin.onServiceConnected(); + + verify(loaderManager).restartLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS, + null /* args */, mMixin /* callback */); + } + + @Test + public void onServiceConnected_hostNotAttached_shouldDoNothing() { + when(mHost.getLoaderManager()).thenReturn(null); + + mMixin = new SuggestionControllerMixin(mContext, mHost, mLifecycle, mComponentName); + mMixin.onServiceConnected(); + + verify(mHost).getLoaderManager(); + } + + @Test + public void onServiceDisconnected_hostNotAttached_shouldDoNothing() { + when(mHost.getLoaderManager()).thenReturn(null); + + mMixin = new SuggestionControllerMixin(mContext, mHost, mLifecycle, mComponentName); + mMixin.onServiceDisconnected(); + + verify(mHost).getLoaderManager(); + } + + @Test + public void doneLoadingg_shouldSetSuggestionLoaded() { + mMixin = new SuggestionControllerMixin(mContext, mHost, mLifecycle, mComponentName); + + mMixin.onLoadFinished(mock(SuggestionLoader.class), null); + + assertThat(mMixin.isSuggestionLoaded()).isTrue(); + + mMixin.onLoaderReset(mock(SuggestionLoader.class)); + + assertThat(mMixin.isSuggestionLoaded()).isFalse(); + } +} -- 2.11.0