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

Commit 7c32ed42 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "Implements initial long screenshot preview UI" into sc-dev am: e07bbc98

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/13418730

MUST ONLY BE SUBMITTED BY AUTOMERGER

Change-Id: I9eeb657c9d3504d23ba6788abe67b4f48da82010
parents e9ce9e20 e07bbc98
Loading
Loading
Loading
Loading
+84 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ 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.
  -->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="?android:colorBackgroundFloating"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/preview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginVertical="8dp"
        android:layout_marginHorizontal="48dp"
        android:adjustViewBounds="true"
        app:layout_constrainedHeight="true"
        app:layout_constrainedWidth="true"
        app:layout_constraintBottom_toBottomOf="@id/guideline"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:background="?android:colorBackground"
        tools:minHeight="100dp"
        tools:minWidth="100dp" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.9" />

    <Button
        android:id="@+id/close"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:text="Close"
        app:layout_constraintEnd_toStartOf="@+id/edit"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline" />

    <Button
        android:id="@+id/edit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:text="Edit"
        app:layout_constraintEnd_toStartOf="@+id/share"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/close"
        app:layout_constraintTop_toTopOf="@+id/guideline" />

    <Button
        android:id="@+id/share"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:text="Share"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/edit"
        app:layout_constraintTop_toTopOf="@+id/guideline" />

</androidx.constraintlayout.widget.ConstraintLayout>
+8 −7
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ import static android.graphics.ColorSpace.Named.SRGB;

import static java.util.Objects.requireNonNull;

import android.annotation.NonNull;
import android.graphics.Bitmap;
import android.graphics.ColorSpace;
import android.graphics.RecordingCanvas;
@@ -50,14 +51,13 @@ class ImageTile implements AutoCloseable {
     * @param image an image containing a hardware buffer
     * @param location the captured area represented by image tile (virtual coordinates)
     */
    ImageTile(Image image, Rect location) {
    ImageTile(@NonNull Image image, @NonNull Rect location) {
        mImage = requireNonNull(image, "image");
        mLocation = location;

        mLocation = requireNonNull(location);
        requireNonNull(mImage.getHardwareBuffer(), "image must be a hardware image");
    }

    RenderNode getDisplayList() {
    synchronized RenderNode getDisplayList() {
        if (mNode == null) {
            mNode = new RenderNode("Tile{" + Integer.toHexString(mImage.hashCode()) + "}");
        }
@@ -69,7 +69,6 @@ class ImageTile implements AutoCloseable {
        mNode.setPosition(0, 0, w, h);

        RecordingCanvas canvas = mNode.beginRecording(w, h);
        Rect rect = new Rect(0, 0, w, h);
        canvas.save();
        canvas.clipRect(0, 0, mLocation.right, mLocation.bottom);
        canvas.drawBitmap(Bitmap.wrapHardwareBuffer(mImage.getHardwareBuffer(), COLOR_SPACE),
@@ -100,10 +99,12 @@ class ImageTile implements AutoCloseable {
    }

    @Override
    public void close() {
    public synchronized void close() {
        mImage.close();
        if (mNode != null) {
            mNode.discardDisplayList();
        }
    }

    @Override
    public String toString() {
+12 −1
Original line number Diff line number Diff line
@@ -603,7 +603,18 @@ public class ScreenshotController {
        cancelTimeout();
        ScrollCaptureController controller = new ScrollCaptureController(mContext, connection,
                mMainExecutor, mBgExecutor, mImageExporter);
        controller.start(/* onDismiss */ () -> dismissScreenshot(false));
        controller.attach(mWindow);
        controller.start(new TakeScreenshotService.RequestCallback() {
            @Override
            public void reportError() {
            }

            @Override
            public void onFinish() {
                Log.d(TAG, "onFinish from ScrollCaptureController");
                finishDismiss();
            }
        });
    }

    /**
+153 −32
Original line number Diff line number Diff line
@@ -16,16 +16,24 @@

package com.android.systemui.screenshot;

import android.annotation.IdRes;
import android.annotation.UiThread;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.UserHandle;
import android.util.Log;
import android.widget.Toast;
import android.view.LayoutInflater;
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.systemui.R;
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;

@@ -38,11 +46,9 @@ import java.util.function.Consumer;
/**
 * Interaction controller between the UI and ScrollCaptureClient.
 */
public class ScrollCaptureController {
public class ScrollCaptureController implements OnComputeInternalInsetsListener {
    private static final String TAG = "ScrollCaptureController";

    private static final boolean USE_TILED_IMAGE = false;

    public static final int MAX_PAGES = 5;
    public static final int MAX_HEIGHT = 12000;

@@ -53,9 +59,19 @@ public class ScrollCaptureController {
    private final Executor mBgExecutor;
    private final ImageExporter mImageExporter;
    private final ImageTileSet mImageTileSet;
    private final LayoutInflater mLayoutInflater;

    private ZonedDateTime mCaptureTime;
    private UUID mRequestId;
    private RequestCallback mCallback;
    private Window mWindow;
    private ImageView mPreview;
    private View mClose;
    private View mEdit;
    private View mShare;

    private ListenableFuture<ImageExporter.Result> mExportFuture;
    private Runnable mPendingAction;

    public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor,
            Executor bgExecutor, ImageExporter exporter) {
@@ -65,20 +81,126 @@ public class ScrollCaptureController {
        mBgExecutor = bgExecutor;
        mImageExporter = exporter;
        mImageTileSet = new ImageTileSet();
        mLayoutInflater = mContext.getSystemService(LayoutInflater.class);
    }

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

    /**
     * Run scroll capture!
     *
     * @param after action to take after the flow is complete
     * @param callback request callback to report back to the service
     */
    public void start(final Runnable after) {
    public void start(RequestCallback callback) {
        mCaptureTime = ZonedDateTime.now();
        mRequestId = UUID.randomUUID();
        mConnection.start((session) -> startCapture(session, after));
        mCallback = callback;

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

        mClose = findViewById(R.id.close);
        mEdit = findViewById(R.id.edit);
        mShare = findViewById(R.id.share);

        mClose.setOnClickListener(this::onClicked);
        mEdit.setOnClickListener(this::onClicked);
        mShare.setOnClickListener(this::onClicked);

        mPreview.setImageDrawable(mImageTileSet.getDrawable());
        mConnection.start(this::startCapture);
    }

    private void startCapture(Session session, final Runnable onDismiss) {

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

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

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

        int id = v.getId();
        if (id == R.id.close) {
            v.setPressed(true);
            disableButtons();
            finish();
        } else if (id == R.id.edit) {
            v.setPressed(true);
            disableButtons();
            edit();
        } else if (id == R.id.share) {
            v.setPressed(true);
            disableButtons();
            share();
        }
    }

    private void finish() {
        if (mExportFuture == null) {
            doFinish();
        } else {
            mExportFuture.addListener(this::doFinish, mUiExecutor);
        }
    }

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

    private void edit() {
        sendIntentWhenReady(Intent.ACTION_EDIT);
    }

    private void share() {
        sendIntentWhenReady(Intent.ACTION_SEND);
    }

    void sendIntentWhenReady(String action) {
        if (mExportFuture != null) {
            mExportFuture.addListener(() -> {
                try {
                    ImageExporter.Result result = mExportFuture.get();
                    sendIntent(action, result.uri);
                    mCallback.onFinish();
                } catch (InterruptedException | ExecutionException e) {
                    Log.e(TAG, "failed to export", e);
                    mCallback.onFinish();
                }

            }, mUiExecutor);
        } else {
            mPendingAction = this::edit;
        }
    }

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

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

    private void startCapture(Session session) {
        Log.d(TAG, "startCapture");
        Consumer<ScrollCaptureClient.CaptureResult> consumer =
                new Consumer<ScrollCaptureClient.CaptureResult>() {

@@ -91,17 +213,17 @@ public class ScrollCaptureController {

                        boolean emptyFrame = result.captured.height() == 0;
                        if (!emptyFrame) {
                            mImageTileSet.addTile(new ImageTile(result.image, result.captured));
                            ImageTile tile = new ImageTile(result.image, result.captured);
                            Log.d(TAG, "Adding tile: " + tile);
                            mImageTileSet.addTile(tile);
                            Log.d(TAG, "New dimens: w=" + mImageTileSet.getWidth() + ", "
                                    + "h=" + mImageTileSet.getHeight());
                        }

                        if (emptyFrame || mFrameCount >= MAX_PAGES
                                || mTop + session.getTileHeight() > MAX_HEIGHT) {
                            if (!mImageTileSet.isEmpty()) {
                                exportToFile(mImageTileSet.toBitmap(), session, onDismiss);
                                mImageTileSet.clear();
                            } else {
                                session.end(onDismiss);
                            }

                            mUiExecutor.execute(() -> afterCaptureComplete(session));
                            return;
                        }
                        mTop += result.captured.height();
@@ -113,25 +235,24 @@ public class ScrollCaptureController {
        session.requestTile(0, consumer);
    };

    void exportToFile(Bitmap bitmap, Session session, Runnable afterEnd) {
        mImageExporter.setFormat(Bitmap.CompressFormat.PNG);
        mImageExporter.setQuality(6);
        ListenableFuture<ImageExporter.Result> future =
                mImageExporter.export(mBgExecutor, mRequestId, bitmap, mCaptureTime);
        future.addListener(() -> {
            try {
                ImageExporter.Result result = future.get();
                launchViewer(result.uri);
            } catch (InterruptedException | ExecutionException e) {
                Toast.makeText(mContext, "Failed to write image", Toast.LENGTH_SHORT).show();
                Log.e(TAG, "Error storing screenshot to media store", e.getCause());
    @UiThread
    void afterCaptureComplete(Session session) {
        Log.d(TAG, "afterCaptureComplete");

        if (mImageTileSet.isEmpty()) {
            session.end(mCallback::onFinish);
        } else {
            mExportFuture = mImageExporter.export(
                    mBgExecutor, mRequestId, mImageTileSet.toBitmap(), mCaptureTime);
            // The user chose an action already, link it to the result
            if (mPendingAction != null) {
                mExportFuture.addListener(mPendingAction, mUiExecutor);
            }
        }
            session.end(afterEnd); // end session, close connection, afterEnd.run()
        }, mUiExecutor);
    }

    void launchViewer(Uri uri) {
        Intent editIntent = new Intent(Intent.ACTION_VIEW);
    void sendIntent(String action, Uri uri) {
        Intent editIntent = new Intent(action);
        editIntent.setType("image/png");
        editIntent.setData(uri);
        editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);