Add TabletModeChangedListener for SystemUI.

Bug: 23256614
Change-Id: I6e5d636c24a84846cfad84da800911a469689dda
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 17b4f9c..5a13672 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -17,6 +17,7 @@
 package com.android.server.input;
 
 import android.view.Display;
+import com.android.internal.os.SomeArgs;
 import com.android.internal.R;
 import com.android.internal.util.XmlUtils;
 import com.android.server.DisplayThread;
@@ -52,6 +53,7 @@
 import android.hardware.input.InputDeviceIdentifier;
 import android.hardware.input.InputManager;
 import android.hardware.input.InputManagerInternal;
+import android.hardware.input.ITabletModeChangedListener;
 import android.hardware.input.KeyboardLayout;
 import android.hardware.input.TouchCalibration;
 import android.os.Binder;
@@ -93,6 +95,7 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 
 import libcore.io.Streams;
 import libcore.util.Objects;
@@ -112,6 +115,7 @@
     private static final int MSG_RELOAD_KEYBOARD_LAYOUTS = 3;
     private static final int MSG_UPDATE_KEYBOARD_LAYOUTS = 4;
     private static final int MSG_RELOAD_DEVICE_ALIASES = 5;
+    private static final int MSG_DELIVER_TABLET_MODE_CHANGED = 6;
 
     // Pointer to native input manager service object.
     private final long mPtr;
@@ -124,6 +128,13 @@
     private boolean mSystemReady;
     private NotificationManager mNotificationManager;
 
+    private final Object mTabletModeLock = new Object();
+    // List of currently registered tablet mode changed listeners by process id
+    private final SparseArray<TabletModeChangedListenerRecord> mTabletModeChangedListeners =
+            new SparseArray<>(); // guarded by mTabletModeLock
+    private final List<TabletModeChangedListenerRecord> mTempTabletModeChangedListenersToNotify =
+            new ArrayList<>();
+
     // Persistent data store.  Must be locked each time during use.
     private final PersistentDataStore mDataStore = new PersistentDataStore();
 
@@ -227,6 +238,11 @@
     /** Switch code: Lid switch.  When set, lid is shut. */
     public static final int SW_LID = 0x00;
 
+    /** Switch code: Tablet mode switch.
+     * When set, the device is in tablet mode (i.e. no keyboard is connected).
+     */
+    public static final int SW_TABLET_MODE = 0x01;
+
     /** Switch code: Keypad slide.  When set, keyboard is exposed. */
     public static final int SW_KEYPAD_SLIDE = 0x0a;
 
@@ -246,6 +262,7 @@
     public static final int SW_CAMERA_LENS_COVER = 0x09;
 
     public static final int SW_LID_BIT = 1 << SW_LID;
+    public static final int SW_TABLET_MODE_BIT = 1 << SW_TABLET_MODE;
     public static final int SW_KEYPAD_SLIDE_BIT = 1 << SW_KEYPAD_SLIDE;
     public static final int SW_HEADPHONE_INSERT_BIT = 1 << SW_HEADPHONE_INSERT;
     public static final int SW_MICROPHONE_INSERT_BIT = 1 << SW_MICROPHONE_INSERT;
@@ -774,6 +791,57 @@
         }
     }
 
+    @Override // Binder call
+    public void registerTabletModeChangedListener(ITabletModeChangedListener listener) {
+        if (!checkCallingPermission(android.Manifest.permission.TABLET_MODE_LISTENER,
+                "registerTabletModeChangedListener()")) {
+            throw new SecurityException("Requires TABLET_MODE_LISTENER permission");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+
+        synchronized (mTabletModeLock) {
+            final int callingPid = Binder.getCallingPid();
+            if (mTabletModeChangedListeners.get(callingPid) != null) {
+                throw new IllegalStateException("The calling process has already registered "
+                        + "a TabletModeChangedListener.");
+            }
+            TabletModeChangedListenerRecord record =
+                    new TabletModeChangedListenerRecord(callingPid, listener);
+            try {
+                IBinder binder = listener.asBinder();
+                binder.linkToDeath(record, 0);
+            } catch (RemoteException ex) {
+                throw new RuntimeException(ex);
+            }
+            mTabletModeChangedListeners.put(callingPid, record);
+        }
+    }
+
+    private void onTabletModeChangedListenerDied(int pid) {
+        synchronized (mTabletModeLock) {
+            mTabletModeChangedListeners.remove(pid);
+        }
+    }
+
+    // Must be called on handler
+    private void deliverTabletModeChanged(long whenNanos, boolean inTabletMode) {
+        mTempTabletModeChangedListenersToNotify.clear();
+        final int numListeners;
+        synchronized (mTabletModeLock) {
+            numListeners = mTabletModeChangedListeners.size();
+            for (int i = 0; i < numListeners; i++) {
+                mTempTabletModeChangedListenersToNotify.add(
+                        mTabletModeChangedListeners.valueAt(i));
+            }
+        }
+        for (int i = 0; i < numListeners; i++) {
+            mTempTabletModeChangedListenersToNotify.get(i).notifyTabletModeChanged(
+                    whenNanos, inTabletMode);
+        }
+    }
+
     // Must be called on handler.
     private void showMissingKeyboardLayoutNotification(InputDevice device) {
         if (!mKeyboardLayoutNotificationShown) {
@@ -1419,6 +1487,15 @@
             mWiredAccessoryCallbacks.notifyWiredAccessoryChanged(whenNanos, switchValues,
                     switchMask);
         }
+
+        if ((switchMask & SW_TABLET_MODE) != 0) {
+            SomeArgs args = SomeArgs.obtain();
+            args.argi1 = (int) (whenNanos & 0xFFFFFFFF);
+            args.argi2 = (int) (whenNanos >> 32);
+            args.arg1 = Boolean.valueOf((switchValues & SW_TABLET_MODE_BIT) != 0);
+            mHandler.obtainMessage(MSG_DELIVER_TABLET_MODE_CHANGED,
+                    args).sendToTarget();
+        }
     }
 
     // Native callback.
@@ -1664,6 +1741,12 @@
                 case MSG_RELOAD_DEVICE_ALIASES:
                     reloadDeviceAliases();
                     break;
+                case MSG_DELIVER_TABLET_MODE_CHANGED:
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    long whenNanos = (args.argi1 & 0xFFFFFFFFl) | ((long) args.argi2 << 32);
+                    boolean inTabletMode = (boolean) args.arg1;
+                    deliverTabletModeChanged(whenNanos, inTabletMode);
+                    break;
             }
         }
     }
@@ -1755,6 +1838,34 @@
         }
     }
 
+    private final class TabletModeChangedListenerRecord implements DeathRecipient {
+        private final int mPid;
+        private final ITabletModeChangedListener mListener;
+
+        public TabletModeChangedListenerRecord(int pid, ITabletModeChangedListener listener) {
+            mPid = pid;
+            mListener = listener;
+        }
+
+        @Override
+        public void binderDied() {
+            if (DEBUG) {
+                Slog.d(TAG, "Tablet mode changed listener for pid " + mPid + " died.");
+            }
+            onTabletModeChangedListenerDied(mPid);
+        }
+
+        public void notifyTabletModeChanged(long whenNanos, boolean inTabletMode) {
+            try {
+                mListener.onTabletModeChanged(whenNanos, inTabletMode);
+            } catch (RemoteException ex) {
+                Slog.w(TAG, "Failed to notify process " + mPid +
+                        " that tablet mode changed, assuming it died.", ex);
+                binderDied();
+            }
+        }
+    }
+
     private final class VibratorToken implements DeathRecipient {
         public final int mDeviceId;
         public final IBinder mToken;