blob: 6d70d21e3b84e5bfa108bf6a2edd23e8014e2f14 [file] [log] [blame]
/*
* 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 static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
import android.app.ActivityTaskManager;
import android.app.BroadcastOptions;
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.IBinder.DeathRecipient;
import android.os.IRemoteCallback;
import android.os.PowerManager;
import android.os.RemoteException;
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 com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.UUID;
/**
* 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;
// Extras used with ACTION_CLOSE_SYSTEM_DIALOGS broadcast
private static final String EXTRA_REASON_KEY = "reason";
private static final String EXTRA_REASON_VALUE = "dream";
private final Context mContext;
private final Handler mHandler;
private final Listener mListener;
private final ActivityTaskManager mActivityTaskManager;
private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | FLAG_RECEIVER_FOREGROUND);
private final Intent mDreamingStoppedIntent = new Intent(Intent.ACTION_DREAMING_STOPPED)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | FLAG_RECEIVER_FOREGROUND);
private static final String DREAMING_DELIVERY_GROUP_NAMESPACE = UUID.randomUUID().toString();
private static final String DREAMING_DELIVERY_GROUP_KEY = UUID.randomUUID().toString();
private final Bundle mDreamingStartedStoppedOptions = createDreamingStartedStoppedOptions();
private final Intent mCloseNotificationShadeIntent;
private final Bundle mCloseNotificationShadeOptions;
private DreamRecord mCurrentDream;
// Whether a dreaming started intent has been broadcast.
private boolean mSentStartBroadcast = false;
// When a new dream is started and there is an existing dream, the existing dream is allowed to
// live a little longer until the new dream is started, for a smoother transition. This dream is
// stopped as soon as the new dream is started, and this list is cleared. Usually there should
// only be one previous dream while waiting for a new dream to start, but we store a list to
// proof the edge case of multiple previous dreams.
private final ArrayList<DreamRecord> mPreviousDreams = new ArrayList<>();
public DreamController(Context context, Handler handler, Listener listener) {
mContext = context;
mHandler = handler;
mListener = listener;
mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
mCloseNotificationShadeIntent.putExtra(EXTRA_REASON_KEY, EXTRA_REASON_VALUE);
mCloseNotificationShadeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
mCloseNotificationShadeOptions = BroadcastOptions.makeBasic()
.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
.setDeliveryGroupMatchingKey(Intent.ACTION_CLOSE_SYSTEM_DIALOGS,
EXTRA_REASON_VALUE)
.setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
.toBundle();
}
/**
* Create the {@link BroadcastOptions} bundle that will be used with sending the
* {@link Intent#ACTION_DREAMING_STARTED} and {@link Intent#ACTION_DREAMING_STOPPED}
* broadcasts.
*/
private Bundle createDreamingStartedStoppedOptions() {
final BroadcastOptions options = BroadcastOptions.makeBasic();
// This allows the broadcasting system to discard any older broadcasts
// waiting to be delivered to a process.
options.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
// Set namespace and key to identify which older broadcasts can be discarded.
// We could use any strings here with the following requirements:
// - namespace needs to be unlikely to be reused with in
// the system_server process, as that could result in potentially discarding some
// non-dreaming_started/stopped related broadcast.
// - key needs to be the same for both DREAMING_STARTED and DREAMING_STOPPED broadcasts
// so that dreaming_stopped can also clear any older dreaming_started broadcasts that
// are yet to be delivered.
options.setDeliveryGroupMatchingKey(
DREAMING_DELIVERY_GROUP_NAMESPACE, DREAMING_DELIVERY_GROUP_KEY);
// This allows the broadcast delivery to be delayed to apps in the Cached state.
options.setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE);
return options.toBundle();
}
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(" mIsPreviewMode=" + mCurrentDream.mIsPreviewMode);
pw.println(" mCanDoze=" + mCurrentDream.mCanDoze);
pw.println(" mUserId=" + mCurrentDream.mUserId);
pw.println(" mBound=" + mCurrentDream.mBound);
pw.println(" mService=" + mCurrentDream.mService);
pw.println(" mWakingGently=" + mCurrentDream.mWakingGently);
} else {
pw.println(" mCurrentDream: null");
}
pw.println(" mSentStartBroadcast=" + mSentStartBroadcast);
}
public void startDream(Binder token, ComponentName name,
boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock,
ComponentName overlayComponentName, String reason) {
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,
null /* receiverPermission */, mCloseNotificationShadeOptions);
Slog.i(TAG, "Starting dream: name=" + name
+ ", isPreviewMode=" + isPreviewMode + ", canDoze=" + canDoze
+ ", userId=" + userId + ", reason='" + reason + "'");
final DreamRecord oldDream = mCurrentDream;
mCurrentDream = new DreamRecord(token, name, isPreviewMode, canDoze, userId, wakeLock);
if (oldDream != null) {
if (Objects.equals(oldDream.mName, mCurrentDream.mName)) {
// We are attempting to start a dream that is currently waking up gently.
// Let's silently stop the old instance here to clear the dream state.
// This should happen after the new mCurrentDream is set to avoid announcing
// a "dream stopped" state.
stopDreamInstance(/* immediately */ true, "restarting same dream", oldDream);
} else {
mPreviousDreams.add(oldDream);
}
}
mCurrentDream.mDreamStartTime = SystemClock.elapsedRealtime();
MetricsLogger.visible(mContext,
mCurrentDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
Intent intent = new Intent(DreamService.SERVICE_INTERFACE);
intent.setComponent(name);
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
intent.putExtra(DreamService.EXTRA_DREAM_OVERLAY_COMPONENT, overlayComponentName);
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*/, "bindService failed");
return;
}
} catch (SecurityException ex) {
Slog.e(TAG, "Unable to bind dream service: " + intent, ex);
stopDream(true /*immediate*/, "unable to bind service: SecExp.");
return;
}
mCurrentDream.mBound = true;
mHandler.postDelayed(mCurrentDream.mStopUnconnectedDreamRunnable,
DREAM_CONNECTION_TIMEOUT);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_POWER);
}
}
/**
* Stops dreaming.
*
* The current dream, if any, and any unstopped previous dreams are stopped. The device stops
* dreaming.
*/
public void stopDream(boolean immediate, String reason) {
stopPreviousDreams();
stopDreamInstance(immediate, reason, mCurrentDream);
}
/**
* Stops the given dream instance.
*
* The device may still be dreaming afterwards if there are other dreams running.
*/
private void stopDreamInstance(boolean immediate, String reason, DreamRecord dream) {
if (dream == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_POWER, "stopDream");
try {
if (!immediate) {
if (dream.mWakingGently) {
return; // already waking gently
}
if (dream.mService != null) {
// Give the dream a moment to wake up and finish itself gently.
dream.mWakingGently = true;
try {
dream.mStopReason = reason;
dream.mService.wakeUp();
mHandler.postDelayed(dream.mStopStubbornDreamRunnable,
DREAM_FINISH_TIMEOUT);
return;
} catch (RemoteException ex) {
// oh well, we tried, finish immediately instead
}
}
}
Slog.i(TAG, "Stopping dream: name=" + dream.mName
+ ", isPreviewMode=" + dream.mIsPreviewMode
+ ", canDoze=" + dream.mCanDoze
+ ", userId=" + dream.mUserId
+ ", reason='" + reason + "'"
+ (dream.mStopReason == null ? "" : "(from '"
+ dream.mStopReason + "')"));
MetricsLogger.hidden(mContext,
dream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
MetricsLogger.histogram(mContext,
dream.mCanDoze ? "dozing_minutes" : "dreaming_minutes",
(int) ((SystemClock.elapsedRealtime() - dream.mDreamStartTime) / (1000L
* 60L)));
mHandler.removeCallbacks(dream.mStopUnconnectedDreamRunnable);
mHandler.removeCallbacks(dream.mStopStubbornDreamRunnable);
if (dream.mService != null) {
try {
dream.mService.detach();
} catch (RemoteException ex) {
// we don't care; this thing is on the way out
}
try {
dream.mService.asBinder().unlinkToDeath(dream, 0);
} catch (NoSuchElementException ex) {
// don't care
}
dream.mService = null;
}
if (dream.mBound) {
mContext.unbindService(dream);
}
dream.releaseWakeLockIfNeeded();
// Current dream stopped, device no longer dreaming.
if (dream == mCurrentDream) {
mCurrentDream = null;
if (mSentStartBroadcast) {
mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL,
null /* receiverPermission */, mDreamingStartedStoppedOptions);
mSentStartBroadcast = false;
}
mActivityTaskManager.removeRootTasksWithActivityTypes(
new int[] {ACTIVITY_TYPE_DREAM});
mListener.onDreamStopped(dream.mToken);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_POWER);
}
}
/**
* Stops all previous dreams, if any.
*/
private void stopPreviousDreams() {
if (mPreviousDreams.isEmpty()) {
return;
}
// Using an iterator because mPreviousDreams is modified while the iteration is in process.
for (final Iterator<DreamRecord> it = mPreviousDreams.iterator(); it.hasNext(); ) {
stopDreamInstance(true /*immediate*/, "stop previous dream", it.next());
it.remove();
}
}
private void attach(IDreamService service) {
try {
service.asBinder().linkToDeath(mCurrentDream, 0);
service.attach(mCurrentDream.mToken, mCurrentDream.mCanDoze,
mCurrentDream.mIsPreviewMode, mCurrentDream.mDreamingStartedCallback);
} catch (RemoteException ex) {
Slog.e(TAG, "The dream service died unexpectedly.", ex);
stopDream(true /*immediate*/, "attach failed");
return;
}
mCurrentDream.mService = service;
if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) {
mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL,
null /* receiverPermission */, mDreamingStartedStoppedOptions);
mListener.onDreamStarted(mCurrentDream.mToken);
mSentStartBroadcast = true;
}
}
/**
* Callback interface to be implemented by the {@link DreamManagerService}.
*/
public interface Listener {
void onDreamStarted(Binder token);
void onDreamStopped(Binder token);
}
private final class DreamRecord implements DeathRecipient, ServiceConnection {
public final Binder mToken;
public final ComponentName mName;
public final boolean mIsPreviewMode;
public final boolean mCanDoze;
public final int mUserId;
public PowerManager.WakeLock mWakeLock;
public boolean mBound;
public boolean mConnected;
public IDreamService mService;
private String mStopReason;
private long mDreamStartTime;
public boolean mWakingGently;
private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded;
private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;
private final Runnable mStopUnconnectedDreamRunnable = () -> {
if (mBound && !mConnected) {
Slog.w(TAG, "Bound dream did not connect in the time allotted");
stopDream(true /*immediate*/, "slow to connect" /*reason*/);
}
};
private final Runnable mStopStubbornDreamRunnable = () -> {
Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
stopDream(true /*immediate*/, "slow to finish" /*reason*/);
mStopReason = null;
};
private final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() {
// May be called on any thread.
@Override
public void sendResult(Bundle data) {
mHandler.post(mStopPreviousDreamsIfNeeded);
mHandler.post(mReleaseWakeLockIfNeeded);
}
};
DreamRecord(Binder token, ComponentName name, boolean isPreviewMode,
boolean canDoze, int userId, PowerManager.WakeLock wakeLock) {
mToken = token;
mName = name;
mIsPreviewMode = isPreviewMode;
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.
if (mWakeLock != null) {
mWakeLock.acquire();
}
mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000);
}
// May be called on any thread.
@Override
public void binderDied() {
mHandler.post(() -> {
mService = null;
if (mCurrentDream == DreamRecord.this) {
stopDream(true /*immediate*/, "binder died");
}
});
}
// May be called on any thread.
@Override
public void onServiceConnected(ComponentName name, final IBinder service) {
mHandler.post(() -> {
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(() -> {
mService = null;
if (mCurrentDream == DreamRecord.this) {
stopDream(true /*immediate*/, "service disconnected");
}
});
}
void stopPreviousDreamsIfNeeded() {
if (mCurrentDream == DreamRecord.this) {
stopPreviousDreams();
}
}
void releaseWakeLockIfNeeded() {
if (mWakeLock != null) {
mWakeLock.release();
mWakeLock = null;
mHandler.removeCallbacks(mReleaseWakeLockIfNeeded);
}
}
}
}