diff options
Diffstat (limited to 'services/core/java/com/android/server/dreams')
-rw-r--r-- | services/core/java/com/android/server/dreams/DreamController.java | 273 | ||||
-rw-r--r-- | services/core/java/com/android/server/dreams/DreamManagerService.java | 425 |
2 files changed, 698 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java new file mode 100644 index 0000000..85ef33e --- /dev/null +++ b/services/core/java/com/android/server/dreams/DreamController.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2012 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.dreams; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.IBinder.DeathRecipient; +import android.os.UserHandle; +import android.service.dreams.DreamService; +import android.service.dreams.IDreamService; +import android.util.Slog; +import android.view.IWindowManager; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; + +import java.io.PrintWriter; +import java.util.NoSuchElementException; + +/** + * Internal controller for starting and stopping the current dream and managing related state. + * + * Assumes all operations are called from the dream handler thread. + */ +final class DreamController { + private static final String TAG = "DreamController"; + + // How long we wait for a newly bound dream to create the service connection + private static final int DREAM_CONNECTION_TIMEOUT = 5 * 1000; + + private final Context mContext; + private final Handler mHandler; + private final Listener mListener; + private final IWindowManager mIWindowManager; + + private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED) + .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + private final Intent mDreamingStoppedIntent = new Intent(Intent.ACTION_DREAMING_STOPPED) + .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + + private final Intent mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + + private DreamRecord mCurrentDream; + + private final Runnable mStopUnconnectedDreamRunnable = new Runnable() { + @Override + public void run() { + if (mCurrentDream != null && mCurrentDream.mBound && !mCurrentDream.mConnected) { + Slog.w(TAG, "Bound dream did not connect in the time allotted"); + stopDream(); + } + } + }; + + public DreamController(Context context, Handler handler, Listener listener) { + mContext = context; + mHandler = handler; + mListener = listener; + mIWindowManager = WindowManagerGlobal.getWindowManagerService(); + } + + public void dump(PrintWriter pw) { + pw.println("Dreamland:"); + if (mCurrentDream != null) { + pw.println(" mCurrentDream:"); + pw.println(" mToken=" + mCurrentDream.mToken); + pw.println(" mName=" + mCurrentDream.mName); + pw.println(" mIsTest=" + mCurrentDream.mIsTest); + pw.println(" mUserId=" + mCurrentDream.mUserId); + pw.println(" mBound=" + mCurrentDream.mBound); + pw.println(" mService=" + mCurrentDream.mService); + pw.println(" mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast); + } else { + pw.println(" mCurrentDream: null"); + } + } + + public void startDream(Binder token, ComponentName name, boolean isTest, int userId) { + stopDream(); + + // Close the notification shade. Don't need to send to all, but better to be explicit. + mContext.sendBroadcastAsUser(mCloseNotificationShadeIntent, UserHandle.ALL); + + Slog.i(TAG, "Starting dream: name=" + name + ", isTest=" + isTest + ", userId=" + userId); + + mCurrentDream = new DreamRecord(token, name, isTest, userId); + + try { + mIWindowManager.addWindowToken(token, WindowManager.LayoutParams.TYPE_DREAM); + } catch (RemoteException ex) { + Slog.e(TAG, "Unable to add window token for dream.", ex); + stopDream(); + return; + } + + Intent intent = new Intent(DreamService.SERVICE_INTERFACE); + intent.setComponent(name); + intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + try { + if (!mContext.bindServiceAsUser(intent, mCurrentDream, + Context.BIND_AUTO_CREATE, new UserHandle(userId))) { + Slog.e(TAG, "Unable to bind dream service: " + intent); + stopDream(); + return; + } + } catch (SecurityException ex) { + Slog.e(TAG, "Unable to bind dream service: " + intent, ex); + stopDream(); + return; + } + + mCurrentDream.mBound = true; + mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT); + } + + public void stopDream() { + if (mCurrentDream == null) { + return; + } + + final DreamRecord oldDream = mCurrentDream; + mCurrentDream = null; + Slog.i(TAG, "Stopping dream: name=" + oldDream.mName + + ", isTest=" + oldDream.mIsTest + ", userId=" + oldDream.mUserId); + + mHandler.removeCallbacks(mStopUnconnectedDreamRunnable); + + if (oldDream.mSentStartBroadcast) { + mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); + } + + if (oldDream.mService != null) { + // Tell the dream that it's being stopped so that + // it can shut down nicely before we yank its window token out from + // under it. + try { + oldDream.mService.detach(); + } catch (RemoteException ex) { + // we don't care; this thing is on the way out + } + + try { + oldDream.mService.asBinder().unlinkToDeath(oldDream, 0); + } catch (NoSuchElementException ex) { + // don't care + } + oldDream.mService = null; + } + + if (oldDream.mBound) { + mContext.unbindService(oldDream); + } + + try { + mIWindowManager.removeWindowToken(oldDream.mToken); + } catch (RemoteException ex) { + Slog.w(TAG, "Error removing window token for dream.", ex); + } + + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onDreamStopped(oldDream.mToken); + } + }); + } + + private void attach(IDreamService service) { + try { + service.asBinder().linkToDeath(mCurrentDream, 0); + service.attach(mCurrentDream.mToken); + } catch (RemoteException ex) { + Slog.e(TAG, "The dream service died unexpectedly.", ex); + stopDream(); + return; + } + + mCurrentDream.mService = service; + + if (!mCurrentDream.mIsTest) { + mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL); + mCurrentDream.mSentStartBroadcast = true; + } + } + + /** + * Callback interface to be implemented by the {@link DreamManagerService}. + */ + public interface Listener { + void onDreamStopped(Binder token); + } + + private final class DreamRecord implements DeathRecipient, ServiceConnection { + public final Binder mToken; + public final ComponentName mName; + public final boolean mIsTest; + public final int mUserId; + + public boolean mBound; + public boolean mConnected; + public IDreamService mService; + public boolean mSentStartBroadcast; + + public DreamRecord(Binder token, ComponentName name, + boolean isTest, int userId) { + mToken = token; + mName = name; + mIsTest = isTest; + mUserId = userId; + } + + // May be called on any thread. + @Override + public void binderDied() { + mHandler.post(new Runnable() { + @Override + public void run() { + mService = null; + if (mCurrentDream == DreamRecord.this) { + stopDream(); + } + } + }); + } + + // May be called on any thread. + @Override + public void onServiceConnected(ComponentName name, final IBinder service) { + mHandler.post(new Runnable() { + @Override + public void run() { + mConnected = true; + if (mCurrentDream == DreamRecord.this && mService == null) { + attach(IDreamService.Stub.asInterface(service)); + } + } + }); + } + + // May be called on any thread. + @Override + public void onServiceDisconnected(ComponentName name) { + mHandler.post(new Runnable() { + @Override + public void run() { + mService = null; + if (mCurrentDream == DreamRecord.this) { + stopDream(); + } + } + }); + } + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java new file mode 100644 index 0000000..b6e7781 --- /dev/null +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2012 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.dreams; + +import com.android.internal.util.DumpUtils; + +import android.app.ActivityManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.PowerManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.service.dreams.IDreamManager; +import android.util.Slog; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import libcore.util.Objects; + +/** + * Service api for managing dreams. + * + * @hide + */ +public final class DreamManagerService extends IDreamManager.Stub { + private static final boolean DEBUG = false; + private static final String TAG = "DreamManagerService"; + + private final Object mLock = new Object(); + + private final Context mContext; + private final DreamHandler mHandler; + private final DreamController mController; + private final PowerManager mPowerManager; + + private Binder mCurrentDreamToken; + private ComponentName mCurrentDreamName; + private int mCurrentDreamUserId; + private boolean mCurrentDreamIsTest; + + public DreamManagerService(Context context, Handler mainHandler) { + mContext = context; + mHandler = new DreamHandler(mainHandler.getLooper()); + mController = new DreamController(context, mHandler, mControllerListener); + + mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + } + + public void systemRunning() { + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + synchronized (mLock) { + stopDreamLocked(); + } + } + }, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission("android.permission.DUMP") + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump DreamManager from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + pw.println("DREAM MANAGER (dumpsys dreams)"); + pw.println(); + + pw.println("mCurrentDreamToken=" + mCurrentDreamToken); + pw.println("mCurrentDreamName=" + mCurrentDreamName); + pw.println("mCurrentDreamUserId=" + mCurrentDreamUserId); + pw.println("mCurrentDreamIsTest=" + mCurrentDreamIsTest); + pw.println(); + + DumpUtils.dumpAsync(mHandler, new DumpUtils.Dump() { + @Override + public void dump(PrintWriter pw) { + mController.dump(pw); + } + }, pw, 200); + } + + @Override // Binder call + public ComponentName[] getDreamComponents() { + checkPermission(android.Manifest.permission.READ_DREAM_STATE); + + final int userId = UserHandle.getCallingUserId(); + final long ident = Binder.clearCallingIdentity(); + try { + return getDreamComponentsForUser(userId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + public void setDreamComponents(ComponentName[] componentNames) { + checkPermission(android.Manifest.permission.WRITE_DREAM_STATE); + + final int userId = UserHandle.getCallingUserId(); + final long ident = Binder.clearCallingIdentity(); + try { + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.SCREENSAVER_COMPONENTS, + componentsToString(componentNames), + userId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + public ComponentName getDefaultDreamComponent() { + checkPermission(android.Manifest.permission.READ_DREAM_STATE); + + final int userId = UserHandle.getCallingUserId(); + final long ident = Binder.clearCallingIdentity(); + try { + String name = Settings.Secure.getStringForUser(mContext.getContentResolver(), + Settings.Secure.SCREENSAVER_DEFAULT_COMPONENT, + userId); + return name == null ? null : ComponentName.unflattenFromString(name); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + public boolean isDreaming() { + checkPermission(android.Manifest.permission.READ_DREAM_STATE); + + synchronized (mLock) { + return mCurrentDreamToken != null && !mCurrentDreamIsTest; + } + } + + @Override // Binder call + public void dream() { + checkPermission(android.Manifest.permission.WRITE_DREAM_STATE); + + final long ident = Binder.clearCallingIdentity(); + try { + // Ask the power manager to nap. It will eventually call back into + // startDream() if/when it is appropriate to start dreaming. + // Because napping could cause the screen to turn off immediately if the dream + // cannot be started, we keep one eye open and gently poke user activity. + long time = SystemClock.uptimeMillis(); + mPowerManager.userActivity(time, true /*noChangeLights*/); + mPowerManager.nap(time); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + public void testDream(ComponentName dream) { + checkPermission(android.Manifest.permission.WRITE_DREAM_STATE); + + if (dream == null) { + throw new IllegalArgumentException("dream must not be null"); + } + + final int callingUserId = UserHandle.getCallingUserId(); + final int currentUserId = ActivityManager.getCurrentUser(); + if (callingUserId != currentUserId) { + // This check is inherently prone to races but at least it's something. + Slog.w(TAG, "Aborted attempt to start a test dream while a different " + + " user is active: callingUserId=" + callingUserId + + ", currentUserId=" + currentUserId); + return; + } + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + startDreamLocked(dream, true /*isTest*/, callingUserId); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + public void awaken() { + checkPermission(android.Manifest.permission.WRITE_DREAM_STATE); + + final long ident = Binder.clearCallingIdentity(); + try { + // Treat an explicit request to awaken as user activity so that the + // device doesn't immediately go to sleep if the timeout expired, + // for example when being undocked. + long time = SystemClock.uptimeMillis(); + mPowerManager.userActivity(time, false /*noChangeLights*/); + stopDream(); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + public void finishSelf(IBinder token) { + // Requires no permission, called by Dream from an arbitrary process. + if (token == null) { + throw new IllegalArgumentException("token must not be null"); + } + + final long ident = Binder.clearCallingIdentity(); + try { + if (DEBUG) { + Slog.d(TAG, "Dream finished: " + token); + } + + // Note that a dream finishing and self-terminating is not + // itself considered user activity. If the dream is ending because + // the user interacted with the device then user activity will already + // have been poked so the device will stay awake a bit longer. + // If the dream is ending on its own for other reasons and no wake + // locks are held and the user activity timeout has expired then the + // device may simply go to sleep. + synchronized (mLock) { + if (mCurrentDreamToken == token) { + stopDreamLocked(); + } + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + /** + * Called by the power manager to start a dream. + */ + public void startDream() { + int userId = ActivityManager.getCurrentUser(); + ComponentName dream = chooseDreamForUser(userId); + if (dream != null) { + synchronized (mLock) { + startDreamLocked(dream, false /*isTest*/, userId); + } + } + } + + /** + * Called by the power manager to stop a dream. + */ + public void stopDream() { + synchronized (mLock) { + stopDreamLocked(); + } + } + + private ComponentName chooseDreamForUser(int userId) { + ComponentName[] dreams = getDreamComponentsForUser(userId); + return dreams != null && dreams.length != 0 ? dreams[0] : null; + } + + private ComponentName[] getDreamComponentsForUser(int userId) { + String names = Settings.Secure.getStringForUser(mContext.getContentResolver(), + Settings.Secure.SCREENSAVER_COMPONENTS, + userId); + ComponentName[] components = componentsFromString(names); + + // first, ensure components point to valid services + List<ComponentName> validComponents = new ArrayList<ComponentName>(); + if (components != null) { + for (ComponentName component : components) { + if (serviceExists(component)) { + validComponents.add(component); + } else { + Slog.w(TAG, "Dream " + component + " does not exist"); + } + } + } + + // fallback to the default dream component if necessary + if (validComponents.isEmpty()) { + ComponentName defaultDream = getDefaultDreamComponent(); + if (defaultDream != null) { + Slog.w(TAG, "Falling back to default dream " + defaultDream); + validComponents.add(defaultDream); + } + } + return validComponents.toArray(new ComponentName[validComponents.size()]); + } + + private boolean serviceExists(ComponentName name) { + try { + return name != null && mContext.getPackageManager().getServiceInfo(name, 0) != null; + } catch (NameNotFoundException e) { + return false; + } + } + + private void startDreamLocked(final ComponentName name, + final boolean isTest, final int userId) { + if (Objects.equal(mCurrentDreamName, name) + && mCurrentDreamIsTest == isTest + && mCurrentDreamUserId == userId) { + return; + } + + stopDreamLocked(); + + if (DEBUG) Slog.i(TAG, "Entering dreamland."); + + final Binder newToken = new Binder(); + mCurrentDreamToken = newToken; + mCurrentDreamName = name; + mCurrentDreamIsTest = isTest; + mCurrentDreamUserId = userId; + + mHandler.post(new Runnable() { + @Override + public void run() { + mController.startDream(newToken, name, isTest, userId); + } + }); + } + + private void stopDreamLocked() { + if (mCurrentDreamToken != null) { + if (DEBUG) Slog.i(TAG, "Leaving dreamland."); + + cleanupDreamLocked(); + + mHandler.post(new Runnable() { + @Override + public void run() { + mController.stopDream(); + } + }); + } + } + + private void cleanupDreamLocked() { + mCurrentDreamToken = null; + mCurrentDreamName = null; + mCurrentDreamIsTest = false; + mCurrentDreamUserId = 0; + } + + private void checkPermission(String permission) { + if (mContext.checkCallingOrSelfPermission(permission) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Access denied to process: " + Binder.getCallingPid() + + ", must have permission " + permission); + } + } + + private static String componentsToString(ComponentName[] componentNames) { + StringBuilder names = new StringBuilder(); + if (componentNames != null) { + for (ComponentName componentName : componentNames) { + if (names.length() > 0) { + names.append(','); + } + names.append(componentName.flattenToString()); + } + } + return names.toString(); + } + + private static ComponentName[] componentsFromString(String names) { + if (names == null) { + return null; + } + String[] namesArray = names.split(","); + ComponentName[] componentNames = new ComponentName[namesArray.length]; + for (int i = 0; i < namesArray.length; i++) { + componentNames[i] = ComponentName.unflattenFromString(namesArray[i]); + } + return componentNames; + } + + private final DreamController.Listener mControllerListener = new DreamController.Listener() { + @Override + public void onDreamStopped(Binder token) { + synchronized (mLock) { + if (mCurrentDreamToken == token) { + cleanupDreamLocked(); + } + } + } + }; + + /** + * Handler for asynchronous operations performed by the dream manager. + * Ensures operations to {@link DreamController} are single-threaded. + */ + private final class DreamHandler extends Handler { + public DreamHandler(Looper looper) { + super(looper, null, true /*async*/); + } + } +} |