ConnectionManager.java

/*
 * 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 androidx.health.platform.client.impl.ipc.internal;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.VisibleForTesting;

import java.util.HashMap;
import java.util.Map;


/**
 * Manages connections to a service in a different process.
 *
 * @hide
 */
@RestrictTo(Scope.LIBRARY)
public final class ConnectionManager implements Handler.Callback, ServiceConnection.Callback {
    private static final String TAG = "ConnectionManager";

    @VisibleForTesting
    static final int UNBIND_IDLE_DELAY_MILLISECONDS = 15_000;

    private static final int MSG_CONNECTED = 1;
    private static final int MSG_DISCONNECTED = 2;
    private static final int MSG_EXECUTE = 3;
    private static final int MSG_REGISTER_LISTENER = 4;
    private static final int MSG_UNREGISTER_LISTENER = 5;

    private static final int MSG_UNBIND = 6;

    private final Context mContext;
    private final Handler mHandler;
    private final Map<String, ServiceConnection> mServiceConnectionMap = new HashMap<>();

    private boolean mBindToSelfEnabled;

    public ConnectionManager(Context context, Looper looper) {
        this.mContext = context;
        this.mHandler = new Handler(looper, this);
    }

    /**
     * Schedules operation for execution
     *
     * @param operation Operation prepared for scheduling on the connection queue.
     */
    public void scheduleForExecution(QueueOperation operation) {
        mHandler.sendMessage(mHandler.obtainMessage(MSG_EXECUTE, operation));
    }

    /**
     * Registers a listener by executing an operation represented by the {@link QueueOperation}.
     *
     * @param listenerKey       Key based on which listeners will be distinguished.
     * @param registerOperation Queue operation executed against the corresponding connection to
     *                          register the listener. Will be used to re-register when
     *                          connection is lost.
     */
    public void registerListener(ListenerKey listenerKey, QueueOperation registerOperation) {
        mHandler.sendMessage(
                mHandler.obtainMessage(
                        MSG_REGISTER_LISTENER, new ListenerHolder(listenerKey, registerOperation)));
    }

    /**
     * Unregisters a listener by executing an operation represented by the {@link QueueOperation}.
     *
     * @param listenerKey         Key based on which listeners will be distinguished.
     * @param unregisterOperation Queue operation executed against the corresponding connection to
     *                            unregister the listener.
     */
    public void unregisterListener(ListenerKey listenerKey, QueueOperation unregisterOperation) {
        mHandler.sendMessage(
                mHandler.obtainMessage(
                        MSG_UNREGISTER_LISTENER,
                        new ListenerHolder(listenerKey, unregisterOperation)));
    }

    /**
     * Delays any existing {@code MSG_UNBIND} message to {@code UNBIND_IDLE_DELAY_MILLISECONDS}
     * later.
     *
     * @param serviceConnection Service connection that we will auto-unbind.
     */
    void delayIdleServiceUnbindCheck(@NonNull ServiceConnection serviceConnection) {
        mHandler.removeMessages(MSG_UNBIND, serviceConnection);
        mHandler.sendMessageDelayed(
                mHandler.obtainMessage(MSG_UNBIND, serviceConnection),
                UNBIND_IDLE_DELAY_MILLISECONDS);
    }

    @Override
    public void onConnected(ServiceConnection connection) {
        mHandler.sendMessage(mHandler.obtainMessage(MSG_CONNECTED, connection));
    }

    @Override
    public void onDisconnected(ServiceConnection connection, long reconnectDelayMs) {
        mHandler.sendMessageDelayed(
                mHandler.obtainMessage(MSG_DISCONNECTED, connection), reconnectDelayMs);
    }

    @Override
    public boolean isBindToSelfEnabled() {
        return mBindToSelfEnabled;
    }

    @Override
    public boolean handleMessage(@NonNull Message msg) {
        switch (msg.what) {
            case MSG_CONNECTED:
                ServiceConnection serviceConnection = ((ServiceConnection) msg.obj);
                serviceConnection.reRegisterAllListeners();
                serviceConnection.refreshServiceVersion();
                serviceConnection.flushQueue();
                delayIdleServiceUnbindCheck(serviceConnection);
                return true;
            case MSG_DISCONNECTED:
                ((ServiceConnection) msg.obj).maybeReconnect();
                return true;
            case MSG_EXECUTE:
                QueueOperation queueOperation = (QueueOperation) msg.obj;
                ServiceConnection serviceConnectionForExecute =
                        getConnection(queueOperation.getConnectionConfiguration());
                serviceConnectionForExecute.enqueue(queueOperation);
                delayIdleServiceUnbindCheck(serviceConnectionForExecute);
                return true;
            case MSG_REGISTER_LISTENER:
                ListenerHolder registerListenerHolder = (ListenerHolder) msg.obj;
                ServiceConnection serviceConnectionForRegister =
                        getConnection(
                                registerListenerHolder
                                        .getListenerOperation()
                                        .getConnectionConfiguration());
                serviceConnectionForRegister
                        .registerListener(
                                registerListenerHolder.getListenerKey(),
                                registerListenerHolder.getListenerOperation());
                delayIdleServiceUnbindCheck(serviceConnectionForRegister);
                return true;
            case MSG_UNREGISTER_LISTENER:
                ListenerHolder unregisterListenerHolder = (ListenerHolder) msg.obj;
                ServiceConnection serviceConnectionForUnregister =
                        getConnection(
                                unregisterListenerHolder
                                        .getListenerOperation()
                                        .getConnectionConfiguration());
                serviceConnectionForUnregister
                        .unregisterListener(
                                unregisterListenerHolder.getListenerKey(),
                                unregisterListenerHolder.getListenerOperation());
                delayIdleServiceUnbindCheck(serviceConnectionForUnregister);
                return true;
            case MSG_UNBIND:
                ServiceConnection serviceConnectionToClear = ((ServiceConnection) msg.obj);
                if (mHandler.hasMessages(MSG_EXECUTE)
                        || mHandler.hasMessages(MSG_REGISTER_LISTENER)
                        || mHandler.hasMessages(MSG_UNREGISTER_LISTENER)) {
                    return true;
                }
                boolean isIdle = serviceConnectionToClear.clearConnectionIfIdle();
                if (!isIdle) {
                    delayIdleServiceUnbindCheck(serviceConnectionToClear);
                }
                return true;
            default:
                Log.e(TAG, "Received unknown message: " + msg.what);
                return false;
        }
    }

    public void setBindToSelf(boolean bindToSelfEnabled) {
        this.mBindToSelfEnabled = bindToSelfEnabled;
    }

    @VisibleForTesting
    ServiceConnection getConnection(ConnectionConfiguration connectionConfiguration) {
        String connectionKey = connectionConfiguration.getKey();
        ServiceConnection serviceConnection = mServiceConnectionMap.get(connectionKey);
        if (serviceConnection == null) {
            serviceConnection =
                    new ServiceConnection(
                            mContext, connectionConfiguration, new DefaultExecutionTracker(), this);
            mServiceConnectionMap.put(connectionKey, serviceConnection);
        }
        return serviceConnection;
    }

    private static class ListenerHolder {
        private final ListenerKey mListenerKey;
        private final QueueOperation mListenerOperation;

        ListenerHolder(ListenerKey listenerKey, QueueOperation listenerOperation) {
            this.mListenerKey = listenerKey;
            this.mListenerOperation = listenerOperation;
        }

        ListenerKey getListenerKey() {
            return mListenerKey;
        }

        QueueOperation getListenerOperation() {
            return mListenerOperation;
        }
    }
}