Add skeleton implementation for Attention API

This service will let the system know whether the user is paying attention to
the device. This may be useful in certain scenarios - like keeping the screen
on, even when there has been no touch related user activity.

New permission BIND_ATTENTION_SERVICE is added to ensure that only the system
can bind to the attention service.

Test: manually tested the lifecycle is as expected
Bug: 111939367
Change-Id: I2dab9c69f3d0c6efb0db572f797f517dc6efcc72
diff --git a/services/core/java/com/android/server/attention/AttentionManagerService.java b/services/core/java/com/android/server/attention/AttentionManagerService.java
new file mode 100644
index 0000000..f60d6b0
--- /dev/null
+++ b/services/core/java/com/android/server/attention/AttentionManagerService.java
@@ -0,0 +1,548 @@
+/*
+ * 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.attention;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.attention.AttentionManagerInternal;
+import android.attention.AttentionManagerInternal.AttentionCallbackInternal;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.service.attention.AttentionService;
+import android.service.attention.AttentionService.AttentionFailureCodes;
+import android.service.attention.IAttentionCallback;
+import android.service.attention.IAttentionService;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
+import com.android.server.SystemService;
+
+import java.io.PrintWriter;
+
+/**
+ * An attention service implementation that runs in System Server process.
+ * This service publishes a LocalService and reroutes calls to a {@link AttentionService} that it
+ * manages.
+ */
+public class AttentionManagerService extends SystemService {
+    private static final String LOG_TAG = "AttentionManagerService";
+
+    /** Service will unbind if connection is not used for that amount of time. */
+    private static final long CONNECTION_TTL_MILLIS = 60_000;
+
+    /** If the check attention called within that period - cached value will be returned. */
+    private static final long STALE_AFTER_MILLIS = 5_000;
+
+    private final Context mContext;
+    private final PowerManager mPowerManager;
+    private final ActivityManager mActivityManager;
+    private final Object mLock;
+    @GuardedBy("mLock")
+    private final SparseArray<UserState> mUserStates = new SparseArray<>();
+    private final AttentionHandler mAttentionHandler;
+
+    private ComponentName mComponentName;
+
+    public AttentionManagerService(Context context) {
+        super(context);
+        mContext = Preconditions.checkNotNull(context);
+        mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+        mActivityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+        mLock = new Object();
+        mAttentionHandler = new AttentionHandler();
+    }
+
+    @Override
+    public void onStart() {
+        publishLocalService(AttentionManagerInternal.class, new LocalService());
+    }
+
+    @Override
+    public void onStopUser(int userId) {
+        cancelAndUnbindLocked(peekUserStateLocked(userId),
+                AttentionService.ATTENTION_FAILURE_UNKNOWN);
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        super.onBootPhase(phase);
+        if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
+            mComponentName = resolveAttentionService(mContext);
+            if (mComponentName != null) {
+                // If the service is supported we want to keep receiving the screen off events.
+                mContext.registerReceiver(new ScreenStateReceiver(),
+                        new IntentFilter(Intent.ACTION_SCREEN_OFF));
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if attention service is supported on this device.
+     */
+    public boolean isAttentionServiceSupported() {
+        return mComponentName != null;
+    }
+
+    /**
+     * Checks whether user attention is at the screen and calls in the provided callback.
+     *
+     * @return {@code true} if the framework was able to send the provided callback to the service
+     */
+    public boolean checkAttention(int requestCode, long timeout,
+            AttentionCallbackInternal callback) {
+        Preconditions.checkNotNull(callback);
+
+        if (!isAttentionServiceSupported()) {
+            Slog.w(LOG_TAG, "Trying to call checkAttention() on an unsupported device.");
+            return false;
+        }
+
+        // don't allow attention check in screen off state
+        if (!mPowerManager.isInteractive()) {
+            return false;
+        }
+
+        synchronized (mLock) {
+            unbindAfterTimeoutLocked();
+
+            final UserState userState = getOrCreateCurrentUserStateLocked();
+            // lazily start the service, which should be very lightweight to start
+            if (!userState.bindLocked()) {
+                return false;
+            }
+
+            if (userState.mService == null) {
+                // make sure every callback is called back
+                if (userState.mPendingAttentionCheck != null) {
+                    userState.mPendingAttentionCheck.cancel(
+                            AttentionService.ATTENTION_FAILURE_UNKNOWN);
+                }
+                userState.mPendingAttentionCheck = new PendingAttentionCheck(requestCode,
+                        callback, () -> checkAttention(requestCode, timeout, callback));
+            } else {
+                try {
+                    // throttle frequent requests
+                    final AttentionCheckCache attentionCheckCache = userState.mAttentionCheckCache;
+                    if (attentionCheckCache != null && SystemClock.uptimeMillis()
+                            < attentionCheckCache.mLastComputed + STALE_AFTER_MILLIS) {
+                        callback.onSuccess(requestCode, attentionCheckCache.mResult,
+                                attentionCheckCache.mTimestamp);
+                        return true;
+                    }
+
+                    cancelAfterTimeoutLocked(timeout);
+
+                    userState.mCurrentAttentionCheckRequestCode = requestCode;
+                    userState.mService.checkAttention(requestCode, new IAttentionCallback.Stub() {
+                        @Override
+                        public void onSuccess(int requestCode, int result, long timestamp) {
+                            callback.onSuccess(requestCode, result, timestamp);
+                            userState.mAttentionCheckCache = new AttentionCheckCache(
+                                    SystemClock.uptimeMillis(), result,
+                                    timestamp);
+                        }
+
+                        @Override
+                        public void onFailure(int requestCode, int error) {
+                            callback.onFailure(requestCode, error);
+                        }
+
+                        @Override
+                        public IBinder asBinder() {
+                            return null;
+                        }
+                    });
+                } catch (RemoteException e) {
+                    Slog.e(LOG_TAG, "Cannot call into the AttentionService");
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    /** Cancels the specified attention check. */
+    public void cancelAttentionCheck(int requestCode) {
+        final UserState userState = getOrCreateCurrentUserStateLocked();
+        try {
+            userState.mService.cancelAttentionCheck(requestCode);
+        } catch (RemoteException e) {
+            Slog.e(LOG_TAG, "Cannot call into the AttentionService");
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void unbindAfterTimeoutLocked() {
+        mAttentionHandler.sendEmptyMessageDelayed(AttentionHandler.CONNECTION_EXPIRED,
+                CONNECTION_TTL_MILLIS);
+    }
+
+    @GuardedBy("mLock")
+    private void cancelAfterTimeoutLocked(long timeout) {
+        mAttentionHandler.sendEmptyMessageDelayed(AttentionHandler.ATTENTION_CHECK_TIMEOUT,
+                timeout);
+    }
+
+
+    @GuardedBy("mLock")
+    private UserState getOrCreateCurrentUserStateLocked() {
+        return getOrCreateUserStateLocked(mActivityManager.getCurrentUser());
+    }
+
+    @GuardedBy("mLock")
+    private UserState getOrCreateUserStateLocked(int userId) {
+        UserState result = mUserStates.get(userId);
+        if (result == null) {
+            result = new UserState(userId, mContext, mLock);
+            mUserStates.put(userId, result);
+        }
+        return result;
+    }
+
+    @GuardedBy("mLock")
+    UserState peekCurrentUserStateLocked() {
+        return peekUserStateLocked(mActivityManager.getCurrentUser());
+    }
+
+    @GuardedBy("mLock")
+    UserState peekUserStateLocked(int userId) {
+        return mUserStates.get(userId);
+    }
+
+    /**
+     * Provides attention service component name at runtime, making sure it's provided by the
+     * system.
+     */
+    private static ComponentName resolveAttentionService(Context context) {
+        // TODO(b/111939367): add a flag to turn on/off.
+        final String componentNameString = context.getString(
+                R.string.config_defaultAttentionService);
+
+        if (TextUtils.isEmpty(componentNameString)) {
+            return null;
+        }
+
+        final ComponentName componentName = ComponentName.unflattenFromString(componentNameString);
+        if (componentName == null) {
+            return null;
+        }
+
+        final Intent intent = new Intent(AttentionService.SERVICE_INTERFACE).setPackage(
+                componentName.getPackageName());
+
+        // Make sure that only system apps can declare the AttentionService.
+        final ResolveInfo resolveInfo = context.getPackageManager().resolveService(intent,
+                PackageManager.MATCH_SYSTEM_ONLY);
+        if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+            Slog.wtf(LOG_TAG, String.format("Service %s not found in package %s",
+                    AttentionService.SERVICE_INTERFACE, componentName
+            ));
+            return null;
+        }
+
+        final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+        final String permission = serviceInfo.permission;
+        if (Manifest.permission.BIND_ATTENTION_SERVICE.equals(permission)) {
+            return serviceInfo.getComponentName();
+        }
+        Slog.e(LOG_TAG, String.format(
+                "Service %s should require %s permission. Found %s permission",
+                serviceInfo.getComponentName(),
+                Manifest.permission.BIND_ATTENTION_SERVICE,
+                serviceInfo.permission));
+        return null;
+    }
+
+    private void dumpInternal(PrintWriter pw) {
+        if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        ipw.println("Attention Manager Service (dumpsys attention)\n");
+
+        ipw.printPair("context", mContext);
+        pw.println();
+        synchronized (mLock) {
+            int size = mUserStates.size();
+            ipw.print("Number user states: ");
+            pw.println(size);
+            if (size > 0) {
+                ipw.increaseIndent();
+                for (int i = 0; i < size; i++) {
+                    UserState userState = mUserStates.valueAt(i);
+                    ipw.print(i);
+                    ipw.print(":");
+                    userState.dump(ipw);
+                    ipw.println();
+                }
+                ipw.decreaseIndent();
+            }
+        }
+    }
+
+    private final class LocalService extends AttentionManagerInternal {
+        @Override
+        public boolean isAttentionServiceSupported() {
+            return AttentionManagerService.this.isAttentionServiceSupported();
+        }
+
+        @Override
+        public boolean checkAttention(int requestCode, long timeout,
+                AttentionCallbackInternal callback) {
+            return AttentionManagerService.this.checkAttention(requestCode, timeout, callback);
+        }
+
+        @Override
+        public void cancelAttentionCheck(int requestCode) {
+            AttentionManagerService.this.cancelAttentionCheck(requestCode);
+        }
+    }
+
+    private static final class AttentionCheckCache {
+        private final long mLastComputed;
+        private final int mResult;
+        private final long mTimestamp;
+
+        AttentionCheckCache(long lastComputed, @AttentionService.AttentionSuccessCodes int result,
+                long timestamp) {
+            mLastComputed = lastComputed;
+            mResult = result;
+            mTimestamp = timestamp;
+        }
+    }
+
+    private static final class PendingAttentionCheck {
+        private final int mRequestCode;
+        private final AttentionCallbackInternal mCallback;
+        private final Runnable mRunnable;
+
+        PendingAttentionCheck(int requestCode, AttentionCallbackInternal callback,
+                Runnable runnable) {
+            mRequestCode = requestCode;
+            mCallback = callback;
+            mRunnable = runnable;
+        }
+
+        void cancel(@AttentionFailureCodes int failureCode) {
+            mCallback.onFailure(mRequestCode, failureCode);
+        }
+
+        void run() {
+            mRunnable.run();
+        }
+    }
+
+    private static final class UserState {
+        final AttentionServiceConnection mConnection = new AttentionServiceConnection();
+
+        @GuardedBy("mLock")
+        IAttentionService mService;
+        @GuardedBy("mLock")
+        boolean mBinding;
+        @GuardedBy("mLock")
+        int mCurrentAttentionCheckRequestCode;
+        @GuardedBy("mLock")
+        PendingAttentionCheck mPendingAttentionCheck;
+
+        @GuardedBy("mLock")
+        AttentionCheckCache mAttentionCheckCache;
+
+        @UserIdInt
+        final int mUserId;
+        final Context mContext;
+        final Object mLock;
+
+        private UserState(int userId, Context context, Object lock) {
+            mUserId = userId;
+            mContext = Preconditions.checkNotNull(context);
+            mLock = Preconditions.checkNotNull(lock);
+        }
+
+
+        @GuardedBy("mLock")
+        private void handlePendingCallbackLocked() {
+            if (mService != null && mPendingAttentionCheck != null) {
+                mPendingAttentionCheck.run();
+                mPendingAttentionCheck = null;
+            }
+        }
+
+        /** Binds to the system's AttentionService which provides an actual implementation. */
+        @GuardedBy("mLock")
+        private boolean bindLocked() {
+            // No need to bind if service is binding or has already been bound.
+            if (mBinding || mService != null) {
+                return true;
+            }
+
+            final boolean willBind;
+            final long identity = Binder.clearCallingIdentity();
+
+            try {
+                final ComponentName componentName =
+                        resolveAttentionService(mContext);
+                if (componentName == null) {
+                    // Might happen if the storage is encrypted and the user is not unlocked
+                    return false;
+                }
+                final Intent mServiceIntent = new Intent(
+                        AttentionService.SERVICE_INTERFACE).setComponent(
+                        componentName);
+                willBind = mContext.bindServiceAsUser(mServiceIntent, mConnection,
+                        Context.BIND_AUTO_CREATE, UserHandle.CURRENT);
+                mBinding = willBind;
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+            return willBind;
+        }
+
+        private void dump(IndentingPrintWriter pw) {
+            pw.printPair("context", mContext);
+            pw.printPair("userId", mUserId);
+            synchronized (mLock) {
+                pw.printPair("binding", mBinding);
+                pw.printPair("isAttentionCheckPending", mPendingAttentionCheck != null);
+            }
+        }
+
+        private final class AttentionServiceConnection implements ServiceConnection {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                init(IAttentionService.Stub.asInterface(service));
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                cleanupService();
+            }
+
+            @Override
+            public void onBindingDied(ComponentName name) {
+                cleanupService();
+            }
+
+            @Override
+            public void onNullBinding(ComponentName name) {
+                cleanupService();
+            }
+
+            void cleanupService() {
+                init(null);
+            }
+
+            private void init(@Nullable IAttentionService service) {
+                synchronized (mLock) {
+                    mService = service;
+                    mBinding = false;
+                    handlePendingCallbackLocked();
+                }
+            }
+        }
+    }
+
+    private class AttentionHandler extends Handler {
+        private static final int CONNECTION_EXPIRED = 1;
+        private static final int ATTENTION_CHECK_TIMEOUT = 2;
+
+        AttentionHandler() {
+            super(Looper.myLooper());
+        }
+
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                // Do not occupy resources when not in use - unbind proactively.
+                case CONNECTION_EXPIRED: {
+                    for (int i = 0; i < mUserStates.size(); i++) {
+                        cancelAndUnbindLocked(mUserStates.valueAt(i),
+                                AttentionService.ATTENTION_FAILURE_UNKNOWN);
+                    }
+
+                }
+                break;
+
+                // Callee is no longer interested in the attention check result - cancel.
+                case ATTENTION_CHECK_TIMEOUT: {
+                    cancelAndUnbindLocked(peekCurrentUserStateLocked(),
+                            AttentionService.ATTENTION_FAILURE_TIMED_OUT);
+                }
+                break;
+
+                default:
+                    break;
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void cancelAndUnbindLocked(UserState userState,
+            @AttentionFailureCodes int failureCode) {
+        synchronized (mLock) {
+            if (userState != null && userState.mService != null) {
+                try {
+                    userState.mService.cancelAttentionCheck(
+                            userState.mCurrentAttentionCheckRequestCode);
+                } catch (RemoteException e) {
+                    Slog.e(LOG_TAG, "Unable to cancel attention check");
+                }
+
+                if (userState.mPendingAttentionCheck != null) {
+                    userState.mPendingAttentionCheck.cancel(failureCode);
+                }
+                mContext.unbindService(userState.mConnection);
+                userState.mConnection.cleanupService();
+                mUserStates.remove(userState.mUserId);
+            }
+        }
+    }
+
+    /**
+     * Unbinds and stops the service when the screen off intent is received.
+     * Attention service only makes sense when screen is ON; disconnect and stop service otherwise.
+     */
+    private final class ScreenStateReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
+                cancelAndUnbindLocked(peekCurrentUserStateLocked(),
+                        AttentionService.ATTENTION_FAILURE_UNKNOWN);
+            }
+        }
+    }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index fef2db9..cef47ca 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -74,6 +74,7 @@
 import com.android.internal.widget.ILockSettings;
 import com.android.server.am.ActivityManagerService;
 import com.android.server.appbinding.AppBindingService;
+import com.android.server.attention.AttentionManagerService;
 import com.android.server.audio.AudioService;
 import com.android.server.biometrics.BiometricService;
 import com.android.server.biometrics.face.FaceService;
@@ -148,10 +149,11 @@
 import com.android.server.wm.ActivityTaskManagerService;
 import com.android.server.wm.WindowManagerGlobalLock;
 import com.android.server.wm.WindowManagerService;
-import com.google.android.startop.iorap.IorapForwardingService;
 
 import dalvik.system.VMRuntime;
 
+import com.google.android.startop.iorap.IorapForwardingService;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.Locale;
@@ -1230,6 +1232,10 @@
                 traceEnd();
             }
 
+            traceBeginAndSlog("StartAttentionManagerService");
+            mSystemServiceManager.startService(AttentionManagerService.class);
+            traceEnd();
+
             traceBeginAndSlog("StartNetworkScoreService");
             mSystemServiceManager.startService(NetworkScoreService.Lifecycle.class);
             traceEnd();