/* * 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.server.smartspace; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppGlobals; import android.app.smartspace.ISmartspaceCallback; import android.app.smartspace.SmartspaceConfig; import android.app.smartspace.SmartspaceSessionId; import android.app.smartspace.SmartspaceTargetEvent; import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ServiceInfo; import android.os.IBinder; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.service.smartspace.ISmartspaceService; import android.service.smartspace.SmartspaceService; import android.util.ArrayMap; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.infra.AbstractRemoteService; import com.android.server.infra.AbstractPerUserSystemService; /** * Per-user instance of {@link SmartspaceManagerService}. */ public class SmartspacePerUserService extends AbstractPerUserSystemService implements RemoteSmartspaceService.RemoteSmartspaceServiceCallbacks { private static final String TAG = SmartspacePerUserService.class.getSimpleName(); @GuardedBy("mLock") private final ArrayMap mSessionInfos = new ArrayMap<>(); @Nullable @GuardedBy("mLock") private RemoteSmartspaceService mRemoteService; /** * When {@code true}, remote service died but service state is kept so it's restored after * the system re-binds to it. */ @GuardedBy("mLock") private boolean mZombie; protected SmartspacePerUserService(SmartspaceManagerService master, Object lock, int userId) { super(master, lock, userId); } @Override // from PerUserSystemService protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) throws NameNotFoundException { ServiceInfo si; try { si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, PackageManager.GET_META_DATA, mUserId); } catch (RemoteException e) { throw new NameNotFoundException("Could not get service for " + serviceComponent); } // TODO(b/177858728): must check that either the service is from a system component, // or it matches a service set by shell cmd (so it can be used on CTS tests and when // OEMs are implementing the real service and also verify the proper permissions return si; } @GuardedBy("mLock") @Override // from PerUserSystemService protected boolean updateLocked(boolean disabled) { final boolean enabledChanged = super.updateLocked(disabled); if (enabledChanged) { if (isEnabledLocked()) { // Send the pending sessions over to the service resurrectSessionsLocked(); } else { // Clear the remote service for the next call updateRemoteServiceLocked(); } } return enabledChanged; } /** * Notifies the service of a new smartspace session. */ @GuardedBy("mLock") public void onCreateSmartspaceSessionLocked(@NonNull SmartspaceConfig smartspaceConfig, @NonNull SmartspaceSessionId sessionId, @NonNull IBinder token) { final boolean serviceExists = resolveService(sessionId, s -> s.onCreateSmartspaceSession(smartspaceConfig, sessionId)); if (serviceExists && !mSessionInfos.containsKey(sessionId)) { final SmartspaceSessionInfo sessionInfo = new SmartspaceSessionInfo( sessionId, smartspaceConfig, token, () -> { synchronized (mLock) { onDestroyLocked(sessionId); } }); if (sessionInfo.linkToDeath()) { mSessionInfos.put(sessionId, sessionInfo); } else { // destroy the session if calling process is already dead onDestroyLocked(sessionId); } } } /** * Records an smartspace event to the service. */ @GuardedBy("mLock") public void notifySmartspaceEventLocked(@NonNull SmartspaceSessionId sessionId, @NonNull SmartspaceTargetEvent event) { final SmartspaceSessionInfo sessionInfo = mSessionInfos.get(sessionId); if (sessionInfo == null) return; resolveService(sessionId, s -> s.notifySmartspaceEvent(sessionId, event)); } /** * Requests the service to return smartspace results of an input query. */ @GuardedBy("mLock") public void requestSmartspaceUpdateLocked(@NonNull SmartspaceSessionId sessionId) { final SmartspaceSessionInfo sessionInfo = mSessionInfos.get(sessionId); if (sessionInfo == null) return; resolveService(sessionId, s -> s.requestSmartspaceUpdate(sessionId)); } /** * Registers a callback for continuous updates of predicted apps or shortcuts. */ @GuardedBy("mLock") public void registerSmartspaceUpdatesLocked(@NonNull SmartspaceSessionId sessionId, @NonNull ISmartspaceCallback callback) { final SmartspaceSessionInfo sessionInfo = mSessionInfos.get(sessionId); if (sessionInfo == null) return; final boolean serviceExists = resolveService(sessionId, s -> s.registerSmartspaceUpdates(sessionId, callback)); if (serviceExists) { sessionInfo.addCallbackLocked(callback); } } /** * Unregisters a callback for continuous updates of predicted apps or shortcuts. */ @GuardedBy("mLock") public void unregisterSmartspaceUpdatesLocked(@NonNull SmartspaceSessionId sessionId, @NonNull ISmartspaceCallback callback) { final SmartspaceSessionInfo sessionInfo = mSessionInfos.get(sessionId); if (sessionInfo == null) return; final boolean serviceExists = resolveService(sessionId, s -> s.unregisterSmartspaceUpdates(sessionId, callback)); if (serviceExists) { sessionInfo.removeCallbackLocked(callback); } } /** * Notifies the service of the end of an existing smartspace session. */ @GuardedBy("mLock") public void onDestroyLocked(@NonNull SmartspaceSessionId sessionId) { if (isDebug()) { Slog.d(TAG, "onDestroyLocked(): sessionId=" + sessionId); } final SmartspaceSessionInfo sessionInfo = mSessionInfos.remove(sessionId); if (sessionInfo == null) return; resolveService(sessionId, s -> s.onDestroySmartspaceSession(sessionId)); sessionInfo.destroy(); } @Override public void onFailureOrTimeout(boolean timedOut) { if (isDebug()) { Slog.d(TAG, "onFailureOrTimeout(): timed out=" + timedOut); } // Do nothing, we are just proxying to the smartspace ui service } @Override public void onConnectedStateChanged(boolean connected) { if (isDebug()) { Slog.d(TAG, "onConnectedStateChanged(): connected=" + connected); } if (connected) { synchronized (mLock) { if (mZombie) { // Validation check - shouldn't happen if (mRemoteService == null) { Slog.w(TAG, "Cannot resurrect sessions because remote service is null"); return; } mZombie = false; resurrectSessionsLocked(); } } } } @Override public void onServiceDied(RemoteSmartspaceService service) { if (isDebug()) { Slog.w(TAG, "onServiceDied(): service=" + service); } synchronized (mLock) { mZombie = true; } updateRemoteServiceLocked(); } @GuardedBy("mLock") private void updateRemoteServiceLocked() { if (mRemoteService != null) { mRemoteService.destroy(); mRemoteService = null; } } void onPackageUpdatedLocked() { if (isDebug()) { Slog.v(TAG, "onPackageUpdatedLocked()"); } destroyAndRebindRemoteService(); } void onPackageRestartedLocked() { if (isDebug()) { Slog.v(TAG, "onPackageRestartedLocked()"); } destroyAndRebindRemoteService(); } private void destroyAndRebindRemoteService() { if (mRemoteService == null) { return; } if (isDebug()) { Slog.d(TAG, "Destroying the old remote service."); } mRemoteService.destroy(); mRemoteService = null; synchronized (mLock) { mZombie = true; } mRemoteService = getRemoteServiceLocked(); if (mRemoteService != null) { if (isDebug()) { Slog.d(TAG, "Rebinding to the new remote service."); } mRemoteService.reconnect(); } } /** * Called after the remote service connected, it's used to restore state from a 'zombie' * service (i.e., after it died). */ private void resurrectSessionsLocked() { final int numSessions = mSessionInfos.size(); if (isDebug()) { Slog.d(TAG, "Resurrecting remote service (" + mRemoteService + ") on " + numSessions + " sessions."); } for (SmartspaceSessionInfo sessionInfo : mSessionInfos.values()) { sessionInfo.resurrectSessionLocked(this, sessionInfo.mToken); } } @GuardedBy("mLock") @Nullable protected boolean resolveService( @NonNull final SmartspaceSessionId sessionId, @NonNull final AbstractRemoteService.AsyncRequest cb) { final RemoteSmartspaceService service = getRemoteServiceLocked(); if (service != null) { service.executeOnResolvedService(cb); } return service != null; } @GuardedBy("mLock") @Nullable private RemoteSmartspaceService getRemoteServiceLocked() { if (mRemoteService == null) { final String serviceName = getComponentNameLocked(); if (serviceName == null) { if (mMaster.verbose) { Slog.v(TAG, "getRemoteServiceLocked(): not set"); } return null; } ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName); mRemoteService = new RemoteSmartspaceService(getContext(), SmartspaceService.SERVICE_INTERFACE, serviceComponent, mUserId, this, mMaster.isBindInstantServiceAllowed(), mMaster.verbose); } return mRemoteService; } private static final class SmartspaceSessionInfo { private static final boolean DEBUG = false; // Do not submit with true @NonNull final IBinder mToken; @NonNull final IBinder.DeathRecipient mDeathRecipient; @NonNull private final SmartspaceSessionId mSessionId; @NonNull private final SmartspaceConfig mSmartspaceConfig; private final RemoteCallbackList mCallbacks = new RemoteCallbackList() { @Override public void onCallbackDied(ISmartspaceCallback callback) { if (DEBUG) { Slog.d(TAG, "Binder died for session Id=" + mSessionId + " and callback=" + callback.asBinder()); } if (mCallbacks.getRegisteredCallbackCount() == 0) { destroy(); } } }; SmartspaceSessionInfo( @NonNull final SmartspaceSessionId id, @NonNull final SmartspaceConfig context, @NonNull final IBinder token, @NonNull final IBinder.DeathRecipient deathRecipient) { if (DEBUG) { Slog.d(TAG, "Creating SmartspaceSessionInfo for session Id=" + id); } mSessionId = id; mSmartspaceConfig = context; mToken = token; mDeathRecipient = deathRecipient; } void addCallbackLocked(ISmartspaceCallback callback) { if (DEBUG) { Slog.d(TAG, "Storing callback for session Id=" + mSessionId + " and callback=" + callback.asBinder()); } mCallbacks.register(callback); } void removeCallbackLocked(ISmartspaceCallback callback) { if (DEBUG) { Slog.d(TAG, "Removing callback for session Id=" + mSessionId + " and callback=" + callback.asBinder()); } mCallbacks.unregister(callback); } boolean linkToDeath() { try { mToken.linkToDeath(mDeathRecipient, 0); } catch (RemoteException e) { if (DEBUG) { Slog.w(TAG, "Caller is dead before session can be started, sessionId: " + mSessionId); } return false; } return true; } void destroy() { if (DEBUG) { Slog.d(TAG, "Removing all callbacks for session Id=" + mSessionId + " and " + mCallbacks.getRegisteredCallbackCount() + " callbacks."); } if (mToken != null) { mToken.unlinkToDeath(mDeathRecipient, 0); } mCallbacks.kill(); } void resurrectSessionLocked(SmartspacePerUserService service, IBinder token) { int callbackCount = mCallbacks.getRegisteredCallbackCount(); if (DEBUG) { Slog.d(TAG, "Resurrecting remote service (" + service.getRemoteServiceLocked() + ") for session Id=" + mSessionId + " and " + callbackCount + " callbacks."); } service.onCreateSmartspaceSessionLocked(mSmartspaceConfig, mSessionId, token); mCallbacks.broadcast( callback -> service.registerSmartspaceUpdatesLocked(mSessionId, callback)); } } }