Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 093518ee authored by Zhizhi Liu's avatar Zhizhi Liu Committed by Android (Google) Code Review
Browse files

Merge "Migrate some Suggestions related classes to SettingsLib(I)."

parents 90321a74 1a35c133
Loading
Loading
Loading
Loading
+174 −0
Original line number Original line Diff line number Diff line
/*
 * 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<Suggestion> 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();
                }
            }
        };
    }
}
+140 −0
Original line number Original line Diff line number Diff line
/*
 * 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<List<Suggestion>> {

    public interface SuggestionControllerHost {
        /**
         * Called when suggestion data fetching is ready.
         */
        void onSuggestionReady(List<Suggestion> 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<List<Suggestion>> 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<List<Suggestion>> loader, List<Suggestion> data) {
        mSuggestionLoaded = true;
        mHost.onSuggestionReady(data);
    }

    @Override
    public void onLoaderReset(Loader<List<Suggestion>> loader) {
        mSuggestionLoaded = false;
    }

    public boolean isSuggestionLoaded() {
        return mSuggestionLoaded;
    }

    public void dismissSuggestion(Suggestion suggestion) {
        mSuggestionController.dismissSuggestions(suggestion);
    }

    public void launchSuggestion(Suggestion suggestion) {
        mSuggestionController.launchSuggestion(suggestion);
    }
}
+54 −0
Original line number Original line Diff line number Diff line
/*
 * 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<List<Suggestion>> {

    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<Suggestion> result) {

    }

    @Override
    public List<Suggestion> loadInBackground() {
        final List<Suggestion> 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
+61 −0
Original line number Original line Diff line number Diff line
/*
 * 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<Suggestion> 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<Suggestion> suggestions) {
        sSuggestions = suggestions;
    }

    @Implementation
    public List<Suggestion> getSuggestions() {
        sGetSuggestionCalled = true;
        return sSuggestions;
    }
}
+132 −0
Original line number Original line Diff line number Diff line
/*
 * 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();
    }
}