summaryrefslogtreecommitdiffstats
path: root/services/core/java/com/android/server/dreams
diff options
context:
space:
mode:
authorAmith Yamasani <yamasani@google.com>2013-11-22 08:25:26 -0800
committerAmith Yamasani <yamasani@google.com>2013-12-19 15:25:37 -0800
commit9158825f9c41869689d6b1786d7c7aa8bdd524ce (patch)
treef41944461539f0c70030668b4558296469c307d3 /services/core/java/com/android/server/dreams
parent30d032928a294fbb6f385e9d0367a75b7bf2649b (diff)
downloadframeworks_base-9158825f9c41869689d6b1786d7c7aa8bdd524ce.zip
frameworks_base-9158825f9c41869689d6b1786d7c7aa8bdd524ce.tar.gz
frameworks_base-9158825f9c41869689d6b1786d7c7aa8bdd524ce.tar.bz2
Move some system services to separate directories
Refactored the directory structure so that services can be optionally excluded. This is step 1. Will be followed by another change that makes it possible to remove services from the build. Change-Id: Ideacedfd34b5e213217ad3ff4ebb21c4a8e73f85
Diffstat (limited to 'services/core/java/com/android/server/dreams')
-rw-r--r--services/core/java/com/android/server/dreams/DreamController.java273
-rw-r--r--services/core/java/com/android/server/dreams/DreamManagerService.java425
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*/);
+ }
+ }
+}