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

Commit a6e6b5ff authored by Bryce Lee's avatar Bryce Lee
Browse files

ObservableServiceConnection introduction.

This changelist introduces ObservableServiceConnection,
a concrete implementation of ServiceConnection that can
be observed by multiple clients for service status
change. The connection also is typed transforming the
resulting proxy into an internal type.

Bug: 202758097
Test: atest ObservableServiceConnectionTest
Change-Id: I06710531c77724d81add07291d5f52b34aa36498
parent 2ef3a7eb
Loading
Loading
Loading
Loading
+259 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.systemui.util.service;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * {@link ObservableServiceConnection} is a concrete implementation of {@link ServiceConnection}
 * that enables monitoring the status of a binder connection. It also aides in automatically
 * converting a proxy into an internal wrapper  type.
 * @param <T> The type of the wrapper over the resulting service.
 */
public class ObservableServiceConnection<T> implements ServiceConnection {
    private static final String TAG = "ObservableSvcConn";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    /**
     * An interface for converting the service proxy into a given internal wrapper type.
     * @param <T> The type of the wrapper over the resulting service.
     */
    public interface ServiceTransformer<T> {
        /**
         * Called to convert the service proxy to the wrapper type.
         * @param service The service proxy to create the wrapper type from.
         * @return The wrapper type.
         */
        T convert(IBinder service);
    }

    /**
     * An interface for listening to the connection status.
     * @param <T> The wrapper type.
     */
    public interface Callback<T> {
        /**
         * Invoked when the service has been successfully connected to.
         * @param connection The {@link ObservableServiceConnection} instance that is now connected
         * @param proxy The service proxy converted into the typed wrapper.
         */
        void onConnected(ObservableServiceConnection<T> connection, T proxy);

        /**
         * Invoked when the service has been disconnected.
         * @param connection The {@link ObservableServiceConnection} that is now disconnected.
         * @param reason The reason for the disconnection.
         */
        void onDisconnected(ObservableServiceConnection<T> connection,
                @DisconnectReason int reason);
    }

    /**
     * Disconnection was due to the resulting binding being {@code null}.
     */
    public static final int DISCONNECT_REASON_NULL_BINDING = 1;
    /**
     * Disconnection was due to the remote end disconnecting.
     */
    public static final int DISCONNECT_REASON_DISCONNECTED = 2;
    /**
     * Disconnection due to the binder dying.
     */
    public static final int DISCONNECT_REASON_BINDING_DIED = 3;
    /**
     * Disconnection from an explicit unbinding.
     */
    public static final int DISCONNECT_REASON_UNBIND = 4;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
            DISCONNECT_REASON_NULL_BINDING,
            DISCONNECT_REASON_DISCONNECTED,
            DISCONNECT_REASON_BINDING_DIED,
            DISCONNECT_REASON_UNBIND
    })
    public @interface DisconnectReason {}

    private final Context mContext;
    private final Intent mServiceIntent;
    private final int mFlags;
    private final Executor mExecutor;
    private final ServiceTransformer<T> mTransformer;
    private final ArrayList<WeakReference<Callback<T>>> mCallbacks;
    private Optional<Integer> mLastDisconnectReason;
    private T mProxy;

    private boolean mBoundCalled;

    /**
     * Default constructor for {@link ObservableServiceConnection}.
     * @param context The context from which the service will be bound with.
     * @param serviceIntent The intent to  bind service with.
     * @param flags The flags to use during the binding
     * @param executor The executor for connection callbacks to be delivered on
     * @param transformer A {@link ServiceTransformer} for transforming the resulting service
     *                    into a desired type.
     */
    public ObservableServiceConnection(Context context, Intent serviceIntent,
            @Context.BindServiceFlags int flags, @NonNull @CallbackExecutor Executor executor,
            ServiceTransformer<T> transformer) {
        mContext = context;
        mServiceIntent = serviceIntent;
        mFlags = flags;
        mExecutor = executor;
        mTransformer = transformer;
        mCallbacks = new ArrayList<>();
        mLastDisconnectReason = Optional.empty();
    }

    /**
     * Initiate binding to the service.
     * @return {@code true} if initiating binding succeed, {@code false} otherwise.
     */
    public boolean bind() {
        final boolean bindResult = mContext.bindService(mServiceIntent, mFlags, mExecutor, this);
        mBoundCalled = true;
        if (DEBUG) {
            Log.d(TAG, "bind. bound:" + bindResult);
        }
        return bindResult;
    }

    /**
     * Disconnect from the service if bound.
     */
    public void unbind() {
        if (!mBoundCalled) {
            return;
        }
        mBoundCalled = false;
        mContext.unbindService(this);
        onDisconnected(DISCONNECT_REASON_UNBIND);
    }

    /**
     * Adds a callback for receiving connection updates.
     * @param callback The {@link Callback} to receive future updates.
     */
    public void addCallback(Callback<T> callback) {
        if (DEBUG) {
            Log.d(TAG, "addCallback:" + callback);
        }

        mExecutor.execute(() -> {
            final Iterator<WeakReference<Callback<T>>> iterator = mCallbacks.iterator();

            while (iterator.hasNext()) {
                if (iterator.next().get() == callback) {
                    return;
                }
            }

            mCallbacks.add(new WeakReference<>(callback));

            // If not connected anymore, immediately inform new callback of disconnection and
            // remove.
            if (mProxy != null) {
                callback.onConnected(this, mProxy);
            } else if (mLastDisconnectReason.isPresent()) {
                callback.onDisconnected(this, mLastDisconnectReason.get());
            }
        });
    }

    /**
     * Removes previously added callback from receiving future connection updates.
     * @param callback The {@link Callback} to be removed.
     */
    public void removeCallback(Callback callback) {
        if (DEBUG) {
            Log.d(TAG, "removeCallback:" + callback);
        }

        mExecutor.execute(()-> mCallbacks.removeIf(el -> el.get() == callback));
    }

    private void onDisconnected(@DisconnectReason int reason) {
        if (DEBUG) {
            Log.d(TAG, "onDisconnected:" + reason);
        }

        if (!mBoundCalled) {
            return;
        }

        mLastDisconnectReason = Optional.of(reason);
        unbind();
        mProxy = null;

        applyToCallbacksLocked(callback-> callback.onDisconnected(this,
                mLastDisconnectReason.get()));
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        if (DEBUG) {
            Log.d(TAG, "onServiceConnected");
        }
        mProxy = mTransformer.convert(service);
        applyToCallbacksLocked(callback -> callback.onConnected(this, mProxy));
    }

    private void applyToCallbacksLocked(Consumer<Callback<T>> applicator) {
        final Iterator<WeakReference<Callback<T>>> iterator = mCallbacks.iterator();

        while (iterator.hasNext()) {
            final Callback cb = iterator.next().get();
            if (cb != null) {
                applicator.accept(cb);
            } else {
                iterator.remove();
            }
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        onDisconnected(DISCONNECT_REASON_DISCONNECTED);
    }

    @Override
    public void onBindingDied(ComponentName name) {
        onDisconnected(DISCONNECT_REASON_DISCONNECTED);
    }

    @Override
    public void onNullBinding(ComponentName name) {
        onDisconnected(DISCONNECT_REASON_NULL_BINDING);
    }
}
+148 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.systemui.util.service;

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.testing.AndroidTestingRunner;

import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Objects;

@SmallTest
@RunWith(AndroidTestingRunner.class)
public class ObservableServiceConnectionTest extends SysuiTestCase {
    static class Foo {
        int mValue;

        Foo(int value) {
            mValue = value;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Foo)) return false;
            Foo foo = (Foo) o;
            return mValue == foo.mValue;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mValue);
        }
    }

    @Mock
    Context mContext;

    @Mock
    Intent mIntent;

    @Mock
    Foo mResult;

    @Mock
    ComponentName mComponentName;

    @Mock
    IBinder mBinder;

    @Mock
    ObservableServiceConnection.ServiceTransformer<Foo> mTransformer;

    @Mock
    ObservableServiceConnection.Callback<Foo> mCallback;

    FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testConnect() {
        ObservableServiceConnection<Foo> connection = new ObservableServiceConnection<>(mContext,
                mIntent, 0, mExecutor, mTransformer);
        // Register twice to ensure only one callback occurs.
        connection.addCallback(mCallback);
        connection.addCallback(mCallback);
        mExecutor.runAllReady();
        connection.bind();

        when(mTransformer.convert(eq(mBinder))).thenReturn(mResult);

        connection.onServiceConnected(mComponentName, mBinder);

        mExecutor.runAllReady();

        // Ensure callback is informed of the translated result.
        verify(mCallback, times(1)).onConnected(eq(connection), eq(mResult));
    }

    @Test
    public void testDisconnect() {
        ObservableServiceConnection<Foo> connection = new ObservableServiceConnection<>(mContext,
                mIntent, 0, mExecutor, mTransformer);
        connection.addCallback(mCallback);
        connection.onServiceDisconnected(mComponentName);

        // Disconnects before binds should be ignored.
        verify(mCallback, never()).onDisconnected(eq(connection), anyInt());

        when(mContext.bindService(eq(mIntent), anyInt(), eq(mExecutor), eq(connection)))
                .thenReturn(true);
        connection.bind();
        connection.onServiceDisconnected(mComponentName);

        mExecutor.runAllReady();

        // Ensure proper disconnect reason reported back
        verify(mCallback).onDisconnected(eq(connection),
                eq(ObservableServiceConnection.DISCONNECT_REASON_DISCONNECTED));

        // Verify unbound from service.
        verify(mContext, times(1)).unbindService(eq(connection));

        clearInvocations(mContext);
        // Ensure unbind after disconnect has no effect on the connection
        connection.unbind();
        verify(mContext, never()).unbindService(eq(connection));
    }
}