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

src.com.android.server.people.PeopleService 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) 2019 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.people;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.people.ConversationChannel;
import android.app.people.ConversationStatus;
import android.app.people.IConversationListener;
import android.app.people.IPeopleManager;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionSessionId;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.IPredictionCallback;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
import android.content.pm.ShortcutInfo;
import android.os.Binder;
import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.people.data.DataManager;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/**
 * A service that manages the people and conversations provided by apps.
 */
public class PeopleService extends SystemService {

    private static final String TAG = "PeopleService";

    private DataManager mDataManager;
    @VisibleForTesting
    ConversationListenerHelper mConversationListenerHelper;

    private PackageManagerInternal mPackageManagerInternal;

    /**
     * Initializes the system service.
     *
     * @param context The system server context.
     */
    public PeopleService(Context context) {
        super(context);

        mDataManager = new DataManager(context);
        mConversationListenerHelper = new ConversationListenerHelper();
        mDataManager.addConversationsListener(mConversationListenerHelper);
    }

    @Override
    public void onBootPhase(int phase) {
        if (phase == PHASE_SYSTEM_SERVICES_READY) {
            mDataManager.initialize();
        }
    }

    @Override
    public void onStart() {
        onStart(/* isForTesting= */ false);
    }

    @VisibleForTesting
    protected void onStart(boolean isForTesting) {
        if (!isForTesting) {
            publishBinderService(Context.PEOPLE_SERVICE, mService);
        }
        publishLocalService(PeopleServiceInternal.class, new LocalService());
        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
    }

    @Override
    public void onUserUnlocked(@NonNull TargetUser user) {
        mDataManager.onUserUnlocked(user.getUserIdentifier());
    }

    @Override
    public void onUserStopping(@NonNull TargetUser user) {
        mDataManager.onUserStopping(user.getUserIdentifier());
    }

    /**
     * Enforces that only the system or root UID can make certain calls.
     *
     * @param message used as message if SecurityException is thrown
     * @throws SecurityException if the caller is not system or root
     */
    private static void enforceSystemOrRoot(String message) {
        if (!isSystemOrRoot()) {
            throw new SecurityException("Only system may " + message);
        }
    }

    private static boolean isSystemOrRoot() {
        final int uid = Binder.getCallingUid();
        return UserHandle.isSameApp(uid, Process.SYSTEM_UID) || uid == Process.ROOT_UID;
    }

    private int handleIncomingUser(int userId) {
        try {
            return ActivityManager.getService().handleIncomingUser(
                    Binder.getCallingPid(), Binder.getCallingUid(), userId, true, true, "", null);
        } catch (RemoteException re) {
            // Shouldn't happen, local.
        }
        return userId;
    }

    private void checkCallerIsSameApp(String pkg) {
        final int callingUid = Binder.getCallingUid();
        final int callingUserId = UserHandle.getUserId(callingUid);

        if (mPackageManagerInternal.getPackageUid(pkg, /*flags=*/ 0,
                callingUserId) != callingUid) {
            throw new SecurityException("Calling uid " + callingUid + " cannot query events"
                    + "for package " + pkg);
        }
    }

    /**
     * Enforces that only the system, root UID or SystemUI can make certain calls.
     *
     * @param message used as message if SecurityException is thrown
     * @throws SecurityException if the caller is not system or root
     */
    @VisibleForTesting
    protected void enforceSystemRootOrSystemUI(Context context, String message) {
        if (isSystemOrRoot()) return;
        context.enforceCallingPermission(android.Manifest.permission.STATUS_BAR_SERVICE,
                message);
    }

    @VisibleForTesting
    final IBinder mService = new IPeopleManager.Stub() {

        @Override
        public ConversationChannel getConversation(
                String packageName, int userId, String shortcutId) {
            enforceSystemRootOrSystemUI(getContext(), "get conversation");
            return mDataManager.getConversation(packageName, userId, shortcutId);
        }

        @Override
        public ParceledListSlice getRecentConversations() {
            enforceSystemRootOrSystemUI(getContext(), "get recent conversations");
            return new ParceledListSlice<>(
                    mDataManager.getRecentConversations(
                            Binder.getCallingUserHandle().getIdentifier()));
        }

        @Override
        public void removeRecentConversation(String packageName, int userId, String shortcutId) {
            enforceSystemOrRoot("remove a recent conversation");
            mDataManager.removeRecentConversation(packageName, userId, shortcutId,
                    Binder.getCallingUserHandle().getIdentifier());
        }

        @Override
        public void removeAllRecentConversations() {
            enforceSystemOrRoot("remove all recent conversations");
            mDataManager.removeAllRecentConversations(
                    Binder.getCallingUserHandle().getIdentifier());
        }

        @Override
        public boolean isConversation(String packageName, int userId, String shortcutId) {
            enforceHasReadPeopleDataPermission();
            handleIncomingUser(userId);
            return mDataManager.isConversation(packageName, userId, shortcutId);
        }

        private void enforceHasReadPeopleDataPermission() throws SecurityException {
            if (getContext().checkCallingPermission(Manifest.permission.READ_PEOPLE_DATA)
                    != PackageManager.PERMISSION_GRANTED) {
                throw new SecurityException("Caller doesn't have READ_PEOPLE_DATA permission.");
            }
        }

        @Override
        public long getLastInteraction(String packageName, int userId, String shortcutId) {
            enforceSystemRootOrSystemUI(getContext(), "get last interaction");
            return mDataManager.getLastInteraction(packageName, userId, shortcutId);
        }

        @Override
        public void addOrUpdateStatus(String packageName, int userId, String conversationId,
                ConversationStatus status) {
            handleIncomingUser(userId);
            checkCallerIsSameApp(packageName);
            if (status.getStartTimeMillis() > System.currentTimeMillis()) {
                throw new IllegalArgumentException("Start time must be in the past");
            }
            mDataManager.addOrUpdateStatus(packageName, userId, conversationId, status);
        }

        @Override
        public void clearStatus(String packageName, int userId, String conversationId,
                String statusId) {
            handleIncomingUser(userId);
            checkCallerIsSameApp(packageName);
            mDataManager.clearStatus(packageName, userId, conversationId, statusId);
        }

        @Override
        public void clearStatuses(String packageName, int userId, String conversationId) {
            handleIncomingUser(userId);
            checkCallerIsSameApp(packageName);
            mDataManager.clearStatuses(packageName, userId, conversationId);
        }

        @Override
        public ParceledListSlice getStatuses(String packageName, int userId,
                String conversationId) {
            handleIncomingUser(userId);
            if (!isSystemOrRoot()) {
                checkCallerIsSameApp(packageName);
            }
            return new ParceledListSlice<>(
                    mDataManager.getStatuses(packageName, userId, conversationId));
        }

        @Override
        public void registerConversationListener(
                String packageName, int userId, String shortcutId, IConversationListener listener) {
            enforceSystemRootOrSystemUI(getContext(), "register conversation listener");
            mConversationListenerHelper.addConversationListener(
                    new ListenerKey(packageName, userId, shortcutId), listener);
        }

        @Override
        public void unregisterConversationListener(IConversationListener listener) {
            enforceSystemRootOrSystemUI(getContext(), "unregister conversation listener");
            mConversationListenerHelper.removeConversationListener(listener);
        }
    };

    /**
     * Listeners for conversation changes.
     *
     * @hide
     */
    public interface ConversationsListener {
        /**
         * Triggers with the list of modified conversations from {@link DataManager} for dispatching
         * relevant updates to clients.
         *
         * @param conversations The conversations with modified data
         * @see IPeopleManager#registerConversationListener(String, int, String,
         * android.app.people.ConversationListener)
         */
        default void onConversationsUpdate(@NonNull List conversations) {
        }
    }

    /**
     * Implements {@code ConversationListenerHelper} to dispatch conversation updates to registered
     * clients.
     */
    public static class ConversationListenerHelper implements ConversationsListener {

        ConversationListenerHelper() {
        }

        @VisibleForTesting
        final RemoteCallbackList mListeners =
                new RemoteCallbackList<>();

        /** Adds {@code listener} with {@code key} associated. */
        public synchronized void addConversationListener(ListenerKey key,
                IConversationListener listener) {
            mListeners.unregister(listener);
            mListeners.register(listener, key);
        }

        /** Removes {@code listener}. */
        public synchronized void removeConversationListener(
                IConversationListener listener) {
            mListeners.unregister(listener);
        }

        @Override
        /** Dispatches updates to {@code mListeners} with keys mapped to {@code conversations}. */
        public void onConversationsUpdate(List conversations) {
            int count = mListeners.beginBroadcast();
            // Early opt-out if no listeners are registered.
            if (count == 0) {
                return;
            }
            Map keyedConversations = new HashMap<>();
            for (ConversationChannel conversation : conversations) {
                keyedConversations.put(getListenerKey(conversation), conversation);
            }
            for (int i = 0; i < count; i++) {
                final ListenerKey listenerKey = (ListenerKey) mListeners.getBroadcastCookie(i);
                if (!keyedConversations.containsKey(listenerKey)) {
                    continue;
                }
                final IConversationListener listener = mListeners.getBroadcastItem(i);
                try {
                    ConversationChannel channel = keyedConversations.get(listenerKey);
                    listener.onConversationUpdate(channel);
                } catch (RemoteException e) {
                    // The RemoteCallbackList will take care of removing the dead object.
                }
            }
            mListeners.finishBroadcast();
        }

        private ListenerKey getListenerKey(ConversationChannel conversation) {
            ShortcutInfo info = conversation.getShortcutInfo();
            return new ListenerKey(info.getPackage(), info.getUserId(),
                    info.getId());
        }
    }

    private static class ListenerKey {
        private final String mPackageName;
        private final Integer mUserId;
        private final String mShortcutId;

        ListenerKey(String packageName, Integer userId, String shortcutId) {
            this.mPackageName = packageName;
            this.mUserId = userId;
            this.mShortcutId = shortcutId;
        }

        public String getPackageName() {
            return mPackageName;
        }

        public Integer getUserId() {
            return mUserId;
        }

        public String getShortcutId() {
            return mShortcutId;
        }

        @Override
        public boolean equals(Object o) {
            ListenerKey key = (ListenerKey) o;
            return key.getPackageName().equals(mPackageName) && key.getUserId() == mUserId
                    && key.getShortcutId().equals(mShortcutId);
        }

        @Override
        public int hashCode() {
            return mPackageName.hashCode() + mUserId.hashCode() + mShortcutId.hashCode();
        }
    }

    @VisibleForTesting
    final class LocalService extends PeopleServiceInternal {

        private Map mSessions = new ArrayMap<>();

        @Override
        public void onCreatePredictionSession(AppPredictionContext appPredictionContext,
                AppPredictionSessionId sessionId) {
            mSessions.put(sessionId,
                    new SessionInfo(appPredictionContext, mDataManager, sessionId.getUserId(),
                            getContext()));
        }

        @Override
        public void notifyAppTargetEvent(AppPredictionSessionId sessionId, AppTargetEvent event) {
            runForSession(sessionId,
                    sessionInfo -> sessionInfo.getPredictor().onAppTargetEvent(event));
        }

        @Override
        public void notifyLaunchLocationShown(AppPredictionSessionId sessionId,
                String launchLocation, ParceledListSlice targetIds) {
            runForSession(sessionId,
                    sessionInfo -> sessionInfo.getPredictor().onLaunchLocationShown(
                            launchLocation, targetIds.getList()));
        }

        @Override
        public void sortAppTargets(AppPredictionSessionId sessionId, ParceledListSlice targets,
                IPredictionCallback callback) {
            runForSession(sessionId,
                    sessionInfo -> sessionInfo.getPredictor().onSortAppTargets(
                            targets.getList(),
                            targetList -> invokePredictionCallback(callback, targetList)));
        }

        @Override
        public void registerPredictionUpdates(AppPredictionSessionId sessionId,
                IPredictionCallback callback) {
            runForSession(sessionId, sessionInfo -> sessionInfo.addCallback(callback));
        }

        @Override
        public void unregisterPredictionUpdates(AppPredictionSessionId sessionId,
                IPredictionCallback callback) {
            runForSession(sessionId, sessionInfo -> sessionInfo.removeCallback(callback));
        }

        @Override
        public void requestPredictionUpdate(AppPredictionSessionId sessionId) {
            runForSession(sessionId,
                    sessionInfo -> sessionInfo.getPredictor().onRequestPredictionUpdate());
        }

        @Override
        public void onDestroyPredictionSession(AppPredictionSessionId sessionId) {
            runForSession(sessionId, sessionInfo -> {
                sessionInfo.onDestroy();
                mSessions.remove(sessionId);
            });
        }

        @Override
        public void pruneDataForUser(@UserIdInt int userId, @NonNull CancellationSignal signal) {
            mDataManager.pruneDataForUser(userId, signal);
        }

        @Nullable
        @Override
        public byte[] getBackupPayload(@UserIdInt int userId) {
            return mDataManager.getBackupPayload(userId);
        }

        @Override
        public void restore(@UserIdInt int userId, @NonNull byte[] payload) {
            mDataManager.restore(userId, payload);
        }

        @VisibleForTesting
        SessionInfo getSessionInfo(AppPredictionSessionId sessionId) {
            return mSessions.get(sessionId);
        }

        private void runForSession(AppPredictionSessionId sessionId, Consumer method) {
            SessionInfo sessionInfo = mSessions.get(sessionId);
            if (sessionInfo == null) {
                Slog.e(TAG, "Failed to find the session: " + sessionId);
                return;
            }
            method.accept(sessionInfo);
        }

        private void invokePredictionCallback(IPredictionCallback callback,
                List targets) {
            try {
                callback.onResult(new ParceledListSlice<>(targets));
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to calling callback" + e);
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy