diff options
5 files changed, 255 insertions, 166 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 2faca8dbdcbf..fbe58c505662 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -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" diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java index 2b362b94d1f5..8d2639d4cdd0 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java @@ -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); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java new file mode 100644 index 000000000000..a6433ae94b2b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java @@ -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); + } + }); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 953b40b6e17b..31c693bdde1f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -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); } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java index ad5e637b189e..863116a22ee4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -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(); + } + } |