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

Commit 4f99ddc1 authored by Bryce Lee's avatar Bryce Lee Committed by Android (Google) Code Review
Browse files

Merge "ObservableServiceConnection introduction."

parents 3d0dfc27 a6e6b5ff
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));
    }
}