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

Commit 82894ba2 authored by Nicolò Mazzucato's avatar Nicolò Mazzucato Committed by Android (Google) Code Review
Browse files

Merge "Reproduce camera sound only from default display screenshot controller" into main

parents 6c29a6fb 7c4753ca
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
@@ -19,6 +19,12 @@ package com.android.systemui.util
import android.os.Trace
import android.os.TraceNameSupplier
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async

/**
 * Run a block within a [Trace] section. Calls [Trace.beginSection] before and [Trace.endSection]
@@ -85,5 +91,18 @@ class TraceUtils {
                Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, trackName, cookie)
            }
        }

        /**
         * Convenience method to avoid one indentation level when we want to add a trace when
         * launching a coroutine
         */
        fun <T> CoroutineScope.tracedAsync(
            method: String,
            context: CoroutineContext = EmptyCoroutineContext,
            start: CoroutineStart = CoroutineStart.DEFAULT,
            block: suspend () -> T
        ): Deferred<T> {
            return async(context, start) { traceAsync(method) { block() } }
        }
    }
}
+19 −57
Original line number Diff line number Diff line
@@ -53,9 +53,6 @@ import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.media.AudioAttributes;
import android.media.AudioSystem;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Process;
@@ -86,8 +83,6 @@ import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import android.window.WindowContext;

import androidx.concurrent.futures.CallbackToFutureAdapter;

import com.android.internal.app.ChooserActivity;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.policy.PhoneWindow;
@@ -108,7 +103,6 @@ import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;

import java.io.File;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
@@ -116,11 +110,11 @@ import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Supplier;

import javax.inject.Provider;


/**
 * Controls the state and flow for screenshots.
@@ -274,7 +268,8 @@ public class ScreenshotController {
    private final WindowManager mWindowManager;
    private final WindowManager.LayoutParams mWindowLayoutParams;
    private final AccessibilityManager mAccessibilityManager;
    private final ListenableFuture<MediaPlayer> mCameraSound;
    @Nullable
    private final ScreenshotSoundController mScreenshotSoundController;
    private final ScrollCaptureClient mScrollCaptureClient;
    private final PhoneWindow mWindow;
    private final DisplayManager mDisplayManager;
@@ -339,6 +334,7 @@ public class ScreenshotController {
            UserManager userManager,
            AssistContentRequester assistContentRequester,
            MessageContainerController messageContainerController,
            Provider<ScreenshotSoundController> screenshotSoundController,
            @Assisted int displayId
    ) {
        mScreenshotSmartActions = screenshotSmartActions;
@@ -387,8 +383,12 @@ public class ScreenshotController {
        mConfigChanges.applyNewConfig(context.getResources());
        reloadAssets();

        // Setup the Camera shutter sound
        mCameraSound = loadCameraSound();
        // Sound is only reproduced from the controller of the default display.
        if (displayId == Display.DEFAULT_DISPLAY) {
            mScreenshotSoundController = screenshotSoundController.get();
        } else {
            mScreenshotSoundController = null;
        }

        mCopyBroadcastReceiver = new BroadcastReceiver() {
            @Override
@@ -573,17 +573,8 @@ public class ScreenshotController {
    }

    private void releaseMediaPlayer() {
        // Note that this may block if the sound is still being loaded (very unlikely) but we can't
        // reliably release in the background because the service is being destroyed.
        try {
            MediaPlayer player = mCameraSound.get(1, TimeUnit.SECONDS);
            if (player != null) {
                player.release();
            }
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            mCameraSound.cancel(true);
            Log.w(TAG, "Error releasing shutter sound", e);
        }
        if (mScreenshotSoundController == null) return;
        mScreenshotSoundController.releaseScreenshotSound();
    }

    private void respondToKeyDismissal() {
@@ -889,39 +880,10 @@ public class ScreenshotController {
        }
    }

    private ListenableFuture<MediaPlayer> loadCameraSound() {
        // The media player creation is slow and needs on the background thread.
        return CallbackToFutureAdapter.getFuture((completer) -> {
            mBgExecutor.execute(() -> {
                try {
                    MediaPlayer player = MediaPlayer.create(mContext,
                            Uri.fromFile(new File(mContext.getResources().getString(
                                    com.android.internal.R.string.config_cameraShutterSound))),
                            null,
                            new AudioAttributes.Builder()
                                    .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
                                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                                    .build(), AudioSystem.newAudioSessionId());
                    completer.set(player);
                } catch (IllegalStateException e) {
                    Log.w(TAG, "Screenshot sound initialization failed", e);
                    completer.set(null);
                }
            });
            return "ScreenshotController#loadCameraSound";
        });
    }

    private void playCameraSound() {
        mCameraSound.addListener(() -> {
            try {
                MediaPlayer player = mCameraSound.get();
                if (player != null) {
                    player.start();
                }
            } catch (InterruptedException | ExecutionException e) {
            }
        }, mBgExecutor);
    private void playCameraSoundIfNeeded() {
        if (mScreenshotSoundController == null) return;
        // the controller is not-null only on the default display controller
        mScreenshotSoundController.playCameraSound();
    }

    /**
@@ -930,7 +892,7 @@ public class ScreenshotController {
     */
    private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) {
        // Play the shutter sound to notify that we've taken a screenshot
        playCameraSound();
        playCameraSoundIfNeeded();

        saveScreenshotInWorkerThread(
                owner,
@@ -974,7 +936,7 @@ public class ScreenshotController {
        }

        // Play the shutter sound to notify that we've taken a screenshot
        playCameraSound();
        playCameraSoundIfNeeded();

        if (DEBUG_ANIM) {
            Log.d(TAG, "starting post-screenshot animation");
+79 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.media.MediaPlayer
import android.util.Log
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.TraceUtils.Companion.tracedAsync
import com.google.errorprone.annotations.CanIgnoreReturnValue
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout

/** Controls sound reproduction after a screenshot is taken. */
interface ScreenshotSoundController {
    /** Reproduces the camera sound. */
    @CanIgnoreReturnValue fun playCameraSound(): Deferred<Unit>

    /** Releases the sound. [playCameraSound] behaviour is undefined after this has been called. */
    @CanIgnoreReturnValue fun releaseScreenshotSound(): Deferred<Unit>
}

class ScreenshotSoundControllerImpl
@Inject
constructor(
    private val soundProvider: ScreenshotSoundProvider,
    @Application private val coroutineScope: CoroutineScope,
    @Background private val bgDispatcher: CoroutineDispatcher
) : ScreenshotSoundController {

    val player: Deferred<MediaPlayer?> =
        coroutineScope.tracedAsync("loadCameraSound", bgDispatcher) {
            try {
                soundProvider.getScreenshotSound()
            } catch (e: IllegalStateException) {
                Log.w(TAG, "Screenshot sound initialization failed", e)
                null
            }
        }

    override fun playCameraSound(): Deferred<Unit> {
        return coroutineScope.tracedAsync("playCameraSound", bgDispatcher) {
            player.await()?.start()
        }
    }
    override fun releaseScreenshotSound(): Deferred<Unit> {
        return coroutineScope.tracedAsync("releaseScreenshotSound", bgDispatcher) {
            try {
                withTimeout(1.seconds) { player.await()?.release() }
            } catch (e: TimeoutCancellationException) {
                player.cancel()
                Log.w(TAG, "Error releasing shutter sound", e)
            }
        }
    }

    private companion object {
        const val TAG = "ScreenshotSoundControllerImpl"
    }
}
+57 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.content.Context
import android.media.AudioAttributes
import android.media.AudioSystem
import android.media.MediaPlayer
import android.net.Uri
import com.android.internal.R
import com.android.systemui.dagger.SysUISingleton
import java.io.File
import javax.inject.Inject

/** Provides a [MediaPlayer] that reproduces the screenshot sound. */
interface ScreenshotSoundProvider {

    /**
     * Creates a new [MediaPlayer] that reproduces the screenshot sound. This should be called from
     * a background thread, as it might take time.
     */
    fun getScreenshotSound(): MediaPlayer
}

@SysUISingleton
class ScreenshotSoundProviderImpl
@Inject
constructor(
    private val context: Context,
) : ScreenshotSoundProvider {
    override fun getScreenshotSound(): MediaPlayer {
        return MediaPlayer.create(
            context,
            Uri.fromFile(File(context.resources.getString(R.string.config_cameraShutterSound))),
            /* holder = */ null,
            AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .build(),
            AudioSystem.newAudioSessionId()
        )
    }
}
+12 −0
Original line number Diff line number Diff line
@@ -25,6 +25,10 @@ import com.android.systemui.screenshot.ScreenshotPolicy;
import com.android.systemui.screenshot.ScreenshotPolicyImpl;
import com.android.systemui.screenshot.ScreenshotProxyService;
import com.android.systemui.screenshot.ScreenshotRequestProcessor;
import com.android.systemui.screenshot.ScreenshotSoundController;
import com.android.systemui.screenshot.ScreenshotSoundControllerImpl;
import com.android.systemui.screenshot.ScreenshotSoundProvider;
import com.android.systemui.screenshot.ScreenshotSoundProviderImpl;
import com.android.systemui.screenshot.TakeScreenshotService;
import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
import com.android.systemui.screenshot.appclips.AppClipsService;
@@ -69,4 +73,12 @@ public abstract class ScreenshotModule {
    @Binds
    abstract ScreenshotRequestProcessor bindScreenshotRequestProcessor(
            RequestProcessor requestProcessor);

    @Binds
    abstract ScreenshotSoundProvider bindScreenshotSoundProvider(
            ScreenshotSoundProviderImpl screenshotSoundProviderImpl);

    @Binds
    abstract ScreenshotSoundController bindScreenshotSoundController(
            ScreenshotSoundControllerImpl screenshotSoundProviderImpl);
}
Loading