/* * 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.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.IRemoteCallback; import android.os.PowerManager; import android.os.RemoteException; import android.os.IBinder.DeathRecipient; import android.os.SystemClock; import android.os.Trace; 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; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_DREAM; /** * 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; // Time to allow the dream to perform an exit transition when waking up. private static final int DREAM_FINISH_TIMEOUT = 5 * 1000; private final Context mContext; private final Handler mHandler; private final Listener mListener; private final IWindowManager mIWindowManager; private long mDreamStartTime; 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; 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(true /*immediate*/); } } }; private final Runnable mStopStubbornDreamRunnable = new Runnable() { @Override public void run() { Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted"); stopDream(true /*immediate*/); } }; public DreamController(Context context, Handler handler, Listener listener) { mContext = context; mHandler = handler; mListener = listener; mIWindowManager = WindowManagerGlobal.getWindowManagerService(); mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); mCloseNotificationShadeIntent.putExtra("reason", "dream"); } 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(" mCanDoze=" + mCurrentDream.mCanDoze); pw.println(" mUserId=" + mCurrentDream.mUserId); pw.println(" mBound=" + mCurrentDream.mBound); pw.println(" mService=" + mCurrentDream.mService); pw.println(" mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast); pw.println(" mWakingGently=" + mCurrentDream.mWakingGently); } else { pw.println(" mCurrentDream: null"); } } public void startDream(Binder token, ComponentName name, boolean isTest, boolean canDoze, int userId, PowerManager.WakeLock wakeLock) { stopDream(true /*immediate*/); Trace.traceBegin(Trace.TRACE_TAG_POWER, "startDream"); try { // Close the notification shade. No need to send to all, but better to be explicit. mContext.sendBroadcastAsUser(mCloseNotificationShadeIntent, UserHandle.ALL); Slog.i(TAG, "Starting dream: name=" + name + ", isTest=" + isTest + ", canDoze=" + canDoze + ", userId=" + userId); mCurrentDream = new DreamRecord(token, name, isTest, canDoze, userId, wakeLock); mDreamStartTime = SystemClock.elapsedRealtime(); MetricsLogger.visible(mContext, mCurrentDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); try { mIWindowManager.addWindowToken(token, TYPE_DREAM, DEFAULT_DISPLAY); } catch (RemoteException ex) { Slog.e(TAG, "Unable to add window token for dream.", ex); stopDream(true /*immediate*/); 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 | Context.BIND_FOREGROUND_SERVICE, new UserHandle(userId))) { Slog.e(TAG, "Unable to bind dream service: " + intent); stopDream(true /*immediate*/); return; } } catch (SecurityException ex) { Slog.e(TAG, "Unable to bind dream service: " + intent, ex); stopDream(true /*immediate*/); return; } mCurrentDream.mBound = true; mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT); } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } } public void stopDream(boolean immediate) { if (mCurrentDream == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_POWER, "stopDream"); try { if (!immediate) { if (mCurrentDream.mWakingGently) { return; // already waking gently } if (mCurrentDream.mService != null) { // Give the dream a moment to wake up and finish itself gently. mCurrentDream.mWakingGently = true; try { mCurrentDream.mService.wakeUp(); mHandler.postDelayed(mStopStubbornDreamRunnable, DREAM_FINISH_TIMEOUT); return; } catch (RemoteException ex) { // oh well, we tried, finish immediately instead } } } final DreamRecord oldDream = mCurrentDream; mCurrentDream = null; Slog.i(TAG, "Stopping dream: name=" + oldDream.mName + ", isTest=" + oldDream.mIsTest + ", canDoze=" + oldDream.mCanDoze + ", userId=" + oldDream.mUserId); MetricsLogger.hidden(mContext, oldDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); MetricsLogger.histogram(mContext, oldDream.mCanDoze ? "dozing_minutes" : "dreaming_minutes" , (int) ((SystemClock.elapsedRealtime() - mDreamStartTime) / (1000L * 60L))); mHandler.removeCallbacks(mStopUnconnectedDreamRunnable); mHandler.removeCallbacks(mStopStubbornDreamRunnable); 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); } oldDream.releaseWakeLockIfNeeded(); try { mIWindowManager.removeWindowToken(oldDream.mToken, DEFAULT_DISPLAY); } 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); } }); } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } } private void attach(IDreamService service) { try { service.asBinder().linkToDeath(mCurrentDream, 0); service.attach(mCurrentDream.mToken, mCurrentDream.mCanDoze, mCurrentDream.mDreamingStartedCallback); } catch (RemoteException ex) { Slog.e(TAG, "The dream service died unexpectedly.", ex); stopDream(true /*immediate*/); 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 boolean mCanDoze; public final int mUserId; public PowerManager.WakeLock mWakeLock; public boolean mBound; public boolean mConnected; public IDreamService mService; public boolean mSentStartBroadcast; public boolean mWakingGently; public DreamRecord(Binder token, ComponentName name, boolean isTest, boolean canDoze, int userId, PowerManager.WakeLock wakeLock) { mToken = token; mName = name; mIsTest = isTest; mCanDoze = canDoze; mUserId = userId; mWakeLock = wakeLock; // Hold the lock while we're waiting for the service to connect and start dreaming. // Released after the service has started dreaming, we stop dreaming, or it timed out. mWakeLock.acquire(); mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000); } // 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(true /*immediate*/); } } }); } // 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)); // Wake lock will be released once dreaming starts. } else { releaseWakeLockIfNeeded(); } } }); } // 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(true /*immediate*/); } } }); } void releaseWakeLockIfNeeded() { if (mWakeLock != null) { mWakeLock.release(); mWakeLock = null; mHandler.removeCallbacks(mReleaseWakeLockIfNeeded); } } final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded; final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() { // May be called on any thread. @Override public void sendResult(Bundle data) throws RemoteException { mHandler.post(mReleaseWakeLockIfNeeded); } }; } }