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

Commit d1eb8fd4 authored by Miranda Kephart's avatar Miranda Kephart
Browse files

Remove screenshot_image_exporter flag

Bug: 352308052
Fix: 352308052
Test: manual
Flag: EXEMPT removing rolled out flag
Change-Id: Ife22472c7d157127ad872ad69862561135363c33
parent d81988e0
Loading
Loading
Loading
Loading
+0 −10
Original line number Diff line number Diff line
@@ -605,16 +605,6 @@ flag {
    }
}

flag {
    name: "screenshot_save_image_exporter"
    namespace: "systemui"
    description: "Save all screenshots using ImageExporter"
    bug: "352308052"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "screenshot_ui_controller_refactor"
    namespace: "systemui"
+6 −72
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.systemui.screenshot;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;

import static com.android.systemui.Flags.screenshotSaveImageExporter;
import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
@@ -133,7 +132,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler
    private final MessageContainerController mMessageContainerController;
    private final AnnouncementResolver mAnnouncementResolver;
    private Bitmap mScreenBitmap;
    private SaveImageInBackgroundTask mSaveInBgTask;
    private boolean mScreenshotTakenInPortrait;
    private boolean mAttachRequested;
    private boolean mDetachRequested;
@@ -393,10 +391,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler
    // Any cleanup needed when the service is being destroyed.
    @Override
    public void onDestroy() {
        if (mSaveInBgTask != null) {
            // just log success/failure for the pre-existing screenshot
            mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
        }
        removeWindow();
        releaseMediaPlayer();
        releaseContext();
@@ -598,36 +592,12 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler
        // Play the shutter sound to notify that we've taken a screenshot
        playCameraSoundIfNeeded();

        if (screenshotSaveImageExporter()) {
        saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> {
            if (result.uri != null) {
                mScreenshotHandler.post(() -> Toast.makeText(mContext,
                        R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
            }
        });
        } else {
            saveScreenshotInWorkerThread(
                    screenshot.getUserHandle(),
                    /* onComplete */ finisher,
                    /* actionsReadyListener */ imageData -> {
                        if (DEBUG_CALLBACK) {
                            Log.d(TAG,
                                    "returning URI to finisher (Consumer<URI>): " + imageData.uri);
                        }
                        finisher.accept(imageData.uri);
                        if (imageData.uri == null) {
                            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0,
                                    mPackageName);
                            mNotificationsController.notifyScreenshotError(
                                    R.string.screenshot_failed_to_save_text);
                        } else {
                            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
                            mScreenshotHandler.post(() -> Toast.makeText(mContext,
                                    R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
                        }
                    },
                    null);
        }
    }

    /**
@@ -699,35 +669,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler
        }, mMainExecutor);
    }

    /**
     * Creates a new worker thread and saves the screenshot to the media store.
     */
    private void saveScreenshotInWorkerThread(
            UserHandle owner,
            @NonNull Consumer<Uri> finisher,
            @Nullable SaveImageInBackgroundTask.ActionsReadyListener actionsReadyListener,
            @Nullable SaveImageInBackgroundTask.QuickShareActionReadyListener
                    quickShareActionsReadyListener) {
        SaveImageInBackgroundTask.SaveImageInBackgroundData
                data = new SaveImageInBackgroundTask.SaveImageInBackgroundData();
        data.image = mScreenBitmap;
        data.finisher = finisher;
        data.mActionsReadyListener = actionsReadyListener;
        data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
        data.owner = owner;
        data.displayId = mDisplay.getDisplayId();

        if (mSaveInBgTask != null) {
            // just log success/failure for the pre-existing screenshot
            mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
        }

        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter,
                mScreenshotSmartActions, data,
                mScreenshotNotificationSmartActionsProvider);
        mSaveInBgTask.execute();
    }

    /**
     * Logs success/failure of the screenshot saving task, and shows an error if it failed.
     */
@@ -745,13 +686,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler
        }
    }

    /**
     * Logs success/failure of the screenshot saving task, and shows an error if it failed.
     */
    private void logSuccessOnActionsReady(SaveImageInBackgroundTask.SavedImageData imageData) {
        logScreenshotResultStatus(imageData.uri, imageData.owner);
    }

    private boolean isUserSetupComplete(UserHandle owner) {
        return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
                .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+0 −399
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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 static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE;
import static com.android.systemui.screenshot.LogConfig.logTag;
import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType;

import android.app.Notification;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.systemui.flags.FeatureFlags;

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

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

/**
 * An AsyncTask that saves an image to the media store in the background.
 */
class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
    private static final String TAG = logTag(SaveImageInBackgroundTask.class);

    private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s";
    private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";

    /**
     * POD used in the AsyncTask which saves an image in the background.
     */
    static class SaveImageInBackgroundData {
        public Bitmap image;
        public Consumer<Uri> finisher;
        public ActionsReadyListener mActionsReadyListener;
        public QuickShareActionReadyListener mQuickShareActionsReadyListener;
        public UserHandle owner;
        public int displayId;

        void clearImage() {
            image = null;
        }
    }

    /**
     * Structure returned by the SaveImageInBackgroundTask
     */
    public static class SavedImageData {
        public Uri uri;
        public List<Notification.Action> smartActions;
        public Notification.Action quickShareAction;
        public UserHandle owner;
        public String subject;  // Title for sharing
        public Long imageTime; // Time at which screenshot was saved

        /**
         * Used to reset the return data on error
         */
        public void reset() {
            uri = null;
            smartActions = null;
            quickShareAction = null;
            subject = null;
            imageTime = null;
        }
    }

    /**
     * Structure returned by the QueryQuickShareInBackgroundTask
     */
    static class QuickShareData {
        public Notification.Action quickShareAction;

        /**
         * Used to reset the return data on error
         */
        public void reset() {
            quickShareAction = null;
        }
    }

    interface ActionsReadyListener {
        void onActionsReady(SavedImageData imageData);
    }

    interface QuickShareActionReadyListener {
        void onActionsReady(QuickShareData quickShareData);
    }

    private final Context mContext;
    private FeatureFlags mFlags;
    private final ScreenshotSmartActions mScreenshotSmartActions;
    private final SaveImageInBackgroundData mParams;
    private final SavedImageData mImageData;
    private final QuickShareData mQuickShareData;

    private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
    private String mScreenshotId;
    private final Random mRandom = new Random();
    private final ImageExporter mImageExporter;
    private long mImageTime;

    SaveImageInBackgroundTask(
            Context context,
            FeatureFlags flags,
            ImageExporter exporter,
            ScreenshotSmartActions screenshotSmartActions,
            SaveImageInBackgroundData data,
            ScreenshotNotificationSmartActionsProvider
                    screenshotNotificationSmartActionsProvider
    ) {
        mContext = context;
        mFlags = flags;
        mScreenshotSmartActions = screenshotSmartActions;
        mImageData = new SavedImageData();
        mQuickShareData = new QuickShareData();
        mImageExporter = exporter;

        // Prepare all the output metadata
        mParams = data;

        // Initialize screenshot notification smart actions provider.
        mSmartActionsProvider = screenshotNotificationSmartActionsProvider;
    }

    @Override
    protected Void doInBackground(Void... paramsUnused) {
        if (isCancelled()) {
            if (DEBUG_STORAGE) {
                Log.d(TAG, "cancelled! returning null");
            }
            return null;
        }
        // TODO: move to constructor / from ScreenshotRequest
        final UUID requestId = UUID.randomUUID();

        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);

        Bitmap image = mParams.image;
        mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, requestId);

        boolean savingToOtherUser = mParams.owner != Process.myUserHandle();
        // Smart actions don't yet work for cross-user saves.
        boolean smartActionsEnabled = !savingToOtherUser
                && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
                SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS,
                true);
        try {
            if (smartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) {
                // Since Quick Share target recommendation does not rely on image URL, it is
                // queried and surfaced before image compress/export. Action intent would not be
                // used, because it does not contain image URL.
                Notification.Action quickShare =
                        queryQuickShareAction(mScreenshotId, image, mParams.owner, null);
                if (quickShare != null) {
                    mQuickShareData.quickShareAction = quickShare;
                    mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
                }
            }

            // Call synchronously here since already on a background thread.
            ListenableFuture<ImageExporter.Result> future =
                    mImageExporter.export(Runnable::run, requestId, image, mParams.owner,
                            mParams.displayId);
            ImageExporter.Result result = future.get();
            Log.d(TAG, "Saved screenshot: " + result);
            final Uri uri = result.uri;
            mImageTime = result.timestamp;

            CompletableFuture<List<Notification.Action>> smartActionsFuture =
                    mScreenshotSmartActions.getSmartActionsFuture(
                            mScreenshotId, uri, image, mSmartActionsProvider,
                            ScreenshotSmartActionType.REGULAR_SMART_ACTIONS,
                            smartActionsEnabled, mParams.owner);
            List<Notification.Action> smartActions = new ArrayList<>();
            if (smartActionsEnabled) {
                int timeoutMs = DeviceConfig.getInt(
                        DeviceConfig.NAMESPACE_SYSTEMUI,
                        SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS,
                        1000);
                smartActions.addAll(buildSmartActions(
                        mScreenshotSmartActions.getSmartActions(
                                mScreenshotId, smartActionsFuture, timeoutMs,
                                mSmartActionsProvider,
                                ScreenshotSmartActionType.REGULAR_SMART_ACTIONS),
                        mContext));
            }

            mImageData.uri = uri;
            mImageData.owner = mParams.owner;
            mImageData.smartActions = smartActions;
            mImageData.quickShareAction = createQuickShareAction(
                    mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image,
                    mParams.owner);
            mImageData.subject = getSubjectString(mImageTime);
            mImageData.imageTime = mImageTime;

            mParams.mActionsReadyListener.onActionsReady(mImageData);
            if (DEBUG_CALLBACK) {
                Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) "
                        + "finisher.accept(\"" + mImageData.uri + "\"");
            }
            mParams.finisher.accept(mImageData.uri);
            mParams.image = null;
        } catch (Exception e) {
            // IOException/UnsupportedOperationException may be thrown if external storage is
            // not mounted
            Log.d(TAG, "Failed to store screenshot", e);
            mParams.clearImage();
            mImageData.reset();
            mQuickShareData.reset();
            mParams.mActionsReadyListener.onActionsReady(mImageData);
            if (DEBUG_CALLBACK) {
                Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)");
            }
            mParams.finisher.accept(null);
        }

        return null;
    }

    /**
     * Update the listener run when the saving task completes. Used to avoid showing UI for the
     * first screenshot when a second one is taken.
     */
    void setActionsReadyListener(ActionsReadyListener listener) {
        mParams.mActionsReadyListener = listener;
    }

    @Override
    protected void onCancelled(Void params) {
        // If we are cancelled while the task is running in the background, we may get null
        // params. The finisher is expected to always be called back, so just use the baked-in
        // params from the ctor in any case.
        mImageData.reset();
        mQuickShareData.reset();
        mParams.mActionsReadyListener.onActionsReady(mImageData);
        if (DEBUG_CALLBACK) {
            Log.d(TAG, "onCancelled, calling (Consumer<Uri>) finisher.accept(null)");
        }
        mParams.finisher.accept(null);
        mParams.clearImage();
    }

    private List<Notification.Action> buildSmartActions(
            List<Notification.Action> actions, Context context) {
        List<Notification.Action> broadcastActions = new ArrayList<>();
        for (Notification.Action action : actions) {
            // Proxy smart actions through {@link SmartActionsReceiver} for logging smart actions.
            Bundle extras = action.getExtras();
            String actionType = extras.getString(
                    ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
                    ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE);
            Intent intent = new Intent(context, SmartActionsReceiver.class)
                    .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT, action.actionIntent)
                    .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
            addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */);
            PendingIntent broadcastIntent = PendingIntent.getBroadcast(context,
                    mRandom.nextInt(),
                    intent,
                    PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
            broadcastActions.add(new Notification.Action.Builder(action.getIcon(), action.title,
                    broadcastIntent).setContextual(true).addExtras(extras).build());
        }
        return broadcastActions;
    }

    private static void addIntentExtras(String screenshotId, Intent intent, String actionType,
            boolean smartActionsEnabled) {
        intent
                .putExtra(SmartActionsReceiver.EXTRA_ACTION_TYPE, actionType)
                .putExtra(SmartActionsReceiver.EXTRA_ID, screenshotId)
                .putExtra(SmartActionsReceiver.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled);
    }

    /**
     * Wrap the quickshare intent and populate the fillin intent with the URI
     */
    @VisibleForTesting
    Notification.Action createQuickShareAction(
            Notification.Action quickShare, String screenshotId, Uri uri, long imageTime,
            Bitmap image, UserHandle user) {
        if (quickShare == null) {
            return null;
        } else if (quickShare.actionIntent.isImmutable()) {
            Notification.Action quickShareWithUri =
                    queryQuickShareAction(screenshotId, image, user, uri);
            if (quickShareWithUri == null
                    || !quickShareWithUri.title.toString().contentEquals(quickShare.title)) {
                return null;
            }
            quickShare = quickShareWithUri;
        }

        Intent wrappedIntent = new Intent(mContext, SmartActionsReceiver.class)
                .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT, quickShare.actionIntent)
                .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT_FILLIN,
                        createFillInIntent(uri, imageTime))
                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        Bundle extras = quickShare.getExtras();
        String actionType = extras.getString(
                ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
                ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE);
        // We only query for quick share actions when smart actions are enabled, so we can assert
        // that it's true here.
        addIntentExtras(screenshotId, wrappedIntent, actionType, true /* smartActionsEnabled */);
        PendingIntent broadcastIntent =
                PendingIntent.getBroadcast(mContext, mRandom.nextInt(), wrappedIntent,
                        PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
        return new Notification.Action.Builder(quickShare.getIcon(), quickShare.title,
                broadcastIntent)
                .setContextual(true)
                .addExtras(extras)
                .build();
    }

    private Intent createFillInIntent(Uri uri, long imageTime) {
        Intent fillIn = new Intent();
        fillIn.setType("image/png");
        fillIn.putExtra(Intent.EXTRA_STREAM, uri);
        fillIn.putExtra(Intent.EXTRA_SUBJECT, getSubjectString(imageTime));
        // Include URI in ClipData also, so that grantPermission picks it up.
        // We don't use setData here because some apps interpret this as "to:".
        ClipData clipData = new ClipData(
                new ClipDescription("content", new String[]{"image/png"}),
                new ClipData.Item(uri));
        fillIn.setClipData(clipData);
        fillIn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        return fillIn;
    }

    /**
     * Query and surface Quick Share chip if it is available. Action intent would not be used,
     * because it does not contain image URL which would be populated in {@link
     * #createQuickShareAction(Notification.Action, String, Uri, long, Bitmap, UserHandle)}
     */

    @VisibleForTesting
    Notification.Action queryQuickShareAction(
            String screenshotId, Bitmap image, UserHandle user, Uri uri) {
        CompletableFuture<List<Notification.Action>> quickShareActionsFuture =
                mScreenshotSmartActions.getSmartActionsFuture(
                        screenshotId, uri, image, mSmartActionsProvider,
                        ScreenshotSmartActionType.QUICK_SHARE_ACTION,
                        true /* smartActionsEnabled */, user);
        int timeoutMs = DeviceConfig.getInt(
                DeviceConfig.NAMESPACE_SYSTEMUI,
                SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_QUICK_SHARE_ACTIONS_TIMEOUT_MS,
                500);
        List<Notification.Action> quickShareActions =
                mScreenshotSmartActions.getSmartActions(
                        screenshotId, quickShareActionsFuture, timeoutMs,
                        mSmartActionsProvider,
                        ScreenshotSmartActionType.QUICK_SHARE_ACTION);
        if (!quickShareActions.isEmpty()) {
            return quickShareActions.get(0);
        }
        return null;
    }

    private static String getSubjectString(long imageTime) {
        String subjectDate = DateFormat.getDateTimeInstance().format(new Date(imageTime));
        return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
    }
}
+6 −72

File changed.

Preview size limit exceeded, changes collapsed.

+0 −278

File deleted.

Preview size limit exceeded, changes collapsed.