Introduce SystemUI-managed alternative system bars.

If a service component is defined in a new secure setting,
SystemUI will attempt to use that service as the status bar
provider.

Falls back to the existing in-process implementation configured
in the product config if the setting is missing or invalid.

Nothing changes yet from a permission point of view.  Alternative
system bar implementations still require the status bar permission.

Also nothing changes from an api point of view.  Alternative
system bar implementations use the existing IStatusBar interface.

This simply enables testing alternative system bar implementations
installed from other trusted, platform-signed packages.

Known caveat: the setting is stored per user, multi-user changes
will be handled in a future CL.

Change-Id: I0413df185f7e75f77ad2ae1bc3689306d5e6e0fb
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index 0879e0f..bf82466 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -194,6 +194,18 @@
         }
     };
 
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+                mCurrentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                if (true) Log.v(TAG, "userId " + mCurrentUserId + " is in the house");
+                userSwitched(mCurrentUserId);
+            }
+        }
+    };
+
     public void start() {
         mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
         mWindowManagerService = WindowManagerGlobal.getWindowManagerService();
@@ -271,16 +283,7 @@
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_USER_SWITCHED);
-        mContext.registerReceiver(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                String action = intent.getAction();
-                if (Intent.ACTION_USER_SWITCHED.equals(action)) {
-                    mCurrentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
-                    if (true) Log.v(TAG, "userId " + mCurrentUserId + " is in the house");
-                    userSwitched(mCurrentUserId);
-                }
-            }}, filter);
+        mContext.registerReceiver(mBroadcastReceiver, filter);
 
         mLocale = mContext.getResources().getConfiguration().locale;
     }
@@ -1177,4 +1180,11 @@
     public void resumeAutohide() {
         // hook for subclasses
     }
+
+    public void destroy() {
+        if (mSearchPanelView != null) {
+            mWindowManager.removeViewImmediate(mSearchPanelView);
+        }
+        mContext.unregisterReceiver(mBroadcastReceiver);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java b/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java
new file mode 100644
index 0000000..f763f03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ServiceMonitor.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2013 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.systemui.statusbar;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+
+import java.util.Arrays;
+
+/**
+ * Manages a persistent connection to a service component defined in a secure setting.
+ *
+ * <p>If a valid service component is specified in the secure setting, starts it up and keeps it
+ * running; handling setting changes, package updates, component disabling, and unexpected
+ * process termination.
+ *
+ * <p>Clients can listen for important events using the supplied {@link Callbacks}.
+ */
+public class ServiceMonitor {
+    private static final int RECHECK_DELAY = 2000;
+    private static final int WAIT_FOR_STOP = 500;
+
+    public interface Callbacks {
+        /** The service does not exist or failed to bind */
+        void onNoService();
+        /** The service is about to start, this is a chance to perform cleanup and
+         * delay the start if necessary */
+        long onServiceStartAttempt();
+    }
+
+    // internal handler + messages used to serialize access to internal state
+    public static final int MSG_START_SERVICE = 1;
+    public static final int MSG_CONTINUE_START_SERVICE = 2;
+    public static final int MSG_STOP_SERVICE = 3;
+    public static final int MSG_PACKAGE_INTENT = 4;
+    public static final int MSG_CHECK_BOUND = 5;
+    public static final int MSG_SERVICE_DISCONNECTED = 6;
+
+    private final Handler mHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            switch(msg.what) {
+                case MSG_START_SERVICE:
+                    startService();
+                    break;
+                case MSG_CONTINUE_START_SERVICE:
+                    continueStartService();
+                    break;
+                case MSG_STOP_SERVICE:
+                    stopService();
+                    break;
+                case MSG_PACKAGE_INTENT:
+                    packageIntent((Intent)msg.obj);
+                    break;
+                case MSG_CHECK_BOUND:
+                    checkBound();
+                    break;
+                case MSG_SERVICE_DISCONNECTED:
+                    serviceDisconnected((ComponentName)msg.obj);
+                    break;
+            }
+        }
+    };
+
+    private final ContentObserver mSettingObserver = new ContentObserver(mHandler) {
+        public void onChange(boolean selfChange) {
+            onChange(selfChange, null);
+        }
+
+        public void onChange(boolean selfChange, Uri uri) {
+            if (mDebug) Log.d(mTag, "onChange selfChange=" + selfChange + " uri=" + uri);
+            if (mBound) {
+                mHandler.sendEmptyMessage(MSG_STOP_SERVICE);
+            }
+            mHandler.sendEmptyMessageDelayed(MSG_START_SERVICE, WAIT_FOR_STOP);
+        }
+    };
+
+    private final class SC implements ServiceConnection, IBinder.DeathRecipient {
+        private ComponentName mName;
+        private IBinder mService;
+
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            if (mDebug) Log.d(mTag, "onServiceConnected name=" + name + " service=" + service);
+            mName = name;
+            mService = service;
+            try {
+                service.linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                Log.w(mTag, "Error linking to death", e);
+            }
+        }
+
+        public void onServiceDisconnected(ComponentName name) {
+            if (mDebug) Log.d(mTag, "onServiceDisconnected name=" + name);
+            boolean unlinked = mService.unlinkToDeath(this, 0);
+            if (mDebug) Log.d(mTag, "  unlinked=" + unlinked);
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_SERVICE_DISCONNECTED, mName));
+        }
+
+        public void binderDied() {
+            if (mDebug) Log.d(mTag, "binderDied");
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_SERVICE_DISCONNECTED, mName));
+        }
+    }
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        public void onReceive(Context context, Intent intent) {
+            String pkg = intent.getData().getSchemeSpecificPart();
+            if (mServiceName != null && mServiceName.getPackageName().equals(pkg)) {
+                mHandler.sendMessage(mHandler.obtainMessage(MSG_PACKAGE_INTENT, intent));
+            }
+        }
+    };
+
+    private final String mTag;
+    private final boolean mDebug;
+
+    private final Context mContext;
+    private final String mSettingKey;
+    private final Callbacks mCallbacks;
+
+    private ComponentName mServiceName;
+    private SC mServiceConnection;
+    private boolean mBound;
+
+    public ServiceMonitor(String ownerTag, boolean debug,
+            Context context, String settingKey, Callbacks callbacks) {
+        mTag = ownerTag + ".ServiceMonitor";
+        mDebug = debug;
+        mContext = context;
+        mSettingKey = settingKey;
+        mCallbacks = callbacks;
+    }
+
+    public void start() {
+        // listen for setting changes
+        ContentResolver cr = mContext.getContentResolver();
+        cr.registerContentObserver(Settings.Secure.getUriFor(mSettingKey),
+                false /*notifyForDescendents*/, mSettingObserver, UserHandle.USER_ALL);
+
+        // listen for package/component changes
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addDataScheme("package");
+        mContext.registerReceiver(mBroadcastReceiver, filter);
+
+        mHandler.sendEmptyMessage(MSG_START_SERVICE);
+    }
+
+    // everything below is called on the handler
+
+    private void packageIntent(Intent intent) {
+        if (mDebug) Log.d(mTag, "packageIntent intent=" + intent
+                + " extras=" + bundleToString(intent.getExtras()));
+        if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
+            mHandler.sendEmptyMessage(MSG_START_SERVICE);
+        } else if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
+            PackageManager pm = mContext.getPackageManager();
+            boolean serviceEnabled =
+                    pm.getApplicationEnabledSetting(mServiceName.getPackageName())
+                        != PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+                    && pm.getComponentEnabledSetting(mServiceName)
+                        != PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+            if (mBound && !serviceEnabled) {
+                stopService();
+                scheduleCheckBound();
+            } else if (!mBound && serviceEnabled) {
+                startService();
+            }
+        }
+    }
+
+    private void stopService() {
+        if (mDebug) Log.d(mTag, "stopService");
+        boolean stopped = mContext.stopService(new Intent().setComponent(mServiceName));
+        if (mDebug) Log.d(mTag, "  stopped=" + stopped);
+        mContext.unbindService(mServiceConnection);
+        mBound = false;
+    }
+
+    private void startService() {
+        String cn = Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                mSettingKey, UserHandle.USER_CURRENT);
+        mServiceName = cn == null ? null : ComponentName.unflattenFromString(cn);
+        if (mDebug) Log.d(mTag, "startService mServiceName=" + mServiceName);
+        if (mServiceName == null) {
+            mBound = false;
+            mCallbacks.onNoService();
+        } else {
+            long delay = mCallbacks.onServiceStartAttempt();
+            mHandler.sendEmptyMessageDelayed(MSG_CONTINUE_START_SERVICE, delay);
+        }
+    }
+
+    private void continueStartService() {
+        if (mDebug) Log.d(mTag, "continueStartService");
+        Intent intent = new Intent().setComponent(mServiceName);
+        try {
+            mServiceConnection = new SC();
+            mBound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+            if (mDebug) Log.d(mTag, "mBound: " + mBound);
+        } catch (Throwable t) {
+            Log.w(mTag, "Error binding to service: " + mServiceName, t);
+        }
+        if (!mBound) {
+            mCallbacks.onNoService();
+        }
+    }
+
+    private void serviceDisconnected(ComponentName serviceName) {
+        if (mDebug) Log.d(mTag, "serviceDisconnected serviceName=" + serviceName
+                + " mServiceName=" + mServiceName);
+        if (serviceName.equals(mServiceName)) {
+            mBound = false;
+            scheduleCheckBound();
+        }
+    }
+
+    private void checkBound() {
+        if (mDebug) Log.d(mTag, "checkBound mBound=" + mBound);
+        if (!mBound) {
+            startService();
+        }
+    }
+
+    private void scheduleCheckBound() {
+        mHandler.removeMessages(MSG_CHECK_BOUND);
+        mHandler.sendEmptyMessageDelayed(MSG_CHECK_BOUND, RECHECK_DELAY);
+    }
+
+    private static String bundleToString(Bundle bundle) {
+        if (bundle == null) return null;
+        StringBuilder sb = new StringBuilder('{');
+        for (String key : bundle.keySet()) {
+            if (sb.length() > 1) sb.append(',');
+            Object v = bundle.get(key);
+            v = (v instanceof String[]) ? Arrays.asList((String[]) v) : v;
+            sb.append(key).append('=').append(v);
+        }
+        return sb.append('}').toString();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SystemBars.java b/packages/SystemUI/src/com/android/systemui/statusbar/SystemBars.java
new file mode 100644
index 0000000..847bf96
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SystemBars.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2013 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.systemui.statusbar;
+
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.systemui.R;
+import com.android.systemui.SystemUI;
+
+/**
+ * Ensure a single status bar service implementation is running at all times.
+ *
+ * <p>The implementation either comes from a service component running in a remote process (defined
+ * using a secure setting), else falls back to using the in-process implementation according
+ * to the product config.
+ */
+public class SystemBars extends SystemUI implements ServiceMonitor.Callbacks {
+    private static final String TAG = "SystemBars";
+    private static final boolean DEBUG = true;
+    private static final int WAIT_FOR_BARS_TO_DIE = 500;
+
+    // manages the implementation coming from the remote process
+    private ServiceMonitor mServiceMonitor;
+
+    // in-process fallback implementation, per the product config
+    private BaseStatusBar mStatusBar;
+
+    @Override
+    public void start() {
+        if (DEBUG) Log.d(TAG, "start");
+        mServiceMonitor = new ServiceMonitor(TAG, DEBUG,
+                mContext, Settings.Secure.BAR_SERVICE_COMPONENT, this);
+        mServiceMonitor.start();  // will call onNoService if no remote service is found
+    }
+
+    @Override
+    public void onNoService() {
+        if (DEBUG) Log.d(TAG, "onNoService");
+        createStatusBarFromConfig();  // fallback to using an in-process implementation
+    }
+
+    @Override
+    public long onServiceStartAttempt() {
+        if (DEBUG) Log.d(TAG, "onServiceStartAttempt mStatusBar="+mStatusBar);
+        if (mStatusBar != null) {
+            // tear down the in-process version, we'll recreate it again if needed
+            mStatusBar.destroy();
+            mStatusBar = null;
+            return WAIT_FOR_BARS_TO_DIE;
+        }
+        return 0;
+    }
+
+    private void createStatusBarFromConfig() {
+        if (DEBUG) Log.d(TAG, "createStatusBarFromConfig");
+        final String clsName = mContext.getString(R.string.config_statusBarComponent);
+        if (clsName == null || clsName.length() == 0) {
+            throw andLog("No status bar component configured", null);
+        }
+        Class<?> cls = null;
+        try {
+            cls = mContext.getClassLoader().loadClass(clsName);
+        } catch (Throwable t) {
+            throw andLog("Error loading status bar component: " + clsName, t);
+        }
+        try {
+            mStatusBar = (BaseStatusBar) cls.newInstance();
+        } catch (Throwable t) {
+            throw andLog("Error creating status bar component: " + clsName, t);
+        }
+        mStatusBar.mContext = mContext;
+        mStatusBar.start();
+        if (DEBUG) Log.d(TAG, "started " + mStatusBar.getClass().getSimpleName());
+    }
+
+    private RuntimeException andLog(String msg, Throwable t) {
+        Log.w(TAG, msg, t);
+        throw new RuntimeException(msg, t);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index d86760f..dca8c97e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -2691,4 +2691,16 @@
         public void setBounds(Rect bounds) {
         }
     }
+
+    @Override
+    public void destroy() {
+        super.destroy();
+        if (mStatusBarWindow != null) {
+            mWindowManager.removeViewImmediate(mStatusBarWindow);
+        }
+        if (mNavigationBarView != null) {
+            mWindowManager.removeViewImmediate(mNavigationBarView);
+        }
+        mContext.unregisterReceiver(mBroadcastReceiver);
+    }
 }