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

Commit 24849ab1 authored by Ajinkya Chalke's avatar Ajinkya Chalke
Browse files

Add metrics logging for App Clips.

Test: atest AppClipsActivityTest AppClipsTrampolineActivityTest
Bug: 267815367
Change-Id: I6e9cfa0c54b1999a95a6f2260f23f6605f5f90a5
parent 473b8262
Loading
Loading
Loading
Loading
+7 −1
Original line number Diff line number Diff line
@@ -93,7 +93,13 @@ public enum ScreenshotEvent implements UiEventLogger.UiEventEnum {
    @UiEvent(doc = "User has discarded the result of a long screenshot")
    SCREENSHOT_LONG_SCREENSHOT_EXIT(911),
    @UiEvent(doc = "A screenshot has been taken and saved to work profile")
    SCREENSHOT_SAVED_TO_WORK_PROFILE(1240);
    SCREENSHOT_SAVED_TO_WORK_PROFILE(1240),
    @UiEvent(doc = "Notes application triggered the screenshot for notes")
    SCREENSHOT_FOR_NOTE_TRIGGERED(1308),
    @UiEvent(doc = "User accepted the screenshot to be sent to the notes app")
    SCREENSHOT_FOR_NOTE_ACCEPTED(1309),
    @UiEvent(doc = "User cancelled the screenshot for notes app flow")
    SCREENSHOT_FOR_NOTE_CANCELLED(1310);

    private final int mId;

+46 −2
Original line number Diff line number Diff line
@@ -17,15 +17,21 @@
package com.android.systemui.screenshot;

import static com.android.systemui.screenshot.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE;
import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_NAME;
import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_RESULT_RECEIVER;
import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
import static com.android.systemui.screenshot.AppClipsTrampolineActivity.PERMISSION_SELF;
import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_FOR_NOTE_ACCEPTED;
import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_FOR_NOTE_CANCELLED;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
@@ -33,15 +39,20 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;

import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLogger.UiEventEnum;
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.settings.UserTracker;

import javax.inject.Inject;

@@ -64,9 +75,15 @@ import javax.inject.Inject;
 *
 * TODO(b/267309532): Polish UI and animations.
 */
public final class AppClipsActivity extends ComponentActivity {
public class AppClipsActivity extends ComponentActivity {

    private static final String TAG = AppClipsActivity.class.getSimpleName();
    private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0);

    private final AppClipsViewModel.Factory mViewModelFactory;
    private final PackageManager mPackageManager;
    private final UserTracker mUserTracker;
    private final UiEventLogger mUiEventLogger;
    private final BroadcastReceiver mBroadcastReceiver;
    private final IntentFilter mIntentFilter;

@@ -80,10 +97,17 @@ public final class AppClipsActivity extends ComponentActivity {
    private AppClipsViewModel mViewModel;

    private ResultReceiver mResultReceiver;
    @Nullable
    private String mCallingPackageName;
    private int mCallingPackageUid;

    @Inject
    public AppClipsActivity(AppClipsViewModel.Factory viewModelFactory) {
    public AppClipsActivity(AppClipsViewModel.Factory viewModelFactory,
            PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger) {
        mViewModelFactory = viewModelFactory;
        mPackageManager = packageManager;
        mUserTracker = userTracker;
        mUiEventLogger = uiEventLogger;

        mBroadcastReceiver = new BroadcastReceiver() {
            @Override
@@ -113,6 +137,7 @@ public final class AppClipsActivity extends ComponentActivity {
                RECEIVER_NOT_EXPORTED);

        Intent intent = getIntent();
        setUpUiLogging(intent);
        mResultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER, ResultReceiver.class);
        if (mResultReceiver == null) {
            setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
@@ -169,6 +194,17 @@ public final class AppClipsActivity extends ComponentActivity {
        }
    }

    private void setUpUiLogging(Intent intent) {
        mCallingPackageName = intent.getStringExtra(EXTRA_CALLING_PACKAGE_NAME);
        mCallingPackageUid = 0;
        try {
            mCallingPackageUid = mPackageManager.getApplicationInfoAsUser(mCallingPackageName,
                    APPLICATION_INFO_FLAGS, mUserTracker.getUserId()).uid;
        } catch (NameNotFoundException e) {
            Log.d(TAG, "Couldn't find notes app UID " + e);
        }
    }

    private void setScreenshot(Bitmap screenshot) {
        // Set background, status and navigation bar colors as the activity is no longer
        // translucent.
@@ -228,6 +264,7 @@ public final class AppClipsActivity extends ComponentActivity {
        data.putParcelable(EXTRA_SCREENSHOT_URI, uri);
        try {
            mResultReceiver.send(Activity.RESULT_OK, data);
            logUiEvent(SCREENSHOT_FOR_NOTE_ACCEPTED);
        } catch (Exception e) {
            // Do nothing.
        }
@@ -251,6 +288,9 @@ public final class AppClipsActivity extends ComponentActivity {
        data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode);
        try {
            mResultReceiver.send(RESULT_OK, data);
            if (errorCode == Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED) {
                logUiEvent(SCREENSHOT_FOR_NOTE_CANCELLED);
            }
        } catch (Exception e) {
            // Do nothing.
        }
@@ -259,6 +299,10 @@ public final class AppClipsActivity extends ComponentActivity {
        mResultReceiver = null;
    }

    private void logUiEvent(UiEventEnum uiEvent) {
        mUiEventLogger.log(uiEvent, mCallingPackageUid, mCallingPackageName);
    }

    private void updateImageDimensions() {
        Drawable drawable = mPreview.getDrawable();
        if (drawable == null) {
+43 −5
Original line number Diff line number Diff line
@@ -24,26 +24,33 @@ import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE;
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;

import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_FOR_NOTE_TRIGGERED;

import android.app.Activity;
import android.app.admin.DevicePolicyManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.ResultReceiver;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.notetask.NoteTaskController;
import com.android.systemui.settings.UserTracker;
import com.android.wm.shell.bubbles.Bubbles;

import java.util.Optional;
@@ -70,15 +77,23 @@ import javax.inject.Inject;
public class AppClipsTrampolineActivity extends Activity {

    private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName();
    public static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
    static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI";
    public static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE";
    static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER";
    static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE";
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER";
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME";
    private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0);

    private final DevicePolicyManager mDevicePolicyManager;
    private final FeatureFlags mFeatureFlags;
    private final Optional<Bubbles> mOptionalBubbles;
    private final NoteTaskController mNoteTaskController;
    private final PackageManager mPackageManager;
    private final UserTracker mUserTracker;
    private final UiEventLogger mUiEventLogger;
    private final ResultReceiver mResultReceiver;

    private Intent mKillAppClipsBroadcastIntent;
@@ -86,11 +101,15 @@ public class AppClipsTrampolineActivity extends Activity {
    @Inject
    public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags,
            Optional<Bubbles> optionalBubbles, NoteTaskController noteTaskController,
            PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger,
            @Main Handler mainHandler) {
        mDevicePolicyManager = devicePolicyManager;
        mFeatureFlags = flags;
        mOptionalBubbles = optionalBubbles;
        mNoteTaskController = noteTaskController;
        mPackageManager = packageManager;
        mUserTracker = userTracker;
        mUiEventLogger = uiEventLogger;

        mResultReceiver = createResultReceiver(mainHandler);
    }
@@ -138,8 +157,12 @@ public class AppClipsTrampolineActivity extends Activity {
            return;
        }

        Intent intent = new Intent().setComponent(componentName).addFlags(
                Intent.FLAG_ACTIVITY_NEW_TASK).putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver);
        String callingPackageName = getCallingPackage();
        Intent intent = new Intent().setComponent(componentName)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                .putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver)
                .putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName);

        try {
            // Start the App Clips activity.
            startActivity(intent);
@@ -150,6 +173,9 @@ public class AppClipsTrampolineActivity extends Activity {
                    new Intent(ACTION_FINISH_FROM_TRAMPOLINE)
                            .setComponent(componentName)
                            .setPackage(componentName.getPackageName());

            // Log successful triggering of screenshot for notes.
            logScreenshotTriggeredUiEvent(callingPackageName);
        } catch (ActivityNotFoundException e) {
            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
        }
@@ -170,6 +196,18 @@ public class AppClipsTrampolineActivity extends Activity {
        finish();
    }

    private void logScreenshotTriggeredUiEvent(@Nullable String callingPackageName) {
        int callingPackageUid = 0;
        try {
            callingPackageUid = mPackageManager.getApplicationInfoAsUser(callingPackageName,
                    APPLICATION_INFO_FLAGS, mUserTracker.getUserId()).uid;
        } catch (NameNotFoundException e) {
            Log.d(TAG, "Couldn't find notes app UID " + e);
        }

        mUiEventLogger.log(SCREENSHOT_FOR_NOTE_TRIGGERED, callingPackageUid, callingPackageName);
    }

    private class AppClipsResultReceiver extends ResultReceiver {

        AppClipsResultReceiver(Handler handler) {
+17 −7
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.systemui.screenshot;

import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;

import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;

import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.HardwareRenderer;
@@ -29,6 +31,7 @@ import android.net.Uri;
import android.os.Process;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
@@ -49,7 +52,8 @@ import java.util.concurrent.Executor;
import javax.inject.Inject;

/** A {@link ViewModel} to help with the App Clips screenshot flow. */
final class AppClipsViewModel extends ViewModel {
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public final class AppClipsViewModel extends ViewModel {

    private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
    private final ImageExporter mImageExporter;
@@ -76,7 +80,8 @@ final class AppClipsViewModel extends ViewModel {
    }

    /** Grabs a screenshot and updates the {@link Bitmap} set in screenshot {@link LiveData}. */
    void performScreenshot() {
    @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
    public void performScreenshot() {
        mBgExecutor.execute(() -> {
            Bitmap screenshot = mAppClipsCrossProcessHelper.takeScreenshot();
            mMainExecutor.execute(() -> {
@@ -90,12 +95,14 @@ final class AppClipsViewModel extends ViewModel {
    }

    /** Returns a {@link LiveData} that holds the captured screenshot. */
    LiveData<Bitmap> getScreenshot() {
    @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
    public LiveData<Bitmap> getScreenshot() {
        return mScreenshotLiveData;
    }

    /** Returns a {@link LiveData} that holds the {@link Uri} where screenshot is saved. */
    LiveData<Uri> getResultLiveData() {
    @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
    public LiveData<Uri> getResultLiveData() {
        return mResultLiveData;
    }

@@ -103,7 +110,8 @@ final class AppClipsViewModel extends ViewModel {
     * Returns a {@link LiveData} that holds the error codes for
     * {@link Intent#EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE}.
     */
    LiveData<Integer> getErrorLiveData() {
    @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
    public LiveData<Integer> getErrorLiveData() {
        return mErrorLiveData;
    }

@@ -111,7 +119,8 @@ final class AppClipsViewModel extends ViewModel {
     * Saves the provided {@link Drawable} to storage then informs the result {@link Uri} to
     * {@link LiveData}.
     */
    void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds) {
    @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
    public void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds) {
        mBgExecutor.execute(() -> {
            // Render the screenshot bitmap in background.
            Bitmap screenshotBitmap = renderBitmap(screenshotDrawable, bounds);
@@ -151,7 +160,8 @@ final class AppClipsViewModel extends ViewModel {
    }

    /** Helper factory to help with injecting {@link AppClipsViewModel}. */
    static final class Factory implements ViewModelProvider.Factory {
    @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
    public static final class Factory implements ViewModelProvider.Factory {

        private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
        private final ImageExporter mImageExporter;
+6 −0
Original line number Diff line number Diff line
@@ -165,6 +165,12 @@
            android:exported="false"
            android:permission="com.android.systemui.permission.SELF"
            android:excludeFromRecents="true" />

        <activity
            android:name="com.android.systemui.screenshot.appclips.AppClipsActivityTest$AppClipsActivityTestable"
            android:exported="false"
            android:permission="com.android.systemui.permission.SELF"
            android:excludeFromRecents="true" />
    </application>

    <instrumentation android:name="android.testing.TestableInstrumentation"
Loading