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