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

Commit 3a8a8852 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Move long screenshot UI to an Activity" into sc-dev

parents b717397d 0606a734
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();
    }

}