DreamService.java revision f434d23a16b34e7815049db29bbcd0cd8a4e4862
1/** 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package android.service.dreams; 17 18import java.io.FileDescriptor; 19import java.io.PrintWriter; 20 21import android.annotation.SdkConstant; 22import android.annotation.SdkConstant.SdkConstantType; 23import android.app.Service; 24import android.content.Intent; 25import android.graphics.PixelFormat; 26import android.graphics.drawable.ColorDrawable; 27import android.os.Handler; 28import android.os.IBinder; 29import android.os.ServiceManager; 30import android.util.Slog; 31import android.view.ActionMode; 32import android.view.KeyEvent; 33import android.view.Menu; 34import android.view.MenuItem; 35import android.view.MotionEvent; 36import android.view.View; 37import android.view.ViewGroup; 38import android.view.Window; 39import android.view.WindowManager; 40import android.view.WindowManagerGlobal; 41import android.view.WindowManager.LayoutParams; 42import android.view.accessibility.AccessibilityEvent; 43 44import com.android.internal.policy.PolicyManager; 45 46/** 47 * Extend this class to implement a custom Dream (displayed to the user as a "Sleep Mode"). 48 * 49 * <p>Dreams are interactive screensavers launched when a charging device is idle, or docked in a 50 * desk dock. Dreams provide another modality for apps to express themselves, tailored for 51 * an exhibition/lean-back experience.</p> 52 * 53 * <p>The Dream lifecycle is as follows:</p> 54 * <ul> 55 * <li>onAttachedToWindow</li> 56 * <li>onDreamingStarted</li> 57 * <li>onDreamingStopped</li> 58 * <li>onDetachedFromWindow</li> 59 * </ul> 60 * 61 * <p>In addition, onCreate and onDestroy (from the Service interface) will also be called, but 62 * initialization and teardown should be done by overriding the hooks above.</p> 63 * 64 * <p>To be available to the system, Dreams should be declared in the manifest as follows:</p> 65 * <pre> 66 * <service 67 * android:name=".MyDream" 68 * android:exported="true" 69 * android:icon="@drawable/my_icon" 70 * android:label="@string/my_dream_label" > 71 * 72 * <intent-filter> 73 * <action android:name="android.service.dreams.DreamService" /> 74 * <category android:name="android.intent.category.DEFAULT" /> 75 * </intent-filter> 76 * 77 * <!-- Point to additional information for this dream (optional) --> 78 * <meta-data 79 * android:name="android.service.dream" 80 * android:resource="@xml/my_dream" /> 81 * </service> 82 * </pre> 83 * <p>If specified, additional information for the dream is defined using the 84 * <code><{@link android.R.styleable#Dream dream}></code> element. For example:</p> 85 * <pre> 86 * (in res/xml/my_dream.xml) 87 * 88 * <dream xmlns:android="http://schemas.android.com/apk/res/android" 89 * android:settingsActivity="com.example.app/.MyDreamSettingsActivity" /> 90 * </pre> 91 */ 92public class DreamService extends Service implements Window.Callback { 93 private final String TAG = DreamService.class.getSimpleName() + "[" + getClass().getSimpleName() + "]"; 94 95 /** 96 * The name of the dream manager service. 97 * @hide 98 */ 99 public static final String DREAM_SERVICE = "dreams"; 100 101 /** 102 * The {@link Intent} that must be declared as handled by the service. 103 */ 104 @SdkConstant(SdkConstantType.SERVICE_ACTION) 105 public static final String SERVICE_INTERFACE = 106 "android.service.dreams.DreamService"; 107 108 /** 109 * Name under which a Dream publishes information about itself. 110 * This meta-data must reference an XML resource containing 111 * a <code><{@link android.R.styleable#Dream dream}></code> 112 * tag. 113 */ 114 public static final String DREAM_META_DATA = "android.service.dream"; 115 116 private final Handler mHandler = new Handler(); 117 private IBinder mWindowToken; 118 private Window mWindow; 119 private WindowManager mWindowManager; 120 private IDreamManager mSandman; 121 private boolean mInteractive = false; 122 private boolean mLowProfile = true; 123 private boolean mFullscreen = false; 124 private boolean mScreenBright = true; 125 private boolean mFinished; 126 127 private boolean mDebug = false; 128 129 /** 130 * @hide 131 */ 132 public void setDebug(boolean dbg) { 133 mDebug = dbg; 134 } 135 136 // begin Window.Callback methods 137 /** {@inheritDoc} */ 138 @Override 139 public boolean dispatchKeyEvent(KeyEvent event) { 140 // TODO: create more flexible version of mInteractive that allows use of KEYCODE_BACK 141 if (!mInteractive) { 142 if (mDebug) Slog.v(TAG, "Finishing on keyEvent"); 143 safelyFinish(); 144 return true; 145 } else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 146 if (mDebug) Slog.v(TAG, "Finishing on back key"); 147 safelyFinish(); 148 return true; 149 } 150 return mWindow.superDispatchKeyEvent(event); 151 } 152 153 /** {@inheritDoc} */ 154 @Override 155 public boolean dispatchKeyShortcutEvent(KeyEvent event) { 156 if (!mInteractive) { 157 if (mDebug) Slog.v(TAG, "Finishing on keyShortcutEvent"); 158 safelyFinish(); 159 return true; 160 } 161 return mWindow.superDispatchKeyShortcutEvent(event); 162 } 163 164 /** {@inheritDoc} */ 165 @Override 166 public boolean dispatchTouchEvent(MotionEvent event) { 167 // TODO: create more flexible version of mInteractive that allows clicks 168 // but finish()es on any other kind of activity 169 if (!mInteractive) { 170 if (mDebug) Slog.v(TAG, "Finishing on touchEvent"); 171 safelyFinish(); 172 return true; 173 } 174 return mWindow.superDispatchTouchEvent(event); 175 } 176 177 /** {@inheritDoc} */ 178 @Override 179 public boolean dispatchTrackballEvent(MotionEvent event) { 180 if (!mInteractive) { 181 if (mDebug) Slog.v(TAG, "Finishing on trackballEvent"); 182 safelyFinish(); 183 return true; 184 } 185 return mWindow.superDispatchTrackballEvent(event); 186 } 187 188 /** {@inheritDoc} */ 189 @Override 190 public boolean dispatchGenericMotionEvent(MotionEvent event) { 191 if (!mInteractive) { 192 if (mDebug) Slog.v(TAG, "Finishing on genericMotionEvent"); 193 safelyFinish(); 194 return true; 195 } 196 return mWindow.superDispatchGenericMotionEvent(event); 197 } 198 199 /** {@inheritDoc} */ 200 @Override 201 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 202 return false; 203 } 204 205 /** {@inheritDoc} */ 206 @Override 207 public View onCreatePanelView(int featureId) { 208 return null; 209 } 210 211 /** {@inheritDoc} */ 212 @Override 213 public boolean onCreatePanelMenu(int featureId, Menu menu) { 214 return false; 215 } 216 217 /** {@inheritDoc} */ 218 @Override 219 public boolean onPreparePanel(int featureId, View view, Menu menu) { 220 return false; 221 } 222 223 /** {@inheritDoc} */ 224 @Override 225 public boolean onMenuOpened(int featureId, Menu menu) { 226 return false; 227 } 228 229 /** {@inheritDoc} */ 230 @Override 231 public boolean onMenuItemSelected(int featureId, MenuItem item) { 232 return false; 233 } 234 235 /** {@inheritDoc} */ 236 @Override 237 public void onWindowAttributesChanged(LayoutParams attrs) { 238 } 239 240 /** {@inheritDoc} */ 241 @Override 242 public void onContentChanged() { 243 } 244 245 /** {@inheritDoc} */ 246 @Override 247 public void onWindowFocusChanged(boolean hasFocus) { 248 } 249 250 /** {@inheritDoc} */ 251 @Override 252 public void onAttachedToWindow() { 253 } 254 255 /** {@inheritDoc} */ 256 @Override 257 public void onDetachedFromWindow() { 258 } 259 260 /** {@inheritDoc} */ 261 @Override 262 public void onPanelClosed(int featureId, Menu menu) { 263 } 264 265 /** {@inheritDoc} */ 266 @Override 267 public boolean onSearchRequested() { 268 return false; 269 } 270 271 /** {@inheritDoc} */ 272 @Override 273 public ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback callback) { 274 return null; 275 } 276 277 /** {@inheritDoc} */ 278 @Override 279 public void onActionModeStarted(ActionMode mode) { 280 } 281 282 /** {@inheritDoc} */ 283 @Override 284 public void onActionModeFinished(ActionMode mode) { 285 } 286 // end Window.Callback methods 287 288 // begin public api 289 /** 290 * Retrieves the current {@link android.view.WindowManager} for the dream. 291 * Behaves similarly to {@link android.app.Activity#getWindowManager()}. 292 * 293 * @return The current window manager, or null if the dream is not started. 294 */ 295 public WindowManager getWindowManager() { 296 return mWindowManager; 297 } 298 299 /** 300 * Retrieves the current {@link android.view.Window} for the dream. 301 * Behaves similarly to {@link android.app.Activity#getWindow()}. 302 * 303 * @return The current window, or null if the dream is not started. 304 */ 305 public Window getWindow() { 306 return mWindow; 307 } 308 309 /** 310 * Inflates a layout resource and set it to be the content view for this Dream. 311 * Behaves similarly to {@link android.app.Activity#setContentView(int)}. 312 * 313 * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p> 314 * 315 * @param layoutResID Resource ID to be inflated. 316 * 317 * @see #setContentView(android.view.View) 318 * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) 319 */ 320 public void setContentView(int layoutResID) { 321 getWindow().setContentView(layoutResID); 322 } 323 324 /** 325 * Sets a view to be the content view for this Dream. 326 * Behaves similarly to {@link android.app.Activity#setContentView(android.view.View)}, 327 * including using {@link ViewGroup.LayoutParams#MATCH_PARENT} as the layout height and width of the view. 328 * 329 * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p> 330 * @param view The desired content to display. 331 * 332 * @see #setContentView(int) 333 * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) 334 */ 335 public void setContentView(View view) { 336 getWindow().setContentView(view); 337 } 338 339 /** 340 * Sets a view to be the content view for this Dream. 341 * Behaves similarly to 342 * {@link android.app.Activity#setContentView(android.view.View, android.view.ViewGroup.LayoutParams)}. 343 * 344 * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p> 345 * 346 * @param view The desired content to display. 347 * @param params Layout parameters for the view. 348 * 349 * @see #setContentView(android.view.View) 350 * @see #setContentView(int) 351 */ 352 public void setContentView(View view, ViewGroup.LayoutParams params) { 353 getWindow().setContentView(view, params); 354 } 355 356 /** 357 * Adds a view to the Dream's window, leaving other content views in place. 358 * 359 * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p> 360 * 361 * @param view The desired content to display. 362 * @param params Layout parameters for the view. 363 */ 364 public void addContentView(View view, ViewGroup.LayoutParams params) { 365 getWindow().addContentView(view, params); 366 } 367 368 /** 369 * Finds a view that was identified by the id attribute from the XML that 370 * was processed in {@link #onCreate}. 371 * 372 * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p> 373 * 374 * @return The view if found or null otherwise. 375 */ 376 public View findViewById(int id) { 377 return getWindow().findViewById(id); 378 } 379 380 /** 381 * Marks this dream as interactive to receive input events. 382 * 383 * <p>Non-interactive dreams (default) will dismiss on the first input event.</p> 384 * 385 * <p>Interactive dreams should call {@link #finish()} to dismiss themselves.</p> 386 * 387 * @param interactive True if this dream will handle input events. 388 */ 389 public void setInteractive(boolean interactive) { 390 mInteractive = interactive; 391 } 392 393 /** 394 * Returns whether or not this dream is interactive. Defaults to false. 395 * 396 * @see #setInteractive(boolean) 397 */ 398 public boolean isInteractive() { 399 return mInteractive; 400 } 401 402 /** 403 * Sets View.SYSTEM_UI_FLAG_LOW_PROFILE on the content view. 404 * 405 * @param lowProfile True to set View.SYSTEM_UI_FLAG_LOW_PROFILE 406 * @hide There is no reason to have this -- dreams can set this flag 407 * on their own content view, and from there can actually do the 408 * correct interactions with it (seeing when it is cleared etc). 409 */ 410 public void setLowProfile(boolean lowProfile) { 411 mLowProfile = lowProfile; 412 int flag = View.SYSTEM_UI_FLAG_LOW_PROFILE; 413 applySystemUiVisibilityFlags(mLowProfile ? flag : 0, flag); 414 } 415 416 /** 417 * Returns whether or not this dream is in low profile mode. Defaults to true. 418 * 419 * @see #setLowProfile(boolean) 420 * @hide 421 */ 422 public boolean isLowProfile() { 423 return getSystemUiVisibilityFlagValue(View.SYSTEM_UI_FLAG_LOW_PROFILE, mLowProfile); 424 } 425 426 /** 427 * Controls {@link android.view.WindowManager.LayoutParams#FLAG_FULLSCREEN} 428 * on the dream's window. 429 * 430 * @param fullscreen If true, the fullscreen flag will be set; else it 431 * will be cleared. 432 */ 433 public void setFullscreen(boolean fullscreen) { 434 mFullscreen = fullscreen; 435 int flag = WindowManager.LayoutParams.FLAG_FULLSCREEN; 436 applyWindowFlags(mFullscreen ? flag : 0, flag); 437 } 438 439 /** 440 * Returns whether or not this dream is in fullscreen mode. Defaults to false. 441 * 442 * @see #setFullscreen(boolean) 443 */ 444 public boolean isFullscreen() { 445 return mFullscreen; 446 } 447 448 /** 449 * Marks this dream as keeping the screen bright while dreaming. 450 * 451 * @param screenBright True to keep the screen bright while dreaming. 452 */ 453 public void setScreenBright(boolean screenBright) { 454 mScreenBright = screenBright; 455 int flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; 456 applyWindowFlags(mScreenBright ? flag : 0, flag); 457 } 458 459 /** 460 * Returns whether or not this dream keeps the screen bright while dreaming. Defaults to false, 461 * allowing the screen to dim if necessary. 462 * 463 * @see #setScreenBright(boolean) 464 */ 465 public boolean isScreenBright() { 466 return getWindowFlagValue(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, mScreenBright); 467 } 468 469 /** 470 * Called when this Dream is constructed. 471 */ 472 @Override 473 public void onCreate() { 474 if (mDebug) Slog.v(TAG, "onCreate() on thread " + Thread.currentThread().getId()); 475 super.onCreate(); 476 } 477 478 /** 479 * Called when the dream's window has been created and is visible and animation may now begin. 480 */ 481 public void onDreamingStarted() { 482 if (mDebug) Slog.v(TAG, "onDreamingStarted()"); 483 // hook for subclasses 484 } 485 486 /** 487 * Called when this Dream is stopped, either by external request or by calling finish(), 488 * before the window has been removed. 489 */ 490 public void onDreamingStopped() { 491 if (mDebug) Slog.v(TAG, "onDreamingStopped()"); 492 // hook for subclasses 493 } 494 495 /** {@inheritDoc} */ 496 @Override 497 public final IBinder onBind(Intent intent) { 498 if (mDebug) Slog.v(TAG, "onBind() intent = " + intent); 499 return new DreamServiceWrapper(); 500 } 501 502 /** 503 * Stops the dream, detaches from the window, and wakes up. 504 */ 505 public final void finish() { 506 if (mDebug) Slog.v(TAG, "finish()"); 507 finishInternal(); 508 } 509 510 /** {@inheritDoc} */ 511 @Override 512 public void onDestroy() { 513 if (mDebug) Slog.v(TAG, "onDestroy()"); 514 // hook for subclasses 515 516 // Just in case destroy came in before detach, let's take care of that now 517 detach(); 518 519 super.onDestroy(); 520 } 521 522 // end public api 523 524 private void loadSandman() { 525 mSandman = IDreamManager.Stub.asInterface(ServiceManager.getService(DREAM_SERVICE)); 526 } 527 528 /** 529 * Called by DreamController.stopDream() when the Dream is about to be unbound and destroyed. 530 * 531 * Must run on mHandler. 532 */ 533 private final void detach() { 534 if (mWindow == null) { 535 // already detached! 536 return; 537 } 538 539 try { 540 onDreamingStopped(); 541 } catch (Throwable t) { 542 Slog.w(TAG, "Crashed in onDreamingStopped()", t); 543 // we were going to stop anyway 544 } 545 546 if (mDebug) Slog.v(TAG, "detach(): Removing window from window manager"); 547 try { 548 // force our window to be removed synchronously 549 mWindowManager.removeViewImmediate(mWindow.getDecorView()); 550 // the following will print a log message if it finds any other leaked windows 551 WindowManagerGlobal.getInstance().closeAll(mWindowToken, 552 this.getClass().getName(), "Dream"); 553 } catch (Throwable t) { 554 Slog.w(TAG, "Crashed removing window view", t); 555 } 556 557 mWindow = null; 558 mWindowToken = null; 559 } 560 561 /** 562 * Called when the Dream is ready to be shown. 563 * 564 * Must run on mHandler. 565 * 566 * @param windowToken A window token that will allow a window to be created in the correct layer. 567 */ 568 private final void attach(IBinder windowToken) { 569 if (mWindowToken != null) { 570 Slog.e(TAG, "attach() called when already attached with token=" + mWindowToken); 571 return; 572 } 573 574 if (mDebug) Slog.v(TAG, "Attached on thread " + Thread.currentThread().getId()); 575 576 if (mSandman == null) { 577 loadSandman(); 578 } 579 mWindowToken = windowToken; 580 mWindow = PolicyManager.makeNewWindow(this); 581 mWindow.setCallback(this); 582 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 583 mWindow.setBackgroundDrawable(new ColorDrawable(0xFF000000)); 584 mWindow.setFormat(PixelFormat.OPAQUE); 585 586 if (mDebug) Slog.v(TAG, String.format("Attaching window token: %s to window of type %s", 587 windowToken, WindowManager.LayoutParams.TYPE_DREAM)); 588 589 WindowManager.LayoutParams lp = mWindow.getAttributes(); 590 lp.type = WindowManager.LayoutParams.TYPE_DREAM; 591 lp.token = windowToken; 592 lp.windowAnimations = com.android.internal.R.style.Animation_Dream; 593 lp.flags |= ( WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 594 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR 595 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 596 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD 597 | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON 598 | (mFullscreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : 0) 599 | (mScreenBright ? WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON : 0) 600 ); 601 mWindow.setAttributes(lp); 602 603 if (mDebug) Slog.v(TAG, "Created and attached window: " + mWindow); 604 605 mWindow.setWindowManager(null, windowToken, "dream", true); 606 mWindowManager = mWindow.getWindowManager(); 607 608 if (mDebug) Slog.v(TAG, "Window added on thread " + Thread.currentThread().getId()); 609 try { 610 applySystemUiVisibilityFlags( 611 (mLowProfile ? View.SYSTEM_UI_FLAG_LOW_PROFILE : 0), 612 View.SYSTEM_UI_FLAG_LOW_PROFILE); 613 getWindowManager().addView(mWindow.getDecorView(), mWindow.getAttributes()); 614 } catch (Throwable t) { 615 Slog.w(TAG, "Crashed adding window view", t); 616 safelyFinish(); 617 return; 618 } 619 620 // start it up 621 mHandler.post(new Runnable() { 622 @Override 623 public void run() { 624 try { 625 onDreamingStarted(); 626 } catch (Throwable t) { 627 Slog.w(TAG, "Crashed in onDreamingStarted()", t); 628 safelyFinish(); 629 } 630 } 631 }); 632 } 633 634 private void safelyFinish() { 635 if (mDebug) Slog.v(TAG, "safelyFinish()"); 636 try { 637 finish(); 638 } catch (Throwable t) { 639 Slog.w(TAG, "Crashed in safelyFinish()", t); 640 finishInternal(); 641 return; 642 } 643 644 if (!mFinished) { 645 Slog.w(TAG, "Bad dream, did not call super.finish()"); 646 finishInternal(); 647 } 648 } 649 650 private void finishInternal() { 651 if (mDebug) Slog.v(TAG, "finishInternal() mFinished = " + mFinished); 652 if (mFinished) return; 653 try { 654 mFinished = true; 655 656 if (mSandman != null) { 657 mSandman.finishSelf(mWindowToken); 658 } else { 659 Slog.w(TAG, "No dream manager found"); 660 } 661 stopSelf(); // if launched via any other means 662 663 } catch (Throwable t) { 664 Slog.w(TAG, "Crashed in finishInternal()", t); 665 } 666 } 667 668 private boolean getWindowFlagValue(int flag, boolean defaultValue) { 669 return mWindow == null ? defaultValue : (mWindow.getAttributes().flags & flag) != 0; 670 } 671 672 private void applyWindowFlags(int flags, int mask) { 673 if (mWindow != null) { 674 WindowManager.LayoutParams lp = mWindow.getAttributes(); 675 lp.flags = applyFlags(lp.flags, flags, mask); 676 mWindow.setAttributes(lp); 677 mWindowManager.updateViewLayout(mWindow.getDecorView(), lp); 678 } 679 } 680 681 private boolean getSystemUiVisibilityFlagValue(int flag, boolean defaultValue) { 682 View v = mWindow == null ? null : mWindow.getDecorView(); 683 return v == null ? defaultValue : (v.getSystemUiVisibility() & flag) != 0; 684 } 685 686 private void applySystemUiVisibilityFlags(int flags, int mask) { 687 View v = mWindow == null ? null : mWindow.getDecorView(); 688 if (v != null) { 689 v.setSystemUiVisibility(applyFlags(v.getSystemUiVisibility(), flags, mask)); 690 } 691 } 692 693 private int applyFlags(int oldFlags, int flags, int mask) { 694 return (oldFlags&~mask) | (flags&mask); 695 } 696 697 @Override 698 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 699 super.dump(fd, pw, args); 700 701 pw.print(TAG + ": "); 702 if (mWindowToken == null) { 703 pw.println("stopped"); 704 } else { 705 pw.println("running (token=" + mWindowToken + ")"); 706 } 707 pw.println(" window: " + mWindow); 708 pw.print(" flags:"); 709 if (isInteractive()) pw.print(" interactive"); 710 if (isLowProfile()) pw.print(" lowprofile"); 711 if (isFullscreen()) pw.print(" fullscreen"); 712 if (isScreenBright()) pw.print(" bright"); 713 pw.println(); 714 } 715 716 private class DreamServiceWrapper extends IDreamService.Stub { 717 public void attach(final IBinder windowToken) { 718 mHandler.post(new Runnable() { 719 @Override 720 public void run() { 721 DreamService.this.attach(windowToken); 722 } 723 }); 724 } 725 public void detach() { 726 mHandler.post(new Runnable() { 727 @Override 728 public void run() { 729 DreamService.this.detach(); 730 } 731 }); 732 } 733 } 734 735} 736