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

Commit 0606a734 authored by Matt Casey's avatar Matt Casey
Browse files

Move long screenshot UI to an Activity

Separate UI code from image acquisition.
Using a static variable to get scroll capture connection from the
Activity, can do something more elegant later.
Doesn't do full activity restoration yet.

Bug: 179906912
Test: Running through long screenshot flow, turning screen on/off,
returning to task.

Change-Id: If5715c7008bbf3a1c8918a202a733fb3f8e8f876
parent a7e5dab6
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -334,6 +334,11 @@
            </intent-filter>
        </receiver>

        <activity android:name=".screenshot.LongScreenshotActivity"
                  android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
                  android:process=":screenshot"
                  android:finishOnTaskLaunch="true" />

        <activity android:name=".screenrecord.ScreenRecordDialog"
            android:theme="@style/ScreenRecord"
            android:showForAllUsers="true"
+7 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import com.android.systemui.ForegroundServicesDialog;
import com.android.systemui.keyguard.WorkLockActivity;
import com.android.systemui.people.PeopleSpaceActivity;
import com.android.systemui.screenrecord.ScreenRecordDialog;
import com.android.systemui.screenshot.LongScreenshotActivity;
import com.android.systemui.settings.brightness.BrightnessDialog;
import com.android.systemui.statusbar.tv.notifications.TvNotificationPanelActivity;
import com.android.systemui.tuner.TunerActivity;
@@ -99,4 +100,10 @@ public abstract class DefaultActivityBinder {
    @IntoMap
    @ClassKey(PeopleSpaceActivity.class)
    public abstract Activity bindPeopleSpaceActivity(PeopleSpaceActivity activity);

    /** Inject into LongScreenshotActivity. */
    @Binds
    @IntoMap
    @ClassKey(LongScreenshotActivity.class)
    public abstract Activity bindLongScreenshotActivity(LongScreenshotActivity activity);
}
+201 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;

import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;

import java.util.concurrent.Executor;

import javax.inject.Inject;

/**
 * LongScreenshotActivity acquires bitmap data for a long screenshot and lets the user trim the top
 * and bottom before saving/sharing/editing.
 */
public class LongScreenshotActivity extends Activity {
    private static final String TAG = "LongScreenshotActivity";

    private final UiEventLogger mUiEventLogger;
    private final ScrollCaptureController mScrollCaptureController;

    private ImageView mPreview;
    private View mSave;
    private View mCancel;
    private View mEdit;
    private View mShare;
    private CropView mCropView;
    private MagnifierView mMagnifierView;

    private enum PendingAction {
        SHARE,
        EDIT,
        SAVE
    }

    @Inject
    public LongScreenshotActivity(UiEventLogger uiEventLogger,
            ImageExporter imageExporter,
            @Main Executor mainExecutor,
            @Background Executor bgExecutor,
            Context context) {
        mUiEventLogger = uiEventLogger;

        mScrollCaptureController = new ScrollCaptureController(context,
                ScreenshotController.sScrollConnection, mainExecutor, bgExecutor, imageExporter);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.long_screenshot);

        mPreview = findViewById(R.id.preview);
        mSave = findViewById(R.id.save);
        mCancel = findViewById(R.id.cancel);
        mEdit = findViewById(R.id.edit);
        mShare = findViewById(R.id.share);
        mCropView = findViewById(R.id.crop_view);
        mMagnifierView = findViewById(R.id.magnifier);
        mCropView.setCropInteractionListener(mMagnifierView);

        mSave.setOnClickListener(this::onClicked);
        mCancel.setOnClickListener(this::onClicked);
        mEdit.setOnClickListener(this::onClicked);
        mShare.setOnClickListener(this::onClicked);
    }

    @Override
    public void onStart() {
        super.onStart();
        if (mPreview.getDrawable() == null) {
            doCapture();
        }
    }

    private void disableButtons() {
        mSave.setEnabled(false);
        mCancel.setEnabled(false);
        mEdit.setEnabled(false);
        mShare.setEnabled(false);
    }

    private void doEdit(Uri uri) {
        String editorPackage = getString(R.string.config_screenshotEditor);
        Intent intent = new Intent(Intent.ACTION_EDIT);
        if (!TextUtils.isEmpty(editorPackage)) {
            intent.setComponent(ComponentName.unflattenFromString(editorPackage));
        }
        intent.setType("image/png");
        intent.setData(uri);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
                | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

        startActivityAsUser(intent, UserHandle.CURRENT);
        finishAndRemoveTask();
    }

    private void doShare(Uri uri) {
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType("image/png");
        intent.setData(uri);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
                | Intent.FLAG_GRANT_READ_URI_PERMISSION);
        Intent sharingChooserIntent = Intent.createChooser(intent, null)
                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK
                        | Intent.FLAG_GRANT_READ_URI_PERMISSION);

        startActivityAsUser(sharingChooserIntent, UserHandle.CURRENT);
    }

    private void onClicked(View v) {
        int id = v.getId();
        v.setPressed(true);
        disableButtons();
        if (id == R.id.save) {
            startExport(PendingAction.SAVE);
        } else if (id == R.id.cancel) {
            finishAndRemoveTask();
        } else if (id == R.id.edit) {
            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_EDIT);
            startExport(PendingAction.EDIT);
        } else if (id == R.id.share) {
            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_SHARE);
            startExport(PendingAction.SHARE);
        }
    }

    private void startExport(PendingAction action) {
        mScrollCaptureController.startExport(mCropView.getTopBoundary(),
                mCropView.getBottomBoundary(), new ScrollCaptureController.ExportCallback() {
                    @Override
                    public void onError() {
                        Log.e(TAG, "Error exporting image data.");
                    }

                    @Override
                    public void onExportComplete(Uri outputUri) {
                        switch (action) {
                            case EDIT:
                                doEdit(outputUri);
                                break;
                            case SHARE:
                                doShare(outputUri);
                                break;
                            case SAVE:
                                // Nothing more to do
                                finishAndRemoveTask();
                                break;
                        }
                    }
                });
    }

    private void doCapture() {
        mScrollCaptureController.start(new ScrollCaptureController.ScrollCaptureCallback() {
            @Override
            public void onError() {
                Log.e(TAG, "Error!");
                finishAndRemoveTask();
            }

            @Override
            public void onComplete(ImageTileSet imageTileSet) {
                Log.i(TAG, "Got tiles " + imageTileSet.getWidth() + " x "
                        + imageTileSet.getHeight());
                mPreview.setImageDrawable(imageTileSet.getDrawable());
                mMagnifierView.setImageTileset(imageTileSet);
                mCropView.animateBoundaryTo(CropView.CropBoundary.BOTTOM, 0.5f);
            }
        });
    }
}
+8 −14
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import android.app.Notification;
import android.app.WindowContext;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.graphics.Insets;
@@ -100,6 +101,8 @@ import javax.inject.Inject;
public class ScreenshotController {
    private static final String TAG = logTag(ScreenshotController.class);

    public static ScrollCaptureClient.Connection sScrollConnection;

    /**
     * POD used in the AsyncTask which saves an image in the background.
     */
@@ -597,21 +600,12 @@ public class ScreenshotController {
    }

    private void runScrollCapture(ScrollCaptureClient.Connection connection) {
        cancelTimeout();
        ScrollCaptureController controller = new ScrollCaptureController(mContext, connection,
                mMainExecutor, mBgExecutor, mImageExporter, mUiEventLogger);
        controller.attach(mWindow);
        controller.start(new TakeScreenshotService.RequestCallback() {
            @Override
            public void reportError() {
            }
        sScrollConnection = connection;  // For LongScreenshotActivity to pick up.

            @Override
            public void onFinish() {
                Log.d(TAG, "onFinish from ScrollCaptureController");
                finishDismiss();
            }
        });
        Intent intent = new Intent(mContext, LongScreenshotActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        mContext.startActivity(intent);
        dismissScreenshot(false);
    }

    /**
+34 −152
Original line number Diff line number Diff line
@@ -16,29 +16,16 @@

package com.android.systemui.screenshot;

import android.annotation.IdRes;
import android.annotation.UiThread;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.net.Uri;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver.InternalInsetsInfo;
import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
import android.view.Window;
import android.widget.ImageView;

import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;

import com.android.systemui.screenshot.ScrollCaptureClient.CaptureResult;
import com.android.systemui.screenshot.ScrollCaptureClient.Connection;
import com.android.systemui.screenshot.ScrollCaptureClient.Session;
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;

import com.google.common.util.concurrent.ListenableFuture;

@@ -50,7 +37,7 @@ import java.util.concurrent.Executor;
/**
 * Interaction controller between the UI and ScrollCaptureClient.
 */
public class ScrollCaptureController implements OnComputeInternalInsetsListener {
public class ScrollCaptureController {
    private static final String TAG = "ScrollCaptureController";
    private static final float MAX_PAGES_DEFAULT = 3f;

@@ -64,13 +51,6 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
    private boolean mAtTopEdge;
    private Session mSession;

    // TODO: Support saving without additional action.
    private enum PendingAction {
        SHARE,
        EDIT,
        SAVE
    }

    public static final int MAX_HEIGHT = 12000;

    private final Connection mConnection;
@@ -80,172 +60,59 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
    private final Executor mBgExecutor;
    private final ImageExporter mImageExporter;
    private final ImageTileSet mImageTileSet;
    private final UiEventLogger mUiEventLogger;

    private ZonedDateTime mCaptureTime;
    private UUID mRequestId;
    private RequestCallback mCallback;
    private Window mWindow;
    private ImageView mPreview;
    private View mSave;
    private View mCancel;
    private View mEdit;
    private View mShare;
    private CropView mCropView;
    private MagnifierView mMagnifierView;
    private ScrollCaptureCallback mCaptureCallback;

    public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor,
            Executor bgExecutor, ImageExporter exporter, UiEventLogger uiEventLogger) {
            Executor bgExecutor, ImageExporter exporter) {
        mContext = context;
        mConnection = connection;
        mUiExecutor = uiExecutor;
        mBgExecutor = bgExecutor;
        mImageExporter = exporter;
        mUiEventLogger = uiEventLogger;
        mImageTileSet = new ImageTileSet(context.getMainThreadHandler());
    }

    /**
     * @param window the window to display the preview
     */
    public void attach(Window window) {
        mWindow = window;
    }

    /**
     * Run scroll capture!
     *
     * @param callback request callback to report back to the service
     */
    public void start(RequestCallback callback) {
    public void start(ScrollCaptureCallback callback) {
        mCaptureTime = ZonedDateTime.now();
        mRequestId = UUID.randomUUID();
        mCallback = callback;

        setContentView(R.layout.long_screenshot);
        mWindow.getDecorView().getViewTreeObserver()
                .addOnComputeInternalInsetsListener(this);
        mPreview = findViewById(R.id.preview);

        mSave = findViewById(R.id.save);
        mCancel = findViewById(R.id.cancel);
        mEdit = findViewById(R.id.edit);
        mShare = findViewById(R.id.share);
        mCropView = findViewById(R.id.crop_view);
        mMagnifierView = findViewById(R.id.magnifier);
        mCropView.setCropInteractionListener(mMagnifierView);

        mSave.setOnClickListener(this::onClicked);
        mCancel.setOnClickListener(this::onClicked);
        mEdit.setOnClickListener(this::onClicked);
        mShare.setOnClickListener(this::onClicked);
        mCaptureCallback = callback;

        float maxPages = Settings.Secure.getFloat(mContext.getContentResolver(),
                SETTING_KEY_MAX_PAGES, MAX_PAGES_DEFAULT);
        mConnection.start(this::startCapture, maxPages);
    }


    /** Ensure the entire window is touchable */
    public void onComputeInternalInsets(InternalInsetsInfo inoutInfo) {
        inoutInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
    }

    void disableButtons() {
        mSave.setEnabled(false);
        mCancel.setEnabled(false);
        mEdit.setEnabled(false);
        mShare.setEnabled(false);
    }

    private void onClicked(View v) {
        Log.d(TAG, "button clicked!");

        int id = v.getId();
        v.setPressed(true);
        disableButtons();
        if (id == R.id.save) {
            startExport(PendingAction.SAVE);
        } else if (id == R.id.cancel) {
            doFinish();
        } else if (id == R.id.edit) {
            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_EDIT);
            startExport(PendingAction.EDIT);
        } else if (id == R.id.share) {
            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_SHARE);
            startExport(PendingAction.SHARE);
        }
    }

    private void doFinish() {
        mPreview.setImageDrawable(null);
        mMagnifierView.setImageTileset(null);
        mImageTileSet.clear();
        mCallback.onFinish();
        mWindow.getDecorView().getViewTreeObserver()
                .removeOnComputeInternalInsetsListener(this);
    }

    private void startExport(PendingAction action) {
    /**
     * @param topCrop    [0,1) fraction of the top of the image to be cropped out.
     * @param bottomCrop (0, 1] fraction to be cropped out, e.g. 0.7 will crop out the bottom 30%.
     */
    public void startExport(float topCrop, float bottomCrop, ExportCallback callback) {
        Rect croppedPortion = new Rect(
                0,
                (int) (mImageTileSet.getHeight() * mCropView.getTopBoundary()),
                (int) (mImageTileSet.getHeight() * topCrop),
                mImageTileSet.getWidth(),
                (int) (mImageTileSet.getHeight() * mCropView.getBottomBoundary()));
                (int) (mImageTileSet.getHeight() * bottomCrop));
        ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
                mBgExecutor, mRequestId, mImageTileSet.toBitmap(croppedPortion), mCaptureTime);
        exportFuture.addListener(() -> {
            try {
                ImageExporter.Result result = exportFuture.get();
                if (action == PendingAction.EDIT) {
                    doEdit(result.uri);
                } else if (action == PendingAction.SHARE) {
                    doShare(result.uri);
                }
                doFinish();
                callback.onExportComplete(result.uri);
            } catch (InterruptedException | ExecutionException e) {
                Log.e(TAG, "failed to export", e);
                mCallback.onFinish();
                callback.onError();
            }
        }, mUiExecutor);
    }

    private void doEdit(Uri uri) {
        String editorPackage = mContext.getString(R.string.config_screenshotEditor);
        Intent intent = new Intent(Intent.ACTION_EDIT);
        if (!TextUtils.isEmpty(editorPackage)) {
            intent.setComponent(ComponentName.unflattenFromString(editorPackage));
        }
        intent.setType("image/png");
        intent.setData(uri);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
                | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
    }

    private void doShare(Uri uri) {
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType("image/png");
        intent.setData(uri);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
                | Intent.FLAG_GRANT_READ_URI_PERMISSION);
        Intent sharingChooserIntent = Intent.createChooser(intent, null)
                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK
                        | Intent.FLAG_GRANT_READ_URI_PERMISSION);

        mContext.startActivityAsUser(sharingChooserIntent, UserHandle.CURRENT);
    }

    private void setContentView(@IdRes int id) {
        mWindow.setContentView(id);
    }

    <T extends View> T findViewById(@IdRes int res) {
        return mWindow.findViewById(res);
    }


    private void onCaptureResult(CaptureResult result) {
        Log.d(TAG, "onCaptureResult: " + result);
        boolean emptyResult = result.captured.height() == 0;
@@ -327,11 +194,26 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
        Log.d(TAG, "afterCaptureComplete");

        if (mImageTileSet.isEmpty()) {
            session.end(mCallback::onFinish);
            mCaptureCallback.onError();
        } else {
            mPreview.setImageDrawable(mImageTileSet.getDrawable());
            mMagnifierView.setImageTileset(mImageTileSet);
            mCropView.animateBoundaryTo(CropView.CropBoundary.BOTTOM, 0.5f);
            mCaptureCallback.onComplete(mImageTileSet);
        }
    }

    /**
     * Callback for image capture completion or error.
     */
    public interface ScrollCaptureCallback {
        void onComplete(ImageTileSet imageTileSet);
        void onError();
    }

    /**
     * Callback for image export completion or error.
     */
    public interface ExportCallback {
        void onExportComplete(Uri outputUri);
        void onError();
    }

}