Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 308f1aa8 authored by Darrell Shi's avatar Darrell Shi
Browse files

Fail duplicated attachment to dream service

Previously if the same dream service is attached a second time, it fails
silently, leading the DreamController to lose track of the previous
dream, and not stopping it properly. This change makes the service
communicate attachment failure back to the DreamController so that it
can manage its state properly.

Test: atest DreamControllerTest
Test: verified the failing CUJ listed in the bug, and dream stops when
      attached a second time, intead of getting stuck
Bug: 434674824
Flag: android.service.dreams.allow_dream_attach_failure
Change-Id: I47789eb618cffc7bb229a47974e450dfc3035b1a
parent a4c06d8a
Loading
Loading
Loading
Loading
+18 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package android.service.dreams;

import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.service.dreams.Flags.allowDreamAttachFailure;
import static android.service.dreams.Flags.dreamHandlesBeingObscured;
import static android.service.dreams.Flags.dreamHandlesConfirmKeys;
import static android.service.dreams.Flags.startAndStopDozingInBackground;
@@ -45,6 +46,7 @@ import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.IRemoteCallback;
@@ -250,6 +252,13 @@ public class DreamService extends Service implements Window.Callback {
    static final String EXTRA_DREAM_OVERLAY_COMPONENT =
            "android.service.dream.DreamService.dream_overlay_component";

    /**
     * The name of the extra that indicates an error during attach.
     * @hide
     */
    public static final String BUNDLE_KEY_ATTACH_ERROR =
            "android.service.dream.DreamService.attach_error";

    private final IDreamManager mDreamManager;
    private final Handler mHandler;
    private IBinder mDreamToken;
@@ -1571,6 +1580,15 @@ public class DreamService extends Service implements Window.Callback {
        if (mDreamToken != null) {
            Slog.e(mTag, "attach() called when dream with token=" + mDreamToken
                    + " already attached");
            if (allowDreamAttachFailure()) {
                try {
                    final Bundle result = new Bundle();
                    result.putBoolean(BUNDLE_KEY_ATTACH_ERROR, true);
                    started.sendResult(result);
                } catch (RemoteException e) {
                    // The dream controller is dead, so there is nothing to do.
                }
            }
            return;
        }
        if (mFinished || mWaking) {
+11 −1
Original line number Diff line number Diff line
@@ -130,3 +130,13 @@ flag {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "allow_dream_attach_failure"
    namespace: "systemui"
    description: "Fails starting dream when attaching to a service that is already attached"
    bug: "434674824"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}
+30 −1
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.server.dreams;
import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
import static android.os.PowerManager.USER_ACTIVITY_EVENT_OTHER;
import static android.os.PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS;
import static android.service.dreams.Flags.allowDreamAttachFailure;

import android.app.ActivityTaskManager;
import android.app.BroadcastOptions;
@@ -410,6 +411,9 @@ final class DreamController {
    private void attach(IDreamService service) {
        try {
            service.asBinder().linkToDeath(mCurrentDream, 0);
            if (allowDreamAttachFailure()) {
                mCurrentDream.mService = service;
            }
            service.attach(mCurrentDream.mToken, mCurrentDream.mCanDoze,
                    mCurrentDream.mIsPreviewMode, mCurrentDream.mDreamingStartedCallback);
        } catch (RemoteException ex) {
@@ -418,8 +422,13 @@ final class DreamController {
            return;
        }

        if (!allowDreamAttachFailure()) {
            mCurrentDream.mService = service;
            onDreamStarted();
        }
    }

    private void onDreamStarted() {
        if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) {
            mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL,
                    null /* receiverPermission */, mDreamingStartedStoppedOptions);
@@ -475,6 +484,26 @@ final class DreamController {
            public void sendResult(Bundle data) {
                mHandler.post(mStopPreviousDreamsIfNeeded);
                mHandler.post(mReleaseWakeLockIfNeeded);

                if (!allowDreamAttachFailure()) {
                    return;
                }

                mHandler.post(() -> {
                    // If the dream has been stopped already, don't do anything.
                    if (mCurrentDream != DreamRecord.this) {
                        return;
                    }

                    if (data != null
                            && data.getBoolean(DreamService.BUNDLE_KEY_ATTACH_ERROR)) {
                        Slog.w(TAG, "Dream failed to start due to attach error");
                        stopDream(true, "dream failed to attach");
                        return;
                    }

                    onDreamStarted();
                });
            }
        };

+63 −1
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
@@ -37,6 +38,7 @@ import android.content.Context;
import android.content.ServiceConnection;
import android.content.res.Resources;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.IPowerManager;
@@ -44,12 +46,18 @@ import android.os.IRemoteCallback;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.dreams.DreamService;
import android.service.dreams.Flags;
import android.service.dreams.IDreamService;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -85,6 +93,9 @@ public class DreamControllerTest {
    @Captor
    private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor;

    @Rule
    public SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    private final TestLooper mLooper = new TestLooper();
    private final Handler mHandler = new Handler(mLooper.getLooper());

@@ -133,8 +144,31 @@ public class DreamControllerTest {
                eq(false) /*preview*/, any());
    }

    @EnableFlags(Flags.FLAG_ALLOW_DREAM_ATTACH_FAILURE)
    @Test
    public void startDream_flagEnabled_dreamListenerNotified() throws RemoteException {
        // Call dream controller to start dreaming.
        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);

        // Mock service connected.
        final ServiceConnection serviceConnection = captureServiceConnection();
        serviceConnection.onServiceConnected(mDreamName, mIBinder);
        mLooper.dispatchAll();

        // Mock dream started callback.
        verify(mIDreamService).attach(eq(mToken), eq(false), eq(false),
                mRemoteCallbackCaptor.capture());
        mRemoteCallbackCaptor.getValue().sendResult(null);
        mLooper.dispatchAll();

        // Verify that dream listener is notified.
        verify(mListener).onDreamStarted(any());
    }

    @DisableFlags(Flags.FLAG_ALLOW_DREAM_ATTACH_FAILURE)
    @Test
    public void startDream_dreamListenerNotified() {
    public void startDream_flagDisabled_dreamListenerNotified() {
        // Call dream controller to start dreaming.
        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
@@ -312,6 +346,34 @@ public class DreamControllerTest {
        assertTrue(mDreamController.dreamIsFrontmost());
    }

    @Test
    @EnableFlags(Flags.FLAG_ALLOW_DREAM_ATTACH_FAILURE)
    public void startDream_attachReturnsError_stopsDream() throws RemoteException {
        // Call dream controller to start dreaming.
        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);

        // Mock service connected.
        final ServiceConnection serviceConnection = captureServiceConnection();
        serviceConnection.onServiceConnected(mDreamName, mIBinder);
        mLooper.dispatchAll();

        // Verify that dream service is called to attach.
        verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/,
                eq(false) /*preview*/, mRemoteCallbackCaptor.capture());

        // Mock attach returning an error.
        final Bundle bundle = new Bundle();
        bundle.putBoolean(DreamService.BUNDLE_KEY_ATTACH_ERROR, true);
        mRemoteCallbackCaptor.getValue().sendResult(bundle);
        mLooper.dispatchAll();

        // Verify that the dream is stopped.
        verify(mIDreamService).detach();
        verify(mListener).onDreamStopped(any());
        verify(mListener, never()).onDreamStarted(any());
    }

    private ServiceConnection captureServiceConnection() {
        verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(),
                any());