All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.server.location.contexthub.ContextHubClientBroker Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * Copyright (C) 2020 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.server.location.contexthub;

import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.hardware.location.ContextHubManager.AUTHORIZATION_DENIED;
import static android.hardware.location.ContextHubManager.AUTHORIZATION_DENIED_GRACE_PERIOD;
import static android.hardware.location.ContextHubManager.AUTHORIZATION_GRANTED;

import android.Manifest;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.compat.Compatibility;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.Context;
import android.content.Intent;
import android.hardware.contexthub.V1_0.ContextHubMsg;
import android.hardware.contexthub.V1_0.Result;
import android.hardware.location.ContextHubInfo;
import android.hardware.location.ContextHubManager;
import android.hardware.location.ContextHubTransaction;
import android.hardware.location.IContextHubClient;
import android.hardware.location.IContextHubClientCallback;
import android.hardware.location.IContextHubTransactionCallback;
import android.hardware.location.NanoAppMessage;
import android.hardware.location.NanoAppState;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import android.util.proto.ProtoOutputStream;

import com.android.server.location.ClientBrokerProto;

import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

/**
 * A class that acts as a broker for the ContextHubClient, which handles messaging and life-cycle
 * notification callbacks. This class implements the IContextHubClient object, and the implemented
 * APIs must be thread-safe.
 *
 * Additionally, this class is responsible for enforcing permissions usage and attribution are
 * handled appropriately for a given client. In general, this works as follows:
 *
 * Client sending a message to a nanoapp:
 * 1) When initially sending a message to nanoapps, clients are by default in a grace period state
 *    which allows them to always send their first message to nanoapps. This is done to allow
 *    clients (especially callback clients) to reset their conection to the nanoapp if they are
 *    killed / restarted (e.g. following a permission revocation).
 * 2) After the initial message is sent, a check of permissions state is performed. If the
 *    client doesn't have permissions to communicate, it is placed into the denied grace period
 *    state and notified so that it can clean up its communication before it is completely denied
 *    access.
 * 3) For subsequent messages, the auth state is checked synchronously and messages are denied if
 *    the client is denied authorization
 *
 * Client receiving a message from a nanoapp:
 * 1) If a nanoapp sends a message to the client, the authentication state is checked synchronously.
 *    If there has been no message between the two before, the auth state is assumed granted.
 * 2) The broker then checks that the client has all permissions the nanoapp requires and attributes
 *    all permissions required to consume the message being sent. If both of those checks pass, then
 *    the message is delivered. Otherwise, it's dropped.
 *
 * Client losing or gaining permissions (callback client):
 * 1) Clients are killed when they lose permissions. This will cause callback clients to completely
 *    disconnect from the service. When they are restarted, their initial message will still be
 *    be allowed through and their permissions will be rechecked at that time.
 * 2) If they gain a permission, the broker will notify them if that permission allows them to
 *    communicate with a nanoapp again.
 *
 * Client losing or gaining permissions (PendingIntent client):
 * 1) Unlike callback clients, PendingIntent clients are able to maintain their connection to the
 *    service when they are killed. In their case, they will receive notifications of the broker
 *    that they have been denied required permissions or gain required permissions.
 *
 * TODO: Consider refactoring this class via inheritance
 *
 * @hide
 */
public class ContextHubClientBroker extends IContextHubClient.Stub
        implements IBinder.DeathRecipient, AppOpsManager.OnOpChangedListener {
    private static final String TAG = "ContextHubClientBroker";

    /**
     * Internal only authorization value used when the auth state is unknown.
     */
    private static final int AUTHORIZATION_UNKNOWN = -1;

    /**
     * Message used by noteOp when this client receives a message from a nanoapp.
     */
    private static final String RECEIVE_MSG_NOTE = "NanoappMessageDelivery ";

    /**
     * For clients targeting S and above, a SecurityException is thrown when they are in the denied
     * authorization state and attempt to send a message to a nanoapp.
     */
    @ChangeId
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R)
    private static final long CHANGE_ID_AUTH_STATE_DENIED = 181350407L;

    /*
     * The context of the service.
     */
    private final Context mContext;

    /*
     * The proxy to talk to the Context Hub HAL.
     */
    private final IContextHubWrapper mContextHubProxy;

    /*
     * The manager that registered this client.
     */
    private final ContextHubClientManager mClientManager;

    /*
     * The object describing the hub that this client is attached to.
     */
    private final ContextHubInfo mAttachedContextHubInfo;

    /*
     * The host end point ID of this client.
     */
    private final short mHostEndPointId;

    /*
     * The remote callback interface for this client. This will be set to null whenever the
     * client connection is closed (either explicitly or via binder death).
     */
    private IContextHubClientCallback mCallbackInterface = null;

    /*
     * True if the client is still registered with the Context Hub Service, false otherwise.
     */
    private boolean mRegistered = true;

    /**
     * String containing an attribution tag that was denoted in the {@link Context} of the
     * creator of this broker. This is used when attributing the permissions usage of the broker.
     */
    private @Nullable String mAttributionTag;

    /*
     * Internal interface used to invoke client callbacks.
     */
    private interface CallbackConsumer {
        void accept(IContextHubClientCallback callback) throws RemoteException;
    }

    /*
     * The PendingIntent registered with this client.
     */
    private final PendingIntentRequest mPendingIntentRequest;

    /*
     * The host package associated with this client.
     */
    private final String mPackage;

    /**
     * The PID associated with this client.
     */
    private final int mPid;

    /**
     * The UID associated with this client.
     */
    private final int mUid;

    /**
     * Manager used for noting permissions usage of this broker.
     */
    private final AppOpsManager mAppOpsManager;

    /**
     * Manager used to queue transactions to the context hub.
     */
    private final ContextHubTransactionManager mTransactionManager;

    /*
     * True if a PendingIntent has been cancelled.
     */
    private AtomicBoolean mIsPendingIntentCancelled = new AtomicBoolean(false);

    /*
     * True if the application creating the client has the ACCESS_CONTEXT_HUB permission.
     */
    private final boolean mHasAccessContextHubPermission;

    /*
     * Map containing all nanoapps this client has a messaging channel with and whether it is
     * allowed to communicate over that channel. A channel is defined to have been opened if the
     * client has sent or received messages from the particular nanoapp.
     */
    private final Map mMessageChannelNanoappIdMap = new ConcurrentHashMap<>();

    /**
     * Set containing all nanoapps that have been forcefully transitioned to the denied
     * authorization state (via CLI) to ensure they don't transition back to the granted state
     * later if, for example, a permission check is performed due to another nanoapp
     */
    private final Set mForceDeniedNapps = new HashSet<>();

    /**
     * Map containing all nanoapps that have active auth state denial timers.
     */
    private final Map mNappToAuthTimerMap = new ConcurrentHashMap<>();

    /**
     * Callback used to obtain the latest set of nanoapp permissions and verify this client has
     * each nanoapps permissions granted.
     */
    private final IContextHubTransactionCallback mQueryPermsCallback =
            new IContextHubTransactionCallback.Stub() {
            @Override
            public void onTransactionComplete(int result) {
            }

            @Override
            public void onQueryResponse(int result, List nanoAppStateList) {
                if (result != ContextHubTransaction.RESULT_SUCCESS && nanoAppStateList != null) {
                    Log.e(TAG, "Permissions query failed, but still received nanoapp state");
                } else if (nanoAppStateList != null) {
                    for (NanoAppState state : nanoAppStateList) {
                        if (mMessageChannelNanoappIdMap.containsKey(state.getNanoAppId())) {
                            List permissions = state.getNanoAppPermissions();
                            updateNanoAppAuthState(state.getNanoAppId(),
                                    permissions, false /* gracePeriodExpired */);
                        }
                    }
                }
            }
        };

    /*
     * Helper class to manage registered PendingIntent requests from the client.
     */
    private class PendingIntentRequest {
        /*
         * The PendingIntent object to request, null if there is no open request.
         */
        private PendingIntent mPendingIntent;

        /*
         * The ID of the nanoapp the request is for, invalid if there is no open request.
         */
        private long mNanoAppId;

        private boolean mValid = false;

        PendingIntentRequest() {
        }

        PendingIntentRequest(PendingIntent pendingIntent, long nanoAppId) {
            mPendingIntent = pendingIntent;
            mNanoAppId = nanoAppId;
            mValid = true;
        }

        public long getNanoAppId() {
            return mNanoAppId;
        }

        public PendingIntent getPendingIntent() {
            return mPendingIntent;
        }

        public boolean hasPendingIntent() {
            return mPendingIntent != null;
        }

        public void clear() {
            mPendingIntent = null;
        }

        public boolean isValid() {
            return mValid;
        }
    }

    private ContextHubClientBroker(Context context, IContextHubWrapper contextHubProxy,
            ContextHubClientManager clientManager, ContextHubInfo contextHubInfo,
            short hostEndPointId, IContextHubClientCallback callback, String attributionTag,
            ContextHubTransactionManager transactionManager, PendingIntent pendingIntent,
            long nanoAppId, String packageName) {
        mContext = context;
        mContextHubProxy = contextHubProxy;
        mClientManager = clientManager;
        mAttachedContextHubInfo = contextHubInfo;
        mHostEndPointId = hostEndPointId;
        mCallbackInterface = callback;
        if (pendingIntent == null) {
            mPendingIntentRequest = new PendingIntentRequest();
        } else {
            mPendingIntentRequest = new PendingIntentRequest(pendingIntent, nanoAppId);
        }
        mPackage = packageName;
        mAttributionTag = attributionTag;
        mTransactionManager = transactionManager;

        mPid = Binder.getCallingPid();
        mUid = Binder.getCallingUid();
        mHasAccessContextHubPermission = context.checkCallingPermission(
                Manifest.permission.ACCESS_CONTEXT_HUB) == PERMISSION_GRANTED;
        mAppOpsManager = context.getSystemService(AppOpsManager.class);

        startMonitoringOpChanges();
    }

    /* package */ ContextHubClientBroker(
            Context context, IContextHubWrapper contextHubProxy,
            ContextHubClientManager clientManager, ContextHubInfo contextHubInfo,
            short hostEndPointId, IContextHubClientCallback callback, String attributionTag,
            ContextHubTransactionManager transactionManager, String packageName) {
        this(context, contextHubProxy, clientManager, contextHubInfo, hostEndPointId, callback,
                attributionTag, transactionManager, null /* pendingIntent */, 0 /* nanoAppId */,
                packageName);
    }

    /* package */ ContextHubClientBroker(
            Context context, IContextHubWrapper contextHubProxy,
            ContextHubClientManager clientManager, ContextHubInfo contextHubInfo,
            short hostEndPointId, PendingIntent pendingIntent, long nanoAppId,
            String attributionTag, ContextHubTransactionManager transactionManager) {
        this(context, contextHubProxy, clientManager, contextHubInfo, hostEndPointId,
                null /* callback */, attributionTag, transactionManager, pendingIntent, nanoAppId,
                pendingIntent.getCreatorPackage());
    }

    private void startMonitoringOpChanges() {
        mAppOpsManager.startWatchingMode(AppOpsManager.OP_NONE, mPackage, this);
    }

    /**
     * Sends from this client to a nanoapp.
     *
     * @param message the message to send
     * @return the error code of sending the message
     * @throws SecurityException if this client doesn't have permissions to send a message to the
     * nanoapp
     */
    @ContextHubTransaction.Result
    @Override
    public int sendMessageToNanoApp(NanoAppMessage message) {
        ContextHubServiceUtil.checkPermissions(mContext);

        int result;
        if (isRegistered()) {
            int authState = mMessageChannelNanoappIdMap.getOrDefault(
                    message.getNanoAppId(), AUTHORIZATION_UNKNOWN);
            if (authState == AUTHORIZATION_DENIED) {
                if (Compatibility.isChangeEnabled(CHANGE_ID_AUTH_STATE_DENIED)) {
                    throw new SecurityException("Client doesn't have valid permissions to send"
                            + " message to " + message.getNanoAppId());
                }
                // Return a bland error code for apps targeting old SDKs since they wouldn't be able
                // to use an error code added in S.
                return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
            } else if (authState == AUTHORIZATION_UNKNOWN) {
                // Only check permissions the first time a nanoapp is queried since nanoapp
                // permissions don't currently change at runtime. If the host permission changes
                // later, that'll be checked by onOpChanged.
                checkNanoappPermsAsync();
            }

            ContextHubMsg messageToNanoApp =
                    ContextHubServiceUtil.createHidlContextHubMessage(mHostEndPointId, message);

            int contextHubId = mAttachedContextHubInfo.getId();
            try {
                result = mContextHubProxy.getHub().sendMessageToHub(contextHubId, messageToNanoApp);
            } catch (RemoteException e) {
                Log.e(TAG, "RemoteException in sendMessageToNanoApp (target hub ID = "
                        + contextHubId + ")", e);
                result = Result.UNKNOWN_FAILURE;
            }
        } else {
            Log.e(TAG, "Failed to send message to nanoapp: client connection is closed");
            result = Result.UNKNOWN_FAILURE;
        }

        return ContextHubServiceUtil.toTransactionResult(result);
    }

    /**
     * Closes the connection for this client with the service.
     *
     * If the client has a PendingIntent registered, this method also unregisters it.
     */
    @Override
    public void close() {
        synchronized (this) {
            mPendingIntentRequest.clear();
        }
        onClientExit();
    }

    /**
     * Invoked when the underlying binder of this broker has died at the client process.
     */
    @Override
    public void binderDied() {
        onClientExit();
    }

    @Override
    public void onOpChanged(String op, String packageName) {
        if (packageName.equals(mPackage)) {
            if (!mMessageChannelNanoappIdMap.isEmpty()) {
                checkNanoappPermsAsync();
            }
        }
    }

    /* package */ String getPackageName() {
        return mPackage;
    }

    /**
     * Used to override the attribution tag with a newer value if a PendingIntent broker is
     * retrieved.
     */
    /* package */ void setAttributionTag(String attributionTag) {
        mAttributionTag = attributionTag;
    }

    /**
     * @return the attribution tag associated with this broker.
     */
    /* package */ String getAttributionTag() {
        return mAttributionTag;
    }

    /**
     * @return the ID of the context hub this client is attached to
     */
    /* package */ int getAttachedContextHubId() {
        return mAttachedContextHubInfo.getId();
    }

    /**
     * @return the host endpoint ID of this client
     */
    /* package */ short getHostEndPointId() {
        return mHostEndPointId;
    }

    /**
     * Sends a message to the client associated with this object.
     *
     * @param message the message that came from a nanoapp
     * @param nanoappPermissions permissions required to communicate with the nanoapp sending this
     * message
     * @param messagePermissions permissions required to consume the message being delivered. These
     * permissions are what will be attributed to the client through noteOp.
     */
    /* package */ void sendMessageToClient(
            NanoAppMessage message, List nanoappPermissions,
            List messagePermissions) {
        long nanoAppId = message.getNanoAppId();

        int authState = updateNanoAppAuthState(nanoAppId, nanoappPermissions,
                false /* gracePeriodExpired */);

        // If in the grace period, the host may not receive any messages containing permissions
        // covered data.
        if (authState == AUTHORIZATION_DENIED_GRACE_PERIOD && !messagePermissions.isEmpty()) {
            Log.e(TAG, "Dropping message from " + Long.toHexString(nanoAppId) + ". " + mPackage
                    + " in grace period and napp msg has permissions");
            return;
        }

        // If in the grace period, don't check permissions state since it'll cause cleanup
        // messages to be dropped.
        if (authState == AUTHORIZATION_DENIED
                || !notePermissions(messagePermissions, RECEIVE_MSG_NOTE + nanoAppId)) {
            Log.e(TAG, "Dropping message from " + Long.toHexString(nanoAppId) + ". " + mPackage
                    + " doesn't have permission");
            return;
        }

        invokeCallback(callback -> callback.onMessageFromNanoApp(message));

        Supplier supplier =
                () -> createIntent(ContextHubManager.EVENT_NANOAPP_MESSAGE, nanoAppId)
                        .putExtra(ContextHubManager.EXTRA_MESSAGE, message);
        sendPendingIntent(supplier, nanoAppId);
    }

    /**
     * Notifies the client of a nanoapp load event if the connection is open.
     *
     * @param nanoAppId the ID of the nanoapp that was loaded.
     */
    /* package */ void onNanoAppLoaded(long nanoAppId) {
        // Check the latest state to see if the loaded nanoapp's permissions changed such that the
        // host app can communicate with it again.
        checkNanoappPermsAsync();

        invokeCallback(callback -> callback.onNanoAppLoaded(nanoAppId));
        sendPendingIntent(
                () -> createIntent(ContextHubManager.EVENT_NANOAPP_LOADED, nanoAppId), nanoAppId);
    }

    /**
     * Notifies the client of a nanoapp unload event if the connection is open.
     *
     * @param nanoAppId the ID of the nanoapp that was unloaded.
     */
    /* package */ void onNanoAppUnloaded(long nanoAppId) {
        invokeCallback(callback -> callback.onNanoAppUnloaded(nanoAppId));
        sendPendingIntent(
                () -> createIntent(ContextHubManager.EVENT_NANOAPP_UNLOADED, nanoAppId), nanoAppId);
    }

    /**
     * Notifies the client of a hub reset event if the connection is open.
     */
    /* package */ void onHubReset() {
        invokeCallback(callback -> callback.onHubReset());
        sendPendingIntent(() -> createIntent(ContextHubManager.EVENT_HUB_RESET));
    }

    /**
     * Notifies the client of a nanoapp abort event if the connection is open.
     *
     * @param nanoAppId the ID of the nanoapp that aborted
     * @param abortCode the nanoapp specific abort code
     */
    /* package */ void onNanoAppAborted(long nanoAppId, int abortCode) {
        invokeCallback(callback -> callback.onNanoAppAborted(nanoAppId, abortCode));

        Supplier supplier =
                () -> createIntent(ContextHubManager.EVENT_NANOAPP_ABORTED, nanoAppId)
                        .putExtra(ContextHubManager.EXTRA_NANOAPP_ABORT_CODE, abortCode);
        sendPendingIntent(supplier, nanoAppId);
    }

    /**
     * @param intent    the PendingIntent to compare to
     * @param nanoAppId the ID of the nanoapp of the PendingIntent to compare to
     * @return true if the given PendingIntent is currently registered, false otherwise
     */
    /* package */ boolean hasPendingIntent(PendingIntent intent, long nanoAppId) {
        PendingIntent pendingIntent = null;
        long intentNanoAppId;
        synchronized (this) {
            pendingIntent = mPendingIntentRequest.getPendingIntent();
            intentNanoAppId = mPendingIntentRequest.getNanoAppId();
        }
        return (pendingIntent != null) && pendingIntent.equals(intent)
                && intentNanoAppId == nanoAppId;
    }

    /**
     * Attaches the death recipient to the callback interface object, if any.
     *
     * @throws RemoteException if the client process already died
     */
    /* package */ void attachDeathRecipient() throws RemoteException {
        if (mCallbackInterface != null) {
            mCallbackInterface.asBinder().linkToDeath(this, 0 /* flags */);
        }
    }

    /**
     * Checks that this client has all of the provided permissions.
     *
     * @param permissions list of permissions to check
     * @return true if the client has all of the permissions granted
     */
    /* package */ boolean hasPermissions(List permissions) {
        for (String permission : permissions) {
            if (mContext.checkPermission(permission, mPid, mUid) != PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    /**
     * Attributes the provided permissions to the package of this client.
     *
     * @param permissions list of permissions covering data the client is about to receive
     * @param noteMessage message that should be noted alongside permissions attribution to
     * facilitate debugging
     * @return true if client has ability to use all of the provided permissions
     */
    /* package */ boolean notePermissions(List permissions, String noteMessage) {
        for (String permission : permissions) {
            int opCode = mAppOpsManager.permissionToOpCode(permission);
            if (opCode != AppOpsManager.OP_NONE) {
                if (mAppOpsManager.noteOp(opCode, mUid, mPackage, mAttributionTag, noteMessage)
                        != AppOpsManager.MODE_ALLOWED) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * @return true if the client is a PendingIntent client that has been cancelled.
     */
    /* package */ boolean isPendingIntentCancelled() {
        return mIsPendingIntentCancelled.get();
    }

    /**
     * Handles timer expiry for a client whose auth state with a nanoapp was previously in the grace
     * period.
     */
    /* package */ void handleAuthStateTimerExpiry(long nanoAppId) {
        AuthStateDenialTimer timer;
        synchronized (mMessageChannelNanoappIdMap) {
            timer = mNappToAuthTimerMap.remove(nanoAppId);
        }

        if (timer != null) {
            updateNanoAppAuthState(
                    nanoAppId, Collections.emptyList() /* nanoappPermissions */,
                    true /* gracePeriodExpired */);
        }
    }

    /**
     * Verifies this client has the permissions to communicate with all of the nanoapps it has
     * communicated with in the past.
     */
    private void checkNanoappPermsAsync() {
        ContextHubServiceTransaction transaction = mTransactionManager.createQueryTransaction(
                mAttachedContextHubInfo.getId(), mQueryPermsCallback, mPackage);
        mTransactionManager.addTransaction(transaction);
    }

    private int updateNanoAppAuthState(
            long nanoAppId, List nanoappPermissions, boolean gracePeriodExpired) {
        return updateNanoAppAuthState(
                nanoAppId, nanoappPermissions, gracePeriodExpired,
                false /* forceDenied */);
    }

    /**
     * Updates the latest authenticatication state for the given nanoapp.
     *
     * @param nanoAppId the nanoapp that's auth state is being updated
     * @param nanoappPermissions the Android permissions required to communicate with the nanoapp
     * @param gracePeriodExpired indicates whether this invocation is a result of the grace period
     *         expiring
     * @param forceDenied indicates that no matter what auth state is asssociated with this nanoapp
     *         it should transition to denied
     * @return the latest auth state as of the completion of this method.
     */
    /* package */ int updateNanoAppAuthState(
            long nanoAppId, List nanoappPermissions, boolean gracePeriodExpired,
            boolean forceDenied) {
        int curAuthState;
        int newAuthState;
        synchronized (mMessageChannelNanoappIdMap) {
            // Check permission granted state synchronously since this method can be invoked from
            // multiple threads.
            boolean hasPermissions = hasPermissions(nanoappPermissions);

            curAuthState = mMessageChannelNanoappIdMap.getOrDefault(
                    nanoAppId, AUTHORIZATION_UNKNOWN);
            if (curAuthState == AUTHORIZATION_UNKNOWN) {
                // If there's never been an auth check performed, start the state as granted so the
                // appropriate state transitions occur below and clients don't receive a granted
                // callback if they're determined to be in the granted state initially.
                curAuthState = AUTHORIZATION_GRANTED;
                mMessageChannelNanoappIdMap.put(nanoAppId, AUTHORIZATION_GRANTED);
            }

            newAuthState = curAuthState;
            // The below logic ensures that only the following transitions are possible:
            // GRANTED -> DENIED_GRACE_PERIOD only if permissions have been lost
            // DENIED_GRACE_PERIOD -> DENIED only if the grace period expires
            // DENIED/DENIED_GRACE_PERIOD -> GRANTED only if permissions are granted again
            // any state -> DENIED if "forceDenied" is true
            if (forceDenied || mForceDeniedNapps.contains(nanoAppId)) {
                newAuthState = AUTHORIZATION_DENIED;
                mForceDeniedNapps.add(nanoAppId);
            } else if (gracePeriodExpired) {
                if (curAuthState == AUTHORIZATION_DENIED_GRACE_PERIOD) {
                    newAuthState = AUTHORIZATION_DENIED;
                }
            } else {
                if (curAuthState == AUTHORIZATION_GRANTED && !hasPermissions) {
                    newAuthState = AUTHORIZATION_DENIED_GRACE_PERIOD;
                } else if (curAuthState != AUTHORIZATION_GRANTED && hasPermissions) {
                    newAuthState = AUTHORIZATION_GRANTED;
                }
            }

            if (newAuthState != AUTHORIZATION_DENIED_GRACE_PERIOD) {
                AuthStateDenialTimer timer = mNappToAuthTimerMap.remove(nanoAppId);
                if (timer != null) {
                    timer.cancel();
                }
            } else if (curAuthState == AUTHORIZATION_GRANTED) {
                AuthStateDenialTimer timer =
                        new AuthStateDenialTimer(this, nanoAppId, Looper.getMainLooper());
                mNappToAuthTimerMap.put(nanoAppId, timer);
                timer.start();
            }

            if (curAuthState != newAuthState) {
                mMessageChannelNanoappIdMap.put(nanoAppId, newAuthState);
            }
        }
        if (curAuthState != newAuthState) {
            // Don't send the callback in the synchronized block or it could end up in a deadlock.
            sendAuthStateCallback(nanoAppId, newAuthState);
        }
        return newAuthState;
    }

    private void sendAuthStateCallback(long nanoAppId, int authState) {
        invokeCallback(callback -> callback.onClientAuthorizationChanged(nanoAppId, authState));

        Supplier supplier =
                () -> createIntent(ContextHubManager.EVENT_CLIENT_AUTHORIZATION, nanoAppId)
                        .putExtra(ContextHubManager.EXTRA_CLIENT_AUTHORIZATION_STATE, authState);
        sendPendingIntent(supplier, nanoAppId);
    }

    /**
     * Helper function to invoke a specified client callback, if the connection is open.
     *
     * @param consumer the consumer specifying the callback to invoke
     */
    private synchronized void invokeCallback(CallbackConsumer consumer) {
        if (mCallbackInterface != null) {
            try {
                consumer.accept(mCallbackInterface);
            } catch (RemoteException e) {
                Log.e(TAG, "RemoteException while invoking client callback (host endpoint ID = "
                        + mHostEndPointId + ")", e);
            }
        }
    }

    /**
     * Creates an Intent object containing the ContextHubManager.EXTRA_EVENT_TYPE extra field
     *
     * @param eventType the ContextHubManager.Event type describing the event
     * @return the Intent object
     */
    private Intent createIntent(int eventType) {
        Intent intent = new Intent();
        intent.putExtra(ContextHubManager.EXTRA_EVENT_TYPE, eventType);
        intent.putExtra(ContextHubManager.EXTRA_CONTEXT_HUB_INFO, mAttachedContextHubInfo);
        return intent;
    }

    /**
     * Creates an Intent object containing the ContextHubManager.EXTRA_EVENT_TYPE and the
     * ContextHubManager.EXTRA_NANOAPP_ID extra fields
     *
     * @param eventType the ContextHubManager.Event type describing the event
     * @param nanoAppId the ID of the nanoapp this event is for
     * @return the Intent object
     */
    private Intent createIntent(int eventType, long nanoAppId) {
        Intent intent = createIntent(eventType);
        intent.putExtra(ContextHubManager.EXTRA_NANOAPP_ID, nanoAppId);
        return intent;
    }

    /**
     * Sends an intent to any existing PendingIntent
     *
     * @param supplier method to create the extra Intent
     */
    private synchronized void sendPendingIntent(Supplier supplier) {
        if (mPendingIntentRequest.hasPendingIntent()) {
            doSendPendingIntent(mPendingIntentRequest.getPendingIntent(), supplier.get());
        }
    }

    /**
     * Sends an intent to any existing PendingIntent
     *
     * @param supplier  method to create the extra Intent
     * @param nanoAppId the ID of the nanoapp which this event is for
     */
    private synchronized void sendPendingIntent(Supplier supplier, long nanoAppId) {
        if (mPendingIntentRequest.hasPendingIntent()
                && mPendingIntentRequest.getNanoAppId() == nanoAppId) {
            doSendPendingIntent(mPendingIntentRequest.getPendingIntent(), supplier.get());
        }
    }

    /**
     * Sends a PendingIntent with extra Intent data
     *
     * @param pendingIntent the PendingIntent
     * @param intent        the extra Intent data
     */
    private void doSendPendingIntent(PendingIntent pendingIntent, Intent intent) {
        try {
            String requiredPermission = mHasAccessContextHubPermission
                    ? Manifest.permission.ACCESS_CONTEXT_HUB
                    : Manifest.permission.LOCATION_HARDWARE;
            pendingIntent.send(
                    mContext, 0 /* code */, intent, null /* onFinished */, null /* Handler */,
                    requiredPermission, null /* options */);
        } catch (PendingIntent.CanceledException e) {
            mIsPendingIntentCancelled.set(true);
            // The PendingIntent is no longer valid
            Log.w(TAG, "PendingIntent has been canceled, unregistering from client"
                    + " (host endpoint ID " + mHostEndPointId + ")");
            close();
        }
    }

    /**
     * @return true if the client is still registered with the service, false otherwise
     */
    private synchronized boolean isRegistered() {
        return mRegistered;
    }

    /**
     * Invoked when a client exits either explicitly or by binder death.
     */
    private synchronized void onClientExit() {
        if (mCallbackInterface != null) {
            mCallbackInterface.asBinder().unlinkToDeath(this, 0 /* flags */);
            mCallbackInterface = null;
        }
        if (!mPendingIntentRequest.hasPendingIntent() && mRegistered) {
            mClientManager.unregisterClient(mHostEndPointId);
            mRegistered = false;
        }
        mAppOpsManager.stopWatchingMode(this);
    }

    private String authStateToString(@ContextHubManager.AuthorizationState int state) {
        switch (state) {
            case AUTHORIZATION_DENIED:
                return "DENIED";
            case AUTHORIZATION_DENIED_GRACE_PERIOD:
                return "DENIED_GRACE_PERIOD";
            case AUTHORIZATION_GRANTED:
                return "GRANTED";
            default:
                return "UNKNOWN";
        }
    }

    /**
     * Dump debugging info as ClientBrokerProto
     *
     * If the output belongs to a sub message, the caller is responsible for wrapping this function
     * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}.
     *
     * @param proto the ProtoOutputStream to write to
     */
    void dump(ProtoOutputStream proto) {
        proto.write(ClientBrokerProto.ENDPOINT_ID, getHostEndPointId());
        proto.write(ClientBrokerProto.ATTACHED_CONTEXT_HUB_ID, getAttachedContextHubId());
        proto.write(ClientBrokerProto.PACKAGE, mPackage);
        if (mPendingIntentRequest.isValid()) {
            proto.write(ClientBrokerProto.PENDING_INTENT_REQUEST_VALID, true);
            proto.write(ClientBrokerProto.NANO_APP_ID, mPendingIntentRequest.getNanoAppId());
        }
        proto.write(ClientBrokerProto.HAS_PENDING_INTENT, mPendingIntentRequest.hasPendingIntent());
        proto.write(ClientBrokerProto.PENDING_INTENT_CANCELLED, isPendingIntentCancelled());
        proto.write(ClientBrokerProto.REGISTERED, mRegistered);

    }

    @Override
    public String toString() {
        String out = "[ContextHubClient ";
        out += "endpointID: " + getHostEndPointId() + ", ";
        out += "contextHub: " + getAttachedContextHubId() + ", ";
        if (mAttributionTag != null) {
            out += "attributionTag: " + getAttributionTag() + ", ";
        }
        if (mPendingIntentRequest.isValid()) {
            out += "intentCreatorPackage: " + mPackage + ", ";
            out += "nanoAppId: 0x" + Long.toHexString(mPendingIntentRequest.getNanoAppId());
        } else {
            out += "package: " + mPackage;
        }
        if (mMessageChannelNanoappIdMap.size() > 0) {
            out += " messageChannelNanoappSet: (";
            Iterator> it =
                    mMessageChannelNanoappIdMap.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry entry = it.next();
                out += "0x" + Long.toHexString(entry.getKey()) + " auth state: "
                        + authStateToString(entry.getValue());
                if (it.hasNext()) {
                    out += ",";
                }
            }
            out += ")";
        }
        out += "]";

        return out;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy