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

This commit is contained in:
TreeHugger Robot
2021-02-22 16:20:13 +00:00
committed by Android (Google) Code Review
5 changed files with 254 additions and 165 deletions

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -0,0 +1,201 @@
/*
* 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);
}
});
}
}

View File

@@ -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);
}
/**

View File

@@ -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();
}
}