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 */
16
17package com.android.server.dreams;
18
19import com.android.internal.logging.MetricsLogger;
20
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.ServiceConnection;
25import android.os.Binder;
26import android.os.Handler;
27import android.os.IBinder;
28import android.os.RemoteException;
29import android.os.IBinder.DeathRecipient;
30import android.os.SystemClock;
31import android.os.Trace;
32import android.os.UserHandle;
33import android.service.dreams.DreamService;
34import android.service.dreams.IDreamService;
35import android.util.Slog;
36import android.view.IWindowManager;
37import android.view.WindowManager;
38import android.view.WindowManagerGlobal;
39
40import java.io.PrintWriter;
41import java.util.NoSuchElementException;
42
43/**
44 * Internal controller for starting and stopping the current dream and managing related state.
45 *
46 * Assumes all operations are called from the dream handler thread.
47 */
48final class DreamController {
49    private static final String TAG = "DreamController";
50
51    // How long we wait for a newly bound dream to create the service connection
52    private static final int DREAM_CONNECTION_TIMEOUT = 5 * 1000;
53
54    // Time to allow the dream to perform an exit transition when waking up.
55    private static final int DREAM_FINISH_TIMEOUT = 5 * 1000;
56
57    private final Context mContext;
58    private final Handler mHandler;
59    private final Listener mListener;
60    private final IWindowManager mIWindowManager;
61    private long mDreamStartTime;
62
63    private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED)
64            .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
65    private final Intent mDreamingStoppedIntent = new Intent(Intent.ACTION_DREAMING_STOPPED)
66            .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
67
68    private final Intent mCloseNotificationShadeIntent;
69
70    private DreamRecord mCurrentDream;
71
72    private final Runnable mStopUnconnectedDreamRunnable = new Runnable() {
73        @Override
74        public void run() {
75            if (mCurrentDream != null && mCurrentDream.mBound && !mCurrentDream.mConnected) {
76                Slog.w(TAG, "Bound dream did not connect in the time allotted");
77                stopDream(true /*immediate*/);
78            }
79        }
80    };
81
82    private final Runnable mStopStubbornDreamRunnable = new Runnable() {
83        @Override
84        public void run() {
85            Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
86            stopDream(true /*immediate*/);
87        }
88    };
89
90    public DreamController(Context context, Handler handler, Listener listener) {
91        mContext = context;
92        mHandler = handler;
93        mListener = listener;
94        mIWindowManager = WindowManagerGlobal.getWindowManagerService();
95        mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
96        mCloseNotificationShadeIntent.putExtra("reason", "dream");
97    }
98
99    public void dump(PrintWriter pw) {
100        pw.println("Dreamland:");
101        if (mCurrentDream != null) {
102            pw.println("  mCurrentDream:");
103            pw.println("    mToken=" + mCurrentDream.mToken);
104            pw.println("    mName=" + mCurrentDream.mName);
105            pw.println("    mIsTest=" + mCurrentDream.mIsTest);
106            pw.println("    mCanDoze=" + mCurrentDream.mCanDoze);
107            pw.println("    mUserId=" + mCurrentDream.mUserId);
108            pw.println("    mBound=" + mCurrentDream.mBound);
109            pw.println("    mService=" + mCurrentDream.mService);
110            pw.println("    mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast);
111            pw.println("    mWakingGently=" + mCurrentDream.mWakingGently);
112        } else {
113            pw.println("  mCurrentDream: null");
114        }
115    }
116
117    public void startDream(Binder token, ComponentName name,
118            boolean isTest, boolean canDoze, int userId) {
119        stopDream(true /*immediate*/);
120
121        Trace.traceBegin(Trace.TRACE_TAG_POWER, "startDream");
122        try {
123            // Close the notification shade. Don't need to send to all, but better to be explicit.
124            mContext.sendBroadcastAsUser(mCloseNotificationShadeIntent, UserHandle.ALL);
125
126            Slog.i(TAG, "Starting dream: name=" + name
127                    + ", isTest=" + isTest + ", canDoze=" + canDoze
128                    + ", userId=" + userId);
129
130            mCurrentDream = new DreamRecord(token, name, isTest, canDoze, userId);
131
132            mDreamStartTime = SystemClock.elapsedRealtime();
133            MetricsLogger.visible(mContext,
134                    mCurrentDream.mCanDoze ? MetricsLogger.DOZING : MetricsLogger.DREAMING);
135
136            try {
137                mIWindowManager.addWindowToken(token, WindowManager.LayoutParams.TYPE_DREAM);
138            } catch (RemoteException ex) {
139                Slog.e(TAG, "Unable to add window token for dream.", ex);
140                stopDream(true /*immediate*/);
141                return;
142            }
143
144            Intent intent = new Intent(DreamService.SERVICE_INTERFACE);
145            intent.setComponent(name);
146            intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
147            try {
148                if (!mContext.bindServiceAsUser(intent, mCurrentDream,
149                        Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
150                        new UserHandle(userId))) {
151                    Slog.e(TAG, "Unable to bind dream service: " + intent);
152                    stopDream(true /*immediate*/);
153                    return;
154                }
155            } catch (SecurityException ex) {
156                Slog.e(TAG, "Unable to bind dream service: " + intent, ex);
157                stopDream(true /*immediate*/);
158                return;
159            }
160
161            mCurrentDream.mBound = true;
162            mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT);
163        } finally {
164            Trace.traceEnd(Trace.TRACE_TAG_POWER);
165        }
166    }
167
168    public void stopDream(boolean immediate) {
169        if (mCurrentDream == null) {
170            return;
171        }
172
173        Trace.traceBegin(Trace.TRACE_TAG_POWER, "stopDream");
174        try {
175            if (!immediate) {
176                if (mCurrentDream.mWakingGently) {
177                    return; // already waking gently
178                }
179
180                if (mCurrentDream.mService != null) {
181                    // Give the dream a moment to wake up and finish itself gently.
182                    mCurrentDream.mWakingGently = true;
183                    try {
184                        mCurrentDream.mService.wakeUp();
185                        mHandler.postDelayed(mStopStubbornDreamRunnable, DREAM_FINISH_TIMEOUT);
186                        return;
187                    } catch (RemoteException ex) {
188                        // oh well, we tried, finish immediately instead
189                    }
190                }
191            }
192
193            final DreamRecord oldDream = mCurrentDream;
194            mCurrentDream = null;
195            Slog.i(TAG, "Stopping dream: name=" + oldDream.mName
196                    + ", isTest=" + oldDream.mIsTest + ", canDoze=" + oldDream.mCanDoze
197                    + ", userId=" + oldDream.mUserId);
198            MetricsLogger.hidden(mContext,
199                    oldDream.mCanDoze ? MetricsLogger.DOZING : MetricsLogger.DREAMING);
200            MetricsLogger.histogram(mContext,
201                    oldDream.mCanDoze ? "dozing_minutes" : "dreaming_minutes" ,
202                    (int) ((SystemClock.elapsedRealtime() - mDreamStartTime) / (1000L * 60L)));
203
204            mHandler.removeCallbacks(mStopUnconnectedDreamRunnable);
205            mHandler.removeCallbacks(mStopStubbornDreamRunnable);
206
207            if (oldDream.mSentStartBroadcast) {
208                mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
209            }
210
211            if (oldDream.mService != null) {
212                // Tell the dream that it's being stopped so that
213                // it can shut down nicely before we yank its window token out from
214                // under it.
215                try {
216                    oldDream.mService.detach();
217                } catch (RemoteException ex) {
218                    // we don't care; this thing is on the way out
219                }
220
221                try {
222                    oldDream.mService.asBinder().unlinkToDeath(oldDream, 0);
223                } catch (NoSuchElementException ex) {
224                    // don't care
225                }
226                oldDream.mService = null;
227            }
228
229            if (oldDream.mBound) {
230                mContext.unbindService(oldDream);
231            }
232
233            try {
234                mIWindowManager.removeWindowToken(oldDream.mToken);
235            } catch (RemoteException ex) {
236                Slog.w(TAG, "Error removing window token for dream.", ex);
237            }
238
239            mHandler.post(new Runnable() {
240                @Override
241                public void run() {
242                    mListener.onDreamStopped(oldDream.mToken);
243                }
244            });
245        } finally {
246            Trace.traceEnd(Trace.TRACE_TAG_POWER);
247        }
248    }
249
250    private void attach(IDreamService service) {
251        try {
252            service.asBinder().linkToDeath(mCurrentDream, 0);
253            service.attach(mCurrentDream.mToken, mCurrentDream.mCanDoze);
254        } catch (RemoteException ex) {
255            Slog.e(TAG, "The dream service died unexpectedly.", ex);
256            stopDream(true /*immediate*/);
257            return;
258        }
259
260        mCurrentDream.mService = service;
261
262        if (!mCurrentDream.mIsTest) {
263            mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL);
264            mCurrentDream.mSentStartBroadcast = true;
265        }
266    }
267
268    /**
269     * Callback interface to be implemented by the {@link DreamManagerService}.
270     */
271    public interface Listener {
272        void onDreamStopped(Binder token);
273    }
274
275    private final class DreamRecord implements DeathRecipient, ServiceConnection {
276        public final Binder mToken;
277        public final ComponentName mName;
278        public final boolean mIsTest;
279        public final boolean mCanDoze;
280        public final int mUserId;
281
282        public boolean mBound;
283        public boolean mConnected;
284        public IDreamService mService;
285        public boolean mSentStartBroadcast;
286
287        public boolean mWakingGently;
288
289        public DreamRecord(Binder token, ComponentName name,
290                boolean isTest, boolean canDoze, int userId) {
291            mToken = token;
292            mName = name;
293            mIsTest = isTest;
294            mCanDoze = canDoze;
295            mUserId  = userId;
296        }
297
298        // May be called on any thread.
299        @Override
300        public void binderDied() {
301            mHandler.post(new Runnable() {
302                @Override
303                public void run() {
304                    mService = null;
305                    if (mCurrentDream == DreamRecord.this) {
306                        stopDream(true /*immediate*/);
307                    }
308                }
309            });
310        }
311
312        // May be called on any thread.
313        @Override
314        public void onServiceConnected(ComponentName name, final IBinder service) {
315            mHandler.post(new Runnable() {
316                @Override
317                public void run() {
318                    mConnected = true;
319                    if (mCurrentDream == DreamRecord.this && mService == null) {
320                        attach(IDreamService.Stub.asInterface(service));
321                    }
322                }
323            });
324        }
325
326        // May be called on any thread.
327        @Override
328        public void onServiceDisconnected(ComponentName name) {
329            mHandler.post(new Runnable() {
330                @Override
331                public void run() {
332                    mService = null;
333                    if (mCurrentDream == DreamRecord.this) {
334                        stopDream(true /*immediate*/);
335                    }
336                }
337            });
338        }
339    }
340}