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

Commit c07f0a91 authored by Miranda Kephart's avatar Miranda Kephart
Browse files

Move screenshot receivers and add tests

Moves the various broadcast receivers out of GlobalScreenshot and
adds unit tests for them, along with some other changes to make
them more easily testable:
    - ScreenshotSmartActions is now a stateless injectable class,
      instead of a collection of static methods
    - DeleteImageInBackgroundTask removed, in favor of just calling
      a background executor directly
    - remove the TargetChosenReceiver (used to remove notifications
      after a share target is chosen) since we're not using
      notifications anymore

Bug: 160325487
Test: atest SystemUITests, plus manually checked that screenshots
continue to function as expected

Change-Id: I1c054dddd76404f385e59f7ab7317beaafde1106
parent 256cbd1b
Loading
Loading
Loading
Loading
+3 −7
Original line number Diff line number Diff line
@@ -395,19 +395,15 @@

        <!-- Springboard for launching the share and edit activity. This needs to be in the main
             system ui process since we need to notify the status bar to dismiss the keyguard -->
        <receiver android:name=".screenshot.GlobalScreenshot$ActionProxyReceiver"
            android:exported="false" />

        <!-- Callback for dismissing screenshot notification after a share target is picked -->
        <receiver android:name=".screenshot.GlobalScreenshot$TargetChosenReceiver"
        <receiver android:name=".screenshot.ActionProxyReceiver"
            android:exported="false" />

        <!-- Callback for deleting screenshot notification -->
        <receiver android:name=".screenshot.GlobalScreenshot$DeleteScreenshotReceiver"
        <receiver android:name=".screenshot.DeleteScreenshotReceiver"
            android:exported="false" />

        <!-- Callback for invoking a smart action from the screenshot notification. -->
        <receiver android:name=".screenshot.GlobalScreenshot$SmartActionsReceiver"
        <receiver android:name=".screenshot.SmartActionsReceiver"
                  android:exported="false"/>

        <!-- started from UsbDeviceSettingsManager -->
+25 −2
Original line number Diff line number Diff line
@@ -18,7 +18,9 @@ package com.android.systemui.dagger;

import android.content.BroadcastReceiver;

import com.android.systemui.screenshot.GlobalScreenshot.ActionProxyReceiver;
import com.android.systemui.screenshot.ActionProxyReceiver;
import com.android.systemui.screenshot.DeleteScreenshotReceiver;
import com.android.systemui.screenshot.SmartActionsReceiver;

import dagger.Binds;
import dagger.Module;
@@ -30,10 +32,31 @@ import dagger.multibindings.IntoMap;
 */
@Module
public abstract class DefaultBroadcastReceiverBinder {
    /** */
    /**
     *
     */
    @Binds
    @IntoMap
    @ClassKey(ActionProxyReceiver.class)
    public abstract BroadcastReceiver bindActionProxyReceiver(
            ActionProxyReceiver broadcastReceiver);

    /**
     *
     */
    @Binds
    @IntoMap
    @ClassKey(DeleteScreenshotReceiver.class)
    public abstract BroadcastReceiver bindDeleteScreenshotReceiver(
            DeleteScreenshotReceiver broadcastReceiver);

    /**
     *
     */
    @Binds
    @IntoMap
    @ClassKey(SmartActionsReceiver.class)
    public abstract BroadcastReceiver bindSmartActionsReceiver(
            SmartActionsReceiver broadcastReceiver);

}
+105 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.systemui.screenshot;

import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_EDIT;
import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_SHARE;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED;
import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT;

import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.statusbar.phone.StatusBar;

import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.inject.Inject;

/**
 * Receiver to proxy the share or edit intent, used to clean up the notification and send
 * appropriate signals to the system (ie. to dismiss the keyguard if necessary).
 */
public class ActionProxyReceiver extends BroadcastReceiver {
    private static final String TAG = "ActionProxyReceiver";

    private static final int CLOSE_WINDOWS_TIMEOUT_MILLIS = 3000;
    private final StatusBar mStatusBar;
    private final ActivityManagerWrapper mActivityManagerWrapper;
    private final ScreenshotSmartActions mScreenshotSmartActions;

    @Inject
    public ActionProxyReceiver(Optional<StatusBar> statusBar,
            ActivityManagerWrapper activityManagerWrapper,
            ScreenshotSmartActions screenshotSmartActions) {
        mStatusBar = statusBar.orElse(null);
        mActivityManagerWrapper = activityManagerWrapper;
        mScreenshotSmartActions = screenshotSmartActions;
    }

    @Override
    public void onReceive(Context context, final Intent intent) {
        Runnable startActivityRunnable = () -> {
            try {
                mActivityManagerWrapper.closeSystemWindows(
                        SYSTEM_DIALOG_REASON_SCREENSHOT).get(
                        CLOSE_WINDOWS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
            } catch (TimeoutException | InterruptedException | ExecutionException e) {
                Log.e(TAG, "Unable to share screenshot", e);
                return;
            }

            PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
            ActivityOptions opts = ActivityOptions.makeBasic();
            opts.setDisallowEnterPictureInPictureWhileLaunching(
                    intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false));
            try {
                actionIntent.send(context, 0, null, null, null, null, opts.toBundle());
            } catch (PendingIntent.CanceledException e) {
                Log.e(TAG, "Pending intent canceled", e);
            }

        };

        if (mStatusBar != null) {
            mStatusBar.executeRunnableDismissingKeyguard(startActivityRunnable, null,
                    true /* dismissShade */, true /* afterKeyguardGone */,
                    true /* deferred */);
        } else {
            startActivityRunnable.run();
        }

        if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
            String actionType = Intent.ACTION_EDIT.equals(intent.getAction())
                    ? ACTION_TYPE_EDIT
                    : ACTION_TYPE_SHARE;
            mScreenshotSmartActions.notifyScreenshotAction(
                    context, intent.getStringExtra(EXTRA_ID), actionType, false);
        }
    }
}
+68 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 * Copyright (C) 2020 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.
@@ -16,28 +16,53 @@

package com.android.systemui.screenshot;

import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_DELETE;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED;
import static com.android.systemui.screenshot.GlobalScreenshot.SCREENSHOT_URI_ID;

import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;

import com.android.systemui.dagger.qualifiers.Background;

import java.util.concurrent.Executor;

import javax.inject.Inject;

/**
 * An AsyncTask that deletes an image from the media store in the background.
 * Removes the file at a provided URI.
 */
class DeleteImageInBackgroundTask extends AsyncTask<Uri, Void, Void> {
    private Context mContext;
public class DeleteScreenshotReceiver extends BroadcastReceiver {

    DeleteImageInBackgroundTask(Context context) {
        mContext = context;
    private final ScreenshotSmartActions mScreenshotSmartActions;
    private final Executor mBackgroundExecutor;

    @Inject
    public DeleteScreenshotReceiver(ScreenshotSmartActions screenshotSmartActions,
            @Background Executor backgroundExecutor) {
        mScreenshotSmartActions = screenshotSmartActions;
        mBackgroundExecutor = backgroundExecutor;
    }

    @Override
    protected Void doInBackground(Uri... params) {
        if (params.length != 1) return null;
    public void onReceive(Context context, Intent intent) {
        if (!intent.hasExtra(SCREENSHOT_URI_ID)) {
            return;
        }

        Uri screenshotUri = params[0];
        ContentResolver resolver = mContext.getContentResolver();
        resolver.delete(screenshotUri, null, null);
        return null;
        // And delete the image from the media store
        final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID));
        mBackgroundExecutor.execute(() -> {
            ContentResolver resolver = context.getContentResolver();
            resolver.delete(uri, null, null);
        });
        if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
            mScreenshotSmartActions.notifyScreenshotAction(
                    context, intent.getStringExtra(EXTRA_ID), ACTION_TYPE_DELETE, false);
        }
    }
}
+4 −130
Original line number Diff line number Diff line
@@ -21,8 +21,6 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;

import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -30,13 +28,10 @@ import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -63,7 +58,6 @@ import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.MathUtils;
import android.util.Slog;
import android.view.Display;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -88,23 +82,15 @@ import android.widget.Toast;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.phone.StatusBar;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;

import javax.inject.Inject;
import javax.inject.Singleton;

import dagger.Lazy;

/**
 * Class for handling device screen shots
 */
@@ -193,6 +179,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
    private final UiEventLogger mUiEventLogger;

    private final Context mContext;
    private final ScreenshotSmartActions mScreenshotSmartActions;
    private final WindowManager mWindowManager;
    private final WindowManager.LayoutParams mWindowLayoutParams;
    private final Display mDisplay;
@@ -248,9 +235,11 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
    @Inject
    public GlobalScreenshot(
            Context context, @Main Resources resources,
            ScreenshotSmartActions screenshotSmartActions,
            ScreenshotNotificationsController screenshotNotificationsController,
            UiEventLogger uiEventLogger) {
        mContext = context;
        mScreenshotSmartActions = screenshotSmartActions;
        mNotificationsController = screenshotNotificationsController;
        mUiEventLogger = uiEventLogger;

@@ -713,7 +702,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
            });
        }

        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data);
        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data);
        mSaveInBgTask.execute();
    }

@@ -1125,119 +1114,4 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
            return insetDrawable;
        }
    }

    /**
     * Receiver to proxy the share or edit intent, used to clean up the notification and send
     * appropriate signals to the system (ie. to dismiss the keyguard if necessary).
     */
    public static class ActionProxyReceiver extends BroadcastReceiver {
        static final int CLOSE_WINDOWS_TIMEOUT_MILLIS = 3000;
        private final StatusBar mStatusBar;

        @Inject
        public ActionProxyReceiver(Optional<Lazy<StatusBar>> statusBarLazy) {
            Lazy<StatusBar> statusBar = statusBarLazy.orElse(null);
            mStatusBar = statusBar != null ? statusBar.get() : null;
        }

        @Override
        public void onReceive(Context context, final Intent intent) {
            Runnable startActivityRunnable = () -> {
                try {
                    ActivityManagerWrapper.getInstance().closeSystemWindows(
                            SYSTEM_DIALOG_REASON_SCREENSHOT).get(
                            CLOSE_WINDOWS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
                } catch (TimeoutException | InterruptedException | ExecutionException e) {
                    Slog.e(TAG, "Unable to share screenshot", e);
                    return;
                }

                PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
                if (intent.getBooleanExtra(EXTRA_CANCEL_NOTIFICATION, false)) {
                    ScreenshotNotificationsController.cancelScreenshotNotification(context);
                }
                ActivityOptions opts = ActivityOptions.makeBasic();
                opts.setDisallowEnterPictureInPictureWhileLaunching(
                        intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false));
                try {
                    actionIntent.send(context, 0, null, null, null, null, opts.toBundle());
                } catch (PendingIntent.CanceledException e) {
                    Log.e(TAG, "Pending intent canceled", e);
                }

            };

            if (mStatusBar != null) {
                mStatusBar.executeRunnableDismissingKeyguard(startActivityRunnable, null,
                        true /* dismissShade */, true /* afterKeyguardGone */,
                        true /* deferred */);
            } else {
                startActivityRunnable.run();
            }

            if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
                String actionType = Intent.ACTION_EDIT.equals(intent.getAction())
                        ? ACTION_TYPE_EDIT
                        : ACTION_TYPE_SHARE;
                ScreenshotSmartActions.notifyScreenshotAction(
                        context, intent.getStringExtra(EXTRA_ID), actionType, false);
            }
        }
    }

    /**
     * Removes the notification for a screenshot after a share target is chosen.
     */
    public static class TargetChosenReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            // Clear the notification only after the user has chosen a share action
            ScreenshotNotificationsController.cancelScreenshotNotification(context);
        }
    }

    /**
     * Removes the last screenshot.
     */
    public static class DeleteScreenshotReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (!intent.hasExtra(SCREENSHOT_URI_ID)) {
                return;
            }

            // Clear the notification when the image is deleted
            ScreenshotNotificationsController.cancelScreenshotNotification(context);

            // And delete the image from the media store
            final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID));
            new DeleteImageInBackgroundTask(context).execute(uri);
            if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
                ScreenshotSmartActions.notifyScreenshotAction(
                        context, intent.getStringExtra(EXTRA_ID), ACTION_TYPE_DELETE, false);
            }
        }
    }

    /**
     * Executes the smart action tapped by the user in the notification.
     */
    public static class SmartActionsReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
            String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE);
            Slog.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent());
            ActivityOptions opts = ActivityOptions.makeBasic();

            try {
                pendingIntent.send(context, 0, null, null, null, null, opts.toBundle());
            } catch (PendingIntent.CanceledException e) {
                Log.e(TAG, "Pending intent canceled", e);
            }

            ScreenshotSmartActions.notifyScreenshotAction(
                    context, intent.getStringExtra(EXTRA_ID), actionType, true);
        }
    }
}
Loading