diff options
84 files changed, 2256 insertions, 317 deletions
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index c95d6ffe6268..a23df799da59 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -951,7 +951,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall private boolean performSurfaceTransaction(ViewRootImpl viewRoot, Translator translator, boolean creating, boolean sizeChanged, boolean hintChanged, boolean relativeZChanged, - Transaction surfaceUpdateTransaction) { + boolean hdrHeadroomChanged, Transaction surfaceUpdateTransaction) { boolean realSizeChanged = false; mSurfaceLock.lock(); @@ -986,7 +986,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall updateBackgroundVisibility(surfaceUpdateTransaction); updateBackgroundColor(surfaceUpdateTransaction); - if (mLimitedHdrEnabled) { + if (mLimitedHdrEnabled && hdrHeadroomChanged) { surfaceUpdateTransaction.setDesiredHdrHeadroom( mBlastSurfaceControl, mHdrHeadroom); } @@ -1203,7 +1203,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall } final boolean realSizeChanged = performSurfaceTransaction(viewRoot, translator, - creating, sizeChanged, hintChanged, relativeZChanged, + creating, sizeChanged, hintChanged, relativeZChanged, hdrHeadroomChanged, surfaceUpdateTransaction); try { diff --git a/core/jni/com_android_internal_content_FileSystemUtils.cpp b/core/jni/com_android_internal_content_FileSystemUtils.cpp index 4bd2d72a1eb4..01920de88496 100644 --- a/core/jni/com_android_internal_content_FileSystemUtils.cpp +++ b/core/jni/com_android_internal_content_FileSystemUtils.cpp @@ -42,7 +42,11 @@ namespace android { bool punchHoles(const char *filePath, const uint64_t offset, const std::vector<Elf64_Phdr> &programHeaders) { struct stat64 beforePunch; - lstat64(filePath, &beforePunch); + if (int result = lstat64(filePath, &beforePunch); result != 0) { + ALOGE("lstat64 failed for filePath %s, error:%d", filePath, errno); + return false; + } + uint64_t blockSize = beforePunch.st_blksize; IF_ALOGD() { ALOGD("Total number of LOAD segments %zu", programHeaders.size()); @@ -152,7 +156,10 @@ bool punchHoles(const char *filePath, const uint64_t offset, IF_ALOGD() { struct stat64 afterPunch; - lstat64(filePath, &afterPunch); + if (int result = lstat64(filePath, &afterPunch); result != 0) { + ALOGD("lstat64 failed for filePath %s, error:%d", filePath, errno); + return false; + } ALOGD("Size after punching holes st_blocks: %" PRIu64 ", st_blksize: %ld, st_size: %" PRIu64 "", afterPunch.st_blocks, afterPunch.st_blksize, @@ -177,7 +184,7 @@ bool punchHolesInElf64(const char *filePath, const uint64_t offset) { // only consider elf64 for punching holes if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) { - ALOGE("Provided file is not ELF64"); + ALOGW("Provided file is not ELF64"); return false; } @@ -215,4 +222,108 @@ bool punchHolesInElf64(const char *filePath, const uint64_t offset) { return punchHoles(filePath, offset, programHeaders); } +bool punchHolesInZip(const char *filePath, uint64_t offset, uint16_t extraFieldLen) { + android::base::unique_fd fd(open(filePath, O_RDWR | O_CLOEXEC)); + if (!fd.ok()) { + ALOGE("Can't open file to punch %s", filePath); + return false; + } + + struct stat64 beforePunch; + if (int result = lstat64(filePath, &beforePunch); result != 0) { + ALOGE("lstat64 failed for filePath %s, error:%d", filePath, errno); + return false; + } + + uint64_t blockSize = beforePunch.st_blksize; + IF_ALOGD() { + ALOGD("Extra field length: %hu, Size before punching holes st_blocks: %" PRIu64 + ", st_blksize: %ld, st_size: %" PRIu64 "", + extraFieldLen, beforePunch.st_blocks, beforePunch.st_blksize, + static_cast<uint64_t>(beforePunch.st_size)); + } + + if (extraFieldLen < blockSize) { + ALOGD("Skipping punching apk as extra field length is less than block size"); + return false; + } + + // content is preceded by extra field. Zip offset is offset of exact content. + // move back by extraFieldLen so that scan can be started at start of extra field. + uint64_t extraFieldStart; + if (__builtin_sub_overflow(offset, extraFieldLen, &extraFieldStart)) { + ALOGE("Overflow occurred when calculating start of extra field"); + return false; + } + + constexpr uint64_t kMaxSize = 64 * 1024; + // Use malloc to gracefully handle any oom conditions + std::unique_ptr<uint8_t, decltype(&free)> buffer(static_cast<uint8_t *>(malloc(kMaxSize)), + &free); + if (buffer == nullptr) { + ALOGE("Failed to allocate read buffer"); + return false; + } + + // Read the entire extra fields at once and punch file according to zero stretches. + if (!ReadFullyAtOffset(fd, buffer.get(), extraFieldLen, extraFieldStart)) { + ALOGE("Failed to read extra field content"); + return false; + } + + IF_ALOGD() { + ALOGD("Extra field length: %hu content near offset: %s", extraFieldLen, + HexString(buffer.get(), extraFieldLen).c_str()); + } + + uint64_t currentSize = 0; + while (currentSize < extraFieldLen) { + uint64_t end = currentSize; + // find zero ranges + while (end < extraFieldLen && *(buffer.get() + end) == 0) { + ++end; + } + + uint64_t punchLen; + if (__builtin_sub_overflow(end, currentSize, &punchLen)) { + ALOGW("Overflow occurred when calculating punching length"); + return false; + } + + // Don't punch for every stretch of zero which is found + if (punchLen > blockSize) { + uint64_t punchOffset; + if (__builtin_add_overflow(extraFieldStart, currentSize, &punchOffset)) { + ALOGW("Overflow occurred when calculating punch start offset"); + return false; + } + + ALOGD("Punching hole in apk start: %" PRIu64 " len:%" PRIu64 "", punchOffset, punchLen); + + // Punch hole for this entire stretch. + int result = fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, punchOffset, + punchLen); + if (result < 0) { + ALOGE("fallocate failed to punch hole inside apk, error:%d", errno); + return false; + } + } + currentSize = end; + ++currentSize; + } + + IF_ALOGD() { + struct stat64 afterPunch; + if (int result = lstat64(filePath, &afterPunch); result != 0) { + ALOGD("lstat64 failed for filePath %s, error:%d", filePath, errno); + return false; + } + ALOGD("punchHolesInApk:: Size after punching holes st_blocks: %" PRIu64 + ", st_blksize: %ld, st_size: %" PRIu64 "", + afterPunch.st_blocks, afterPunch.st_blksize, + static_cast<uint64_t>(afterPunch.st_size)); + } + return true; +} + }; // namespace android diff --git a/core/jni/com_android_internal_content_FileSystemUtils.h b/core/jni/com_android_internal_content_FileSystemUtils.h index a6b145c690d1..52445e2b4229 100644 --- a/core/jni/com_android_internal_content_FileSystemUtils.h +++ b/core/jni/com_android_internal_content_FileSystemUtils.h @@ -28,4 +28,11 @@ namespace android { */ bool punchHolesInElf64(const char* filePath, uint64_t offset); +/* + * This function punches holes in zero segments of Apk file which are introduced during the + * alignment. Alignment tools add padding inside of extra field in local file header. punch holes in + * extra field for zero stretches till the actual file content. + */ +bool punchHolesInZip(const char* filePath, uint64_t offset, uint16_t extraFieldLen); + } // namespace android
\ No newline at end of file diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp index faa83f8017f7..9b8dab78b342 100644 --- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp +++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp @@ -28,6 +28,7 @@ #include <stdlib.h> #include <string.h> #include <sys/stat.h> +#include <sys/statfs.h> #include <sys/types.h> #include <time.h> #include <unistd.h> @@ -145,8 +146,9 @@ copyFileIfChanged(JNIEnv *env, void* arg, ZipFileRO* zipFile, ZipEntryRO zipEntr uint16_t method; off64_t offset; - - if (!zipFile->getEntryInfo(zipEntry, &method, &uncompLen, nullptr, &offset, &when, &crc)) { + uint16_t extraFieldLength; + if (!zipFile->getEntryInfo(zipEntry, &method, &uncompLen, nullptr, &offset, &when, &crc, + &extraFieldLength)) { ALOGE("Couldn't read zip entry info\n"); return INSTALL_FAILED_INVALID_APK; } @@ -177,6 +179,12 @@ copyFileIfChanged(JNIEnv *env, void* arg, ZipFileRO* zipFile, ZipEntryRO zipEntr "%" PRIu64 "", fileName, zipFile->getZipFileName(), offset); } + + // if extra field for this zip file is present with some length, possibility is that it is + // padding added for zip alignment. Punch holes there too. + if (!punchHolesInZip(zipFile->getZipFileName(), offset, extraFieldLength)) { + ALOGW("Failed to punch apk : %s at extra field", zipFile->getZipFileName()); + } #endif // ENABLE_PUNCH_HOLES return INSTALL_SUCCEEDED; @@ -279,6 +287,25 @@ copyFileIfChanged(JNIEnv *env, void* arg, ZipFileRO* zipFile, ZipEntryRO zipEntr return INSTALL_FAILED_CONTAINER_ERROR; } +#ifdef ENABLE_PUNCH_HOLES + // punch extracted elf files as well. This will fail where compression is on (like f2fs) but it + // will be useful for ext4 based systems + struct statfs64 fsInfo; + int result = statfs64(localFileName, &fsInfo); + if (result < 0) { + ALOGW("Failed to stat file :%s", localFileName); + } + + if (result == 0 && fsInfo.f_type == EXT4_SUPER_MAGIC) { + ALOGD("Punching extracted elf file %s on fs:%" PRIu64 "", fileName, + static_cast<uint64_t>(fsInfo.f_type)); + if (!punchHolesInElf64(localFileName, 0)) { + ALOGW("Failed to punch extracted elf file :%s from apk : %s", fileName, + zipFile->getZipFileName()); + } + } +#endif // ENABLE_PUNCH_HOLES + ALOGV("Successfully moved %s to %s\n", localTmpFileName, localFileName); return INSTALL_SUCCEEDED; diff --git a/libs/androidfw/ZipFileRO.cpp b/libs/androidfw/ZipFileRO.cpp index 34a6bc27b93f..839c7b6fef37 100644 --- a/libs/androidfw/ZipFileRO.cpp +++ b/libs/androidfw/ZipFileRO.cpp @@ -119,30 +119,41 @@ ZipEntryRO ZipFileRO::findEntryByName(const char* entryName) const * appear to be bogus. */ bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, + uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, + uint32_t* pModWhen, uint32_t* pCrc32) const +{ + return getEntryInfo(entry, pMethod, pUncompLen, pCompLen, pOffset, pModWhen, + pCrc32, nullptr); +} + +bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, - uint32_t* pModWhen, uint32_t* pCrc32) const + uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const { const _ZipEntryRO* zipEntry = reinterpret_cast<_ZipEntryRO*>(entry); const ZipEntry& ze = zipEntry->entry; - if (pMethod != NULL) { + if (pMethod != nullptr) { *pMethod = ze.method; } - if (pUncompLen != NULL) { + if (pUncompLen != nullptr) { *pUncompLen = ze.uncompressed_length; } - if (pCompLen != NULL) { + if (pCompLen != nullptr) { *pCompLen = ze.compressed_length; } - if (pOffset != NULL) { + if (pOffset != nullptr) { *pOffset = ze.offset; } - if (pModWhen != NULL) { + if (pModWhen != nullptr) { *pModWhen = ze.mod_time; } - if (pCrc32 != NULL) { + if (pCrc32 != nullptr) { *pCrc32 = ze.crc32; } + if (pExtraFieldSize != nullptr) { + *pExtraFieldSize = ze.extra_field_size; + } return true; } diff --git a/libs/androidfw/include/androidfw/ZipFileRO.h b/libs/androidfw/include/androidfw/ZipFileRO.h index 031d2e8fd48f..f7c5007c80d2 100644 --- a/libs/androidfw/include/androidfw/ZipFileRO.h +++ b/libs/androidfw/include/androidfw/ZipFileRO.h @@ -151,6 +151,10 @@ public: uint32_t* pCompLen, off64_t* pOffset, uint32_t* pModWhen, uint32_t* pCrc32) const; + bool getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, + uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, + uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const; + /* * Create a new FileMap object that maps a subset of the archive. For * an uncompressed entry this effectively provides a pointer to the diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 34932b1b1e25..dc669a5eca73 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -24,7 +24,6 @@ #include <SkImageAndroid.h> #include <SkImageInfo.h> #include <SkMatrix.h> -#include <SkMultiPictureDocument.h> #include <SkOverdrawCanvas.h> #include <SkOverdrawColorFilter.h> #include <SkPicture.h> @@ -38,6 +37,7 @@ #include <android-base/properties.h> #include <gui/TraceUtils.h> #include <include/android/SkSurfaceAndroid.h> +#include <include/docs/SkMultiPictureDocument.h> #include <include/encode/SkPngEncoder.h> #include <include/gpu/ganesh/SkSurfaceGanesh.h> #include <unistd.h> @@ -185,7 +185,7 @@ bool SkiaPipeline::setupMultiFrameCapture() { // we need to keep it until after mMultiPic.close() // procs is passed as a pointer, but just as a method of having an optional default. // procs doesn't need to outlive this Make call. - mMultiPic = SkMakeMultiPictureDocument(mOpenMultiPicStream.get(), &procs, + mMultiPic = SkMultiPictureDocument::Make(mOpenMultiPicStream.get(), &procs, [sharingCtx = mSerialContext.get()](const SkPicture* pic) { SkSharingSerialContext::collectNonTextureImagesFromPicture(pic, sharingCtx); }); diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index cf14b1f9ebe3..823b209017a5 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -18,7 +18,6 @@ #include <SkColorSpace.h> #include <SkDocument.h> -#include <SkMultiPictureDocument.h> #include <SkSurface.h> #include "Lighting.h" diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp index b62711f50c94..21fe6ff14f56 100644 --- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp @@ -16,10 +16,10 @@ #include "VkFunctorDrawable.h" -#include <GrBackendDrawableInfo.h> #include <SkAndroidFrameworkUtils.h> #include <SkImage.h> #include <SkM44.h> +#include <include/gpu/ganesh/vk/GrBackendDrawableInfo.h> #include <gui/TraceUtils.h> #include <private/hwui/DrawVkInfo.h> #include <utils/Color.h> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 36f6ad2f386d..429bdbf5959b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -270,6 +270,15 @@ class CredentialSelectorViewModel( ) } + fun getFlowOnMoreOptionOnlySelected() { + Log.d(Constants.LOG_TAG, "More Option Only selected") + uiState = uiState.copy( + getCredentialUiState = uiState.getCredentialUiState?.copy( + currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY + ) + ) + } + fun getFlowOnMoreOptionOnSnackBarSelected(isNoAccount: Boolean) { Log.d(Constants.LOG_TAG, "More Option on snackBar selected") uiState = uiState.copy( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt index e088d3addaf2..aa721c9f6e13 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt @@ -20,6 +20,7 @@ import android.content.Context import android.graphics.Bitmap import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.BiometricPrompt +import android.hardware.biometrics.CryptoObject import android.os.CancellationSignal import android.util.Log import androidx.core.content.ContextCompat.getMainExecutor @@ -221,13 +222,24 @@ private fun runBiometricFlow( val executor = getMainExecutor(context) try { - biometricPrompt.authenticate(cancellationSignal, executor, callback) + val cryptoOpId = getCryptoOpId(biometricDisplayInfo) + if (cryptoOpId != null) { + biometricPrompt.authenticate( + BiometricPrompt.CryptoObject(cryptoOpId.toLong()), + cancellationSignal, executor, callback) + } else { + biometricPrompt.authenticate(cancellationSignal, executor, callback) + } } catch (e: IllegalArgumentException) { Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n") onBiometricFailureFallback(biometricFlowType) } } +private fun getCryptoOpId(biometricDisplayInfo: BiometricDisplayInfo): Int? { + return biometricDisplayInfo.biometricRequestInfo.opId +} + /** * Sets up the biometric prompt with the UI specific bits. * // TODO(b/333445112) : Pass in opId once dependency is confirmed via CryptoObject diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt index d13d86fccc97..149c14a24085 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -349,6 +349,38 @@ fun MoreOptionTopAppBar( } } +@Composable +fun MoreOptionTopAppBarWithCustomNavigation( + text: String, + onNavigationIconClicked: () -> Unit, + navigationIcon: ImageVector, + navigationIconContentDescription: String, + bottomPadding: Dp, +) { + Row( + modifier = Modifier.padding(top = 12.dp, bottom = bottomPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + modifier = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 4.dp).size(48.dp), + onClick = onNavigationIconClicked + ) { + Box( + modifier = Modifier.size(48.dp), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = navigationIcon, + contentDescription = navigationIconContentDescription, + modifier = Modifier.size(24.dp).autoMirrored(), + tint = LocalAndroidColorScheme.current.onSurfaceVariant, + ) + } + } + LargeTitleText(text = text, modifier = Modifier.padding(horizontal = 4.dp)) + } +} + private fun Modifier.autoMirrored() = composed { when (LocalLayoutDirection.current) { LayoutDirection.Rtl -> graphicsLayer(scaleX = -1f) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index c1120bb356b7..e68baf48475f 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.outlined.QrCodeScanner import androidx.compose.material3.Divider import androidx.compose.material3.TextButton @@ -70,6 +71,7 @@ import com.android.credentialmanager.common.ui.HeadlineText import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant import com.android.credentialmanager.common.ui.ModalBottomSheet import com.android.credentialmanager.common.ui.MoreOptionTopAppBar +import com.android.credentialmanager.common.ui.MoreOptionTopAppBarWithCustomNavigation import com.android.credentialmanager.common.ui.SheetContainerCard import com.android.credentialmanager.common.ui.Snackbar import com.android.credentialmanager.common.ui.SnackbarActionText @@ -148,7 +150,7 @@ fun GetCredentialScreen( .currentScreenState == GetScreenState.BIOMETRIC_SELECTION) { BiometricSelectionPage( biometricEntry = getCredentialUiState.activeEntry, - onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected, + onMoreOptionSelected = viewModel::getFlowOnMoreOptionOnlySelected, onCancelFlowAndFinish = viewModel::onUserCancel, onIllegalStateAndFinish = viewModel::onIllegalUiState, requestDisplayInfo = getCredentialUiState.requestDisplayInfo, @@ -163,6 +165,28 @@ fun GetCredentialScreen( onBiometricPromptStateChange = viewModel::onBiometricPromptStateChange ) + } else if (credmanBiometricApiEnabled() && + getCredentialUiState.currentScreenState + == GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY) { + AllSignInOptionCard( + providerInfoList = getCredentialUiState.providerInfoList, + providerDisplayInfo = getCredentialUiState.providerDisplayInfo, + onEntrySelected = viewModel::getFlowOnEntrySelected, + onBackButtonClicked = viewModel::onUserCancel, + onCancel = viewModel::onUserCancel, + onLog = { viewModel.logUiEvent(it) }, + customTopBar = { MoreOptionTopAppBarWithCustomNavigation( + text = stringResource( + R.string.get_dialog_title_sign_in_options), + onNavigationIconClicked = viewModel::onUserCancel, + navigationIcon = Icons.Filled.Close, + navigationIconContentDescription = + stringResource(R.string.accessibility_close_button), + bottomPadding = 0.dp + ) } + ) + viewModel.uiMetrics.log(GetCredentialEvent + .CREDMAN_GET_CRED_SCREEN_ALL_SIGN_IN_OPTIONS) } else { AllSignInOptionCard( providerInfoList = getCredentialUiState.providerInfoList, @@ -642,7 +666,13 @@ private fun findSingleProviderIdForPrimaryPage( return providerId } -/** Draws the secondary credential selection page, where all sign-in options are listed. */ +/** + * Draws the secondary credential selection page, where all sign-in options are listed. + * + * By default, this card has 'back' navigation whereby user can navigate back to invoke + * [onBackButtonClicked]. However if a different top bar with possibly a different navigation + * is required, then the caller of this Composable can set a [customTopBar]. + */ @Composable fun AllSignInOptionCard( providerInfoList: List<ProviderInfo>, @@ -651,16 +681,21 @@ fun AllSignInOptionCard( onBackButtonClicked: () -> Unit, onCancel: () -> Unit, onLog: @Composable (UiEventEnum) -> Unit, + customTopBar: (@Composable() () -> Unit)? = null ) { val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList val authenticationEntryList = providerDisplayInfo.authenticationEntryList SheetContainerCard(topAppBar = { - MoreOptionTopAppBar( - text = stringResource(R.string.get_dialog_title_sign_in_options), - onNavigationIconClicked = onBackButtonClicked, - bottomPadding = 0.dp, - ) + if (customTopBar != null) { + customTopBar() + } else { + MoreOptionTopAppBar( + text = stringResource(R.string.get_dialog_title_sign_in_options), + onNavigationIconClicked = onBackButtonClicked, + bottomPadding = 0.dp, + ) + } }) { var isFirstSection = true // For username diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index b03407b9ebea..8e7886119a34 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -163,7 +163,11 @@ enum class GetScreenState { /** The single tap biometric selection page. */ BIOMETRIC_SELECTION, - /** The secondary credential selection page, where all sign-in options are listed. */ + /** + * The secondary credential selection page, where all sign-in options are listed. + * + * This state is expected to go back to PRIMARY_SELECTION on back navigation + */ ALL_SIGN_IN_OPTIONS, /** The snackbar only page when there's no account but only a remoteEntry. */ @@ -171,6 +175,14 @@ enum class GetScreenState { /** The snackbar when there are only auth entries and all of them turn out to be empty. */ UNLOCKED_AUTH_ENTRIES_ONLY, + + /** + * The secondary credential selection page, where all sign-in options are listed. + * + * This state has no option for the user to navigate back to PRIMARY_SELECTION, and + * instead can be terminated independently. + */ + ALL_SIGN_IN_OPTIONS_ONLY, } @@ -285,7 +297,7 @@ private fun toGetScreenState( providerDisplayInfo.remoteEntry != null) GetScreenState.REMOTE_ONLY else if (isRequestForAllOptions) - GetScreenState.ALL_SIGN_IN_OPTIONS + GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY else if (isBiometricFlow(providerDisplayInfo, isFlowAutoSelectable(providerDisplayInfo))) GetScreenState.BIOMETRIC_SELECTION else GetScreenState.PRIMARY_SELECTION diff --git a/packages/PrintSpooler/res/values-night/themes.xml b/packages/PrintSpooler/res/values-night/themes.xml index 4428dbbbaaae..3cc64a6ef266 100644 --- a/packages/PrintSpooler/res/values-night/themes.xml +++ b/packages/PrintSpooler/res/values-night/themes.xml @@ -30,6 +30,7 @@ <item name="android:windowIsTranslucent">true</item> <item name="android:windowActionBar">false</item> <item name="android:windowNoTitle">true</item> + <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> </style> </resources> diff --git a/packages/PrintSpooler/res/values/themes.xml b/packages/PrintSpooler/res/values/themes.xml index 4dcad10c793c..bd9602540878 100644 --- a/packages/PrintSpooler/res/values/themes.xml +++ b/packages/PrintSpooler/res/values/themes.xml @@ -31,6 +31,7 @@ <item name="android:windowActionBar">false</item> <item name="android:windowNoTitle">true</item> <item name="android:windowLightStatusBar">true</item> + <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> </style> </resources> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java index ad3eb92b0519..e77cf2fa6543 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java @@ -531,13 +531,22 @@ public final class DeviceConfigService extends Binder { pw.println(" put NAMESPACE KEY VALUE [default]"); pw.println(" Change the contents of KEY to VALUE for the given NAMESPACE."); pw.println(" {default} to set as the default value."); + pw.println(" override NAMESPACE KEY VALUE"); + pw.println(" Set flag NAMESPACE/KEY to the given VALUE, and ignores " + + "server-updates for"); + pw.println(" this flag. This can still be called even if there is no underlying " + + "value set."); pw.println(" delete NAMESPACE KEY"); pw.println(" Delete the entry for KEY for the given NAMESPACE."); + pw.println(" clear_override NAMESPACE KEY"); + pw.println(" Clear local sticky flag override for KEY in the given NAMESPACE."); pw.println(" list_namespaces [--public]"); pw.println(" Prints the name of all (or just the public) namespaces."); pw.println(" list [NAMESPACE]"); pw.println(" Print all keys and values defined, optionally for the given " + "NAMESPACE."); + pw.println(" list_local_overrides"); + pw.println(" Print all flags that have been overridden."); pw.println(" reset RESET_MODE [NAMESPACE]"); pw.println(" Reset all flag values, optionally for a NAMESPACE, according to " + "RESET_MODE."); @@ -547,8 +556,9 @@ public final class DeviceConfigService extends Binder { + "flags are reset"); pw.println(" set_sync_disabled_for_tests SYNC_DISABLED_MODE"); pw.println(" Modifies bulk property setting behavior for tests. When in one of the" - + " disabled modes this ensures that config isn't overwritten."); - pw.println(" SYNC_DISABLED_MODE is one of:"); + + " disabled modes"); + pw.println(" this ensures that config isn't overwritten. SYNC_DISABLED_MODE is " + + "one of:"); pw.println(" none: Sync is not disabled. A reboot may be required to restart" + " syncing."); pw.println(" persistent: Sync is disabled, this state will survive a reboot."); diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 40db52eec81b..c88c3731aa10 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -117,6 +117,7 @@ android_library { "SystemUILogLib", "SystemUIPluginLib", "SystemUISharedLib", + "SystemUI-shared-utils", "SystemUI-statsd", "SettingsLib", "com_android_systemui_flags_lib", @@ -263,6 +264,7 @@ android_library { "SystemUISharedLib", "SystemUICustomizationLib", "SystemUICustomizationTestUtils", + "SystemUI-shared-utils", "SystemUI-statsd", "SettingsLib", "com_android_systemui_flags_lib", diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/UnfoldModifiers.kt b/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/UnfoldModifiers.kt new file mode 100644 index 000000000000..c2a2696777e5 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/UnfoldModifiers.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 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.fold.ui.composable + +import androidx.annotation.FloatRange +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import com.android.compose.modifiers.padding +import kotlin.math.roundToInt + +/** + * Applies a translation that feeds off of the unfold transition that's active while the device is + * being folded or unfolded, effectively shifting the element towards the fold hinge. + * + * @param startSide `true` if the affected element is on the start side (left-hand side in + * left-to-right layouts), `false` otherwise. + * @param fullTranslation The maximum translation to apply when the element is the most shifted. The + * modifier will never apply more than this much translation on the element. + * @param unfoldProgress A provider for the amount of progress of the unfold transition. This should + * be sourced from the `UnfoldTransitionInteractor`, ideally through a view-model. + */ +@Composable +fun Modifier.unfoldTranslation( + startSide: Boolean, + fullTranslation: Dp, + @FloatRange(from = 0.0, to = 1.0) unfoldProgress: () -> Float, +): Modifier { + val translateToTheRight = startSide && LocalLayoutDirection.current == LayoutDirection.Ltr + return this.graphicsLayer { + translationX = + fullTranslation.toPx() * + if (translateToTheRight) { + 1 - unfoldProgress() + } else { + unfoldProgress() - 1 + } + } +} + +/** + * Applies horizontal padding that feeds off of the unfold transition that's active while the device + * is being folded or unfolded, effectively "squishing" the element on both sides. + * + * This is horizontal padding so it's applied on both the start and end sides of the element. + * + * @param fullPadding The maximum padding to apply when the element is the most padded. The modifier + * will never apply more than this much horizontal padding on the element. + * @param unfoldProgress A provider for the amount of progress of the unfold transition. This should + * be sourced from the `UnfoldTransitionInteractor`, ideally through a view-model. + */ +@Composable +fun Modifier.unfoldHorizontalPadding( + fullPadding: Dp, + @FloatRange(from = 0.0, to = 1.0) unfoldProgress: () -> Float, +): Modifier { + return this.padding( + horizontal = { (fullPadding.toPx() * (1 - unfoldProgress())).roundToInt() }, + ) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 9bd6f817cff3..01c27a4dcc2a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -65,6 +65,8 @@ import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.modifiers.thenIf import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.fold.ui.composable.unfoldHorizontalPadding +import com.android.systemui.fold.ui.composable.unfoldTranslation import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.controller.MediaCarouselController import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager @@ -289,6 +291,7 @@ private fun SceneScope.SplitShade( remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) } val tileSquishiness by animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness) + val unfoldTransitionProgress by viewModel.unfoldTransitionProgress.collectAsState() val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val density = LocalDensity.current @@ -337,10 +340,23 @@ private fun SceneScope.SplitShade( modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding) .then(brightnessMirrorShowingModifier) + .unfoldHorizontalPadding( + fullPadding = dimensionResource(R.dimen.notification_side_paddings), + ) { + unfoldTransitionProgress + } ) Row(modifier = Modifier.fillMaxWidth().weight(1f)) { - Box(modifier = Modifier.weight(1f)) { + Box( + modifier = + Modifier.weight(1f).unfoldTranslation( + startSide = true, + fullTranslation = dimensionResource(R.dimen.notification_side_paddings), + ) { + unfoldTransitionProgress + }, + ) { BrightnessMirror( viewModel = viewModel.brightnessMirrorViewModel, qsSceneAdapter = viewModel.qsSceneAdapter, @@ -407,7 +423,16 @@ private fun SceneScope.SplitShade( Modifier.weight(1f) .fillMaxHeight() .padding(bottom = navBarBottomHeight) - .then(brightnessMirrorShowingModifier), + .then(brightnessMirrorShowingModifier) + .unfoldTranslation( + startSide = false, + fullTranslation = + dimensionResource( + R.dimen.notification_side_paddings, + ), + ) { + unfoldTransitionProgress + }, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index 85774c67bccb..60b48f28fc23 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -571,7 +571,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { // THEN the view layout is never updated verify(windowManager, never()).updateViewLayout(any(), any()) - // CLEANUPL we hide to end the job that listens for the finishedGoingToSleep signal + // CLEANUP we hide to end the job that listens for the finishedGoingToSleep signal controllerOverlay.hide() } } @@ -595,7 +595,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { controllerOverlay.updateOverlayParams(overlayParams) // THEN the view layout is updated - verify(windowManager, never()).updateViewLayout(any(), any()) + verify(windowManager).updateViewLayout(any(), any()) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 470d342402e1..65fd1010ec38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -85,6 +85,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobile import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.telephony.data.repository.fakeTelephonyRepository import com.android.systemui.testKosmos +import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -228,6 +229,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { footerActionsController = kosmos.footerActionsController, footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory, sceneInteractor = sceneInteractor, + unfoldTransitionInteractor = kosmos.unfoldTransitionInteractor, ) val displayTracker = FakeDisplayTracker(context) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index ab95e2c2e449..2727af64733c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -44,6 +44,8 @@ import com.android.systemui.shade.domain.startable.shadeStartable import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos +import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor +import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -91,6 +93,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory, footerActionsController = kosmos.footerActionsController, sceneInteractor = kosmos.sceneInteractor, + unfoldTransitionInteractor = kosmos.unfoldTransitionInteractor, ) } @@ -254,4 +257,26 @@ class ShadeSceneViewModelTest : SysuiTestCase() { shadeRepository.setShadeMode(ShadeMode.Split) assertThat(shadeMode).isEqualTo(ShadeMode.Split) } + + @Test + fun unfoldTransitionProgress() = + testScope.runTest { + val unfoldProvider = kosmos.fakeUnfoldTransitionProgressProvider + val progress by collectLastValue(underTest.unfoldTransitionProgress) + + unfoldProvider.onTransitionStarted() + assertThat(progress).isEqualTo(1f) + + repeat(10) { repetition -> + val transitionProgress = 0.1f * (repetition + 1) + unfoldProvider.onTransitionProgress(transitionProgress) + assertThat(progress).isEqualTo(transitionProgress) + } + + unfoldProvider.onTransitionFinishing() + assertThat(progress).isEqualTo(1f) + + unfoldProvider.onTransitionFinished() + assertThat(progress).isEqualTo(1f) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt index 6a801e01a4a5..3b4cce448da9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt @@ -15,42 +15,31 @@ */ package com.android.systemui.unfold.domain.interactor -import android.testing.AndroidTestingRunner +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.unfold.TestUnfoldTransitionProvider -import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider import com.google.common.truth.Truth.assertThat -import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest -@RunWith(AndroidTestingRunner::class) -open class UnfoldTransitionInteractorTest : SysuiTestCase() { +@RunWith(AndroidJUnit4::class) +class UnfoldTransitionInteractorTest : SysuiTestCase() { - private val testScope = TestScope() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val unfoldTransitionProgressProvider = kosmos.fakeUnfoldTransitionProgressProvider - private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider() - private val unfoldTransitionRepository = - UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider)) - - private lateinit var underTest: UnfoldTransitionInteractor - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - - underTest = UnfoldTransitionInteractorImpl(unfoldTransitionRepository) - } + private val underTest: UnfoldTransitionInteractor = kosmos.unfoldTransitionInteractor @Test fun waitForTransitionFinish_noEvents_doesNotComplete() = @@ -88,4 +77,26 @@ open class UnfoldTransitionInteractorTest : SysuiTestCase() { assertThat(deferred.isCompleted).isFalse() deferred.cancel() } + + @Test + fun unfoldProgress() = + testScope.runTest { + val progress by collectLastValue(underTest.unfoldProgress) + runCurrent() + + unfoldTransitionProgressProvider.onTransitionStarted() + assertThat(progress).isEqualTo(1f) + + repeat(10) { repetition -> + val transitionProgress = 0.1f * (repetition + 1) + unfoldTransitionProgressProvider.onTransitionProgress(transitionProgress) + assertThat(progress).isEqualTo(transitionProgress) + } + + unfoldTransitionProgressProvider.onTransitionFinishing() + assertThat(progress).isEqualTo(1f) + + unfoldTransitionProgressProvider.onTransitionFinished() + assertThat(progress).isEqualTo(1f) + } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt index 8e2bd9b2562b..79bf5f19997b 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt @@ -267,6 +267,9 @@ data class ClockConfig( /** True if the clock will react to tone changes in the seed color. */ val isReactiveToTone: Boolean = true, + + /** True if the clock is large frame clock, which will use weather in compose. */ + val useCustomClockScene: Boolean = false, ) /** Render configuration options for a clock face. Modifies the way SystemUI behaves. */ @@ -283,6 +286,9 @@ data class ClockFaceConfig( * animation will be used (e.g. a simple translation). */ val hasCustomPositionUpdatedAnimation: Boolean = false, + + /** True if the clock is large frame clock, which will use weatherBlueprint in compose. */ + val useCustomClockScene: Boolean = false, ) /** Structure for keeping clock-specific settings */ diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 61d1c713fb77..4a60d195ea36 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -323,7 +323,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( overlayParams = updatedOverlayParams sensorBounds = updatedOverlayParams.sensorBounds getTouchOverlay()?.let { - if (addViewRunnable != null) { + if (addViewRunnable == null) { // Only updateViewLayout if there's no pending view to add to WM. // If there is a pending view, that means the view hasn't been added yet so there's // no need to update any layouts. Instead the correct params will be used when the diff --git a/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt b/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt index d4a1f74234ef..0c181e99b21c 100644 --- a/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt @@ -16,14 +16,14 @@ package com.android.systemui.common.coroutine +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow as wrapped import kotlin.experimental.ExperimentalTypeInference -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.callbackFlow +@Deprecated("Use com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow instead") object ConflatedCallbackFlow { /** @@ -32,9 +32,15 @@ object ConflatedCallbackFlow { * consumer(s) of the values in the flow), the values are buffered and, if the buffer fills up, * we drop the oldest values automatically instead of suspending the producer. */ - @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") - @OptIn(ExperimentalTypeInference::class, ExperimentalCoroutinesApi::class) + @Deprecated( + "Use com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow instead", + ReplaceWith( + "conflatedCallbackFlow", + "com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow" + ) + ) + @OptIn(ExperimentalTypeInference::class) fun <T> conflatedCallbackFlow( @BuilderInference block: suspend ProducerScope<T>.() -> Unit, - ): Flow<T> = callbackFlow(block).buffer(capacity = Channel.CONFLATED) + ): Flow<T> = wrapped(block) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt index 80e94a27bec5..20b7b2a91ade 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt @@ -23,12 +23,14 @@ import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVi import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor +import com.android.systemui.util.kotlin.sample import com.android.systemui.util.kotlin.toPx import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map /** * Distance over which the surface behind the keyguard is animated in during a Y-translation @@ -96,13 +98,21 @@ constructor( .distinctUntilChanged() /** + * Whether a notification launch animation is running when we're not already in the GONE state. + */ + private val isNotificationLaunchAnimationRunningOnKeyguard = + notificationLaunchInteractor.isLaunchAnimationRunning + .sample(transitionInteractor.finishedKeyguardState) + .map { it != KeyguardState.GONE } + + /** * Whether we're animating the surface, or a notification launch animation is running (which * means we're going to animate the surface, even if animators aren't yet running). */ val isAnimatingSurface = combine( repository.isAnimatingSurface, - notificationLaunchInteractor.isLaunchAnimationRunning + isNotificationLaunchAnimationRunningOnKeyguard, ) { animatingSurface, animatingLaunch -> animatingSurface || animatingLaunch } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 6255f0d44609..7178e1bd9357 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -36,7 +36,6 @@ import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.clocks.ClockController -import com.android.systemui.shared.clocks.DEFAULT_CLOCK_ID import kotlinx.coroutines.launch object KeyguardClockViewBinder { @@ -76,13 +75,13 @@ object KeyguardClockViewBinder { } launch { if (!MigrateClocksToBlueprint.isEnabled) return@launch - viewModel.clockShouldBeCentered.collect { clockShouldBeCentered -> + viewModel.clockShouldBeCentered.collect { viewModel.currentClock.value?.let { - // Weather clock also has hasCustomPositionUpdatedAnimation as true - // TODO(b/323020908): remove ID check + // TODO(b/301502635): remove "!it.config.useCustomClockScene" when + // migrate clocks to blueprint is fully rolled out if ( it.largeClock.config.hasCustomPositionUpdatedAnimation && - it.config.id == DEFAULT_CLOCK_ID + !it.config.useCustomClockScene ) { blueprintInteractor.refreshBlueprint(Type.DefaultClockStepping) } else { @@ -93,12 +92,9 @@ object KeyguardClockViewBinder { } launch { if (!MigrateClocksToBlueprint.isEnabled) return@launch - viewModel.isAodIconsVisible.collect { isAodIconsVisible -> + viewModel.isAodIconsVisible.collect { viewModel.currentClock.value?.let { - // Weather clock also has hasCustomPositionUpdatedAnimation as true - if ( - viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER" - ) { + if (viewModel.useLargeClock && it.config.useCustomClockScene) { blueprintInteractor.refreshBlueprint(Type.DefaultTransition) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 5ee35e4f8eb6..cc54920236da 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -30,6 +30,9 @@ import android.view.ViewGroup import android.view.ViewGroup.OnHierarchyChangeListener import android.view.ViewPropertyAnimator import android.view.WindowInsets +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.OnBackPressedDispatcherOwner +import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators @@ -49,6 +52,7 @@ import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor +import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters @@ -125,6 +129,21 @@ object KeyguardRootViewBinder { disposables += view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { + if (ComposeLockscreen.isEnabled) { + view.setViewTreeOnBackPressedDispatcherOwner( + object : OnBackPressedDispatcherOwner { + override val onBackPressedDispatcher = + OnBackPressedDispatcher().apply { + setOnBackInvokedDispatcher( + view.viewRootImpl.onBackInvokedDispatcher + ) + } + + override val lifecycle: Lifecycle = + this@repeatWhenAttached.lifecycle + } + ) + } launch { occludingAppDeviceEntryMessageViewModel.message.collect { biometricMessage -> diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index f6da033f1bd3..a6d3312fd6d2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -118,8 +118,7 @@ constructor( currentClock ) { isLargeClockVisible, clockShouldBeCentered, shadeMode, currentClock -> val shouldUseSplitShade = shadeMode == ShadeMode.Split - // TODO(b/326098079): make id a constant field in config - if (currentClock?.config?.id == "DIGITAL_CLOCK_WEATHER") { + if (currentClock?.config?.useCustomClockScene == true) { val weatherClockLayout = when { shouldUseSplitShade && clockShouldBeCentered -> diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt new file mode 100644 index 000000000000..14a917999bb7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 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.media.controls.ui.binder + +import android.widget.ImageButton +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.res.R + +object MediaControlViewBinder { + + fun setVisibleAndAlpha(set: ConstraintSet, resId: Int, visible: Boolean) { + setVisibleAndAlpha(set, resId, visible, ConstraintSet.GONE) + } + + private fun setVisibleAndAlpha( + set: ConstraintSet, + resId: Int, + visible: Boolean, + notVisibleValue: Int + ) { + set.setVisibility(resId, if (visible) ConstraintSet.VISIBLE else notVisibleValue) + set.setAlpha(resId, if (visible) 1.0f else 0.0f) + } + + fun updateSeekBarVisibility(constraintSet: ConstraintSet, isSeekBarEnabled: Boolean) { + if (isSeekBarEnabled) { + constraintSet.setVisibility(R.id.media_progress_bar, ConstraintSet.VISIBLE) + constraintSet.setAlpha(R.id.media_progress_bar, 1.0f) + } else { + constraintSet.setVisibility(R.id.media_progress_bar, ConstraintSet.INVISIBLE) + constraintSet.setAlpha(R.id.media_progress_bar, 0.0f) + } + } + + fun setSemanticButtonVisibleAndAlpha( + button: ImageButton, + expandedSet: ConstraintSet, + collapsedSet: ConstraintSet, + visible: Boolean, + notVisibleValue: Int, + showInCollapsed: Boolean + ) { + if (notVisibleValue == ConstraintSet.INVISIBLE) { + // Since time views should appear instead of buttons. + button.isFocusable = visible + button.isClickable = visible + } + setVisibleAndAlpha(expandedSet, button.id, visible, notVisibleValue) + setVisibleAndAlpha(collapsedSet, button.id, visible = visible && showInCollapsed) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index b315cac28953..7fced5f8036f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -16,41 +16,73 @@ package com.android.systemui.media.controls.ui.controller +import android.animation.Animator +import android.animation.AnimatorInflater +import android.animation.AnimatorSet import android.content.Context import android.content.res.Configuration +import android.graphics.Color +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.provider.Settings +import android.view.View +import android.view.animation.Interpolator import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT +import com.android.app.animation.Interpolators import com.android.app.tracing.traceSection +import com.android.systemui.Flags +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.controls.ui.animation.ColorSchemeTransition +import com.android.systemui.media.controls.ui.animation.MetadataAnimationHandler +import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder +import com.android.systemui.media.controls.ui.binder.SeekBarObserver import com.android.systemui.media.controls.ui.controller.MediaCarouselController.Companion.calculateAlpha import com.android.systemui.media.controls.ui.view.GutsViewHolder import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.view.RecommendationViewHolder +import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel +import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.surfaceeffects.PaintDrawCallback +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView +import com.android.systemui.surfaceeffects.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.MeasurementOutput import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionLayoutController import com.android.systemui.util.animation.TransitionViewState +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.settings.GlobalSettings import java.lang.Float.max import java.lang.Float.min +import java.util.Random import javax.inject.Inject /** * A class responsible for controlling a single instance of a media player handling interactions * with the view instance and keeping the media view states up to date. */ -class MediaViewController +open class MediaViewController @Inject constructor( private val context: Context, private val configurationController: ConfigurationController, private val mediaHostStatesManager: MediaHostStatesManager, private val logger: MediaViewLogger, + private val seekBarViewModel: SeekBarViewModel, + @Main private val mainExecutor: DelayableExecutor, private val mediaFlags: MediaFlags, + private val globalSettings: GlobalSettings, ) { /** @@ -131,6 +163,72 @@ constructor( return transitionLayout?.translationY ?: 0.0f } + /** Whether artwork is bound. */ + var isArtworkBound: Boolean = false + + /** previous background artwork */ + var prevArtwork: Drawable? = null + + /** Whether scrubbing time can show */ + var canShowScrubbingTime: Boolean = false + + /** Whether user is touching the seek bar to change the position */ + var isScrubbing: Boolean = false + + var isSeekBarEnabled: Boolean = false + + /** Not visible value for previous button when scrubbing */ + private var prevNotVisibleValue = ConstraintSet.GONE + private var isPrevButtonAvailable = false + + /** Not visible value for next button when scrubbing */ + private var nextNotVisibleValue = ConstraintSet.GONE + private var isNextButtonAvailable = false + + private lateinit var mediaViewHolder: MediaViewHolder + private lateinit var seekBarObserver: SeekBarObserver + private lateinit var turbulenceNoiseController: TurbulenceNoiseController + private lateinit var loadingEffect: LoadingEffect + private lateinit var turbulenceNoiseAnimationConfig: TurbulenceNoiseAnimationConfig + private lateinit var noiseDrawCallback: PaintDrawCallback + private lateinit var stateChangedCallback: LoadingEffect.AnimationStateChangedCallback + internal lateinit var metadataAnimationHandler: MetadataAnimationHandler + internal lateinit var colorSchemeTransition: ColorSchemeTransition + internal lateinit var multiRippleController: MultiRippleController + + private val scrubbingChangeListener = + object : SeekBarViewModel.ScrubbingChangeListener { + override fun onScrubbingChanged(scrubbing: Boolean) { + if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (isScrubbing == scrubbing) return + isScrubbing = scrubbing + updateDisplayForScrubbingChange() + } + } + + private val enabledChangeListener = + object : SeekBarViewModel.EnabledChangeListener { + override fun onEnabledChanged(enabled: Boolean) { + if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (isSeekBarEnabled == enabled) return + isSeekBarEnabled = enabled + MediaControlViewBinder.updateSeekBarVisibility(expandedLayout, isSeekBarEnabled) + } + } + + /** + * Sets the listening state of the player. + * + * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid + * unnecessary work when the QS panel is closed. + * + * @param listening True when player should be active. Otherwise, false. + */ + fun setListening(listening: Boolean) { + if (!mediaFlags.isMediaControlsRefactorEnabled()) return + seekBarViewModel.listening = listening + } + /** A callback for config changes */ private val configurationListener = object : ConfigurationController.ConfigurationListener { @@ -232,6 +330,14 @@ constructor( * Notify this controller that the view has been removed and all listeners should be destroyed */ fun onDestroy() { + if (mediaFlags.isMediaControlsRefactorEnabled()) { + if (this::seekBarObserver.isInitialized) { + seekBarViewModel.progress.removeObserver(seekBarObserver) + } + seekBarViewModel.removeScrubbingChangeListener(scrubbingChangeListener) + seekBarViewModel.removeEnabledChangeListener(enabledChangeListener) + seekBarViewModel.onDestroy() + } mediaHostStatesManager.removeController(this) configurationController.removeCallback(configurationListener) } @@ -546,6 +652,178 @@ constructor( ) } + fun attachPlayer(mediaViewHolder: MediaViewHolder) { + if (!mediaFlags.isMediaControlsRefactorEnabled()) return + this.mediaViewHolder = mediaViewHolder + + // Setting up seek bar. + seekBarObserver = SeekBarObserver(mediaViewHolder) + seekBarViewModel.progress.observeForever(seekBarObserver) + seekBarViewModel.attachTouchHandlers(mediaViewHolder.seekBar) + seekBarViewModel.setScrubbingChangeListener(scrubbingChangeListener) + seekBarViewModel.setEnabledChangeListener(enabledChangeListener) + + val mediaCard = mediaViewHolder.player + attach(mediaViewHolder.player, TYPE.PLAYER) + + val turbulenceNoiseView = mediaViewHolder.turbulenceNoiseView + turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView) + + multiRippleController = MultiRippleController(mediaViewHolder.multiRippleView) + + // Metadata Animation + val titleText = mediaViewHolder.titleText + val artistText = mediaViewHolder.artistText + val explicitIndicator = mediaViewHolder.explicitIndicator + val enter = + loadAnimator( + mediaCard.context, + R.anim.media_metadata_enter, + Interpolators.EMPHASIZED_DECELERATE, + titleText, + artistText, + explicitIndicator + ) + val exit = + loadAnimator( + mediaCard.context, + R.anim.media_metadata_exit, + Interpolators.EMPHASIZED_ACCELERATE, + titleText, + artistText, + explicitIndicator + ) + metadataAnimationHandler = MetadataAnimationHandler(exit, enter) + + colorSchemeTransition = + ColorSchemeTransition( + mediaCard.context, + mediaViewHolder, + multiRippleController, + turbulenceNoiseController + ) + + // For Turbulence noise. + val loadingEffectView = mediaViewHolder.loadingEffectView + turbulenceNoiseAnimationConfig = + createTurbulenceNoiseConfig( + loadingEffectView, + turbulenceNoiseView, + colorSchemeTransition + ) + noiseDrawCallback = + object : PaintDrawCallback { + override fun onDraw(paint: Paint) { + loadingEffectView.draw(paint) + } + } + stateChangedCallback = + object : LoadingEffect.AnimationStateChangedCallback { + override fun onStateChanged( + oldState: LoadingEffect.AnimationState, + newState: LoadingEffect.AnimationState + ) { + if (newState === LoadingEffect.AnimationState.NOT_PLAYING) { + loadingEffectView.visibility = View.INVISIBLE + } else { + loadingEffectView.visibility = View.VISIBLE + } + } + } + } + + fun updateAnimatorDurationScale() { + if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (this::seekBarObserver.isInitialized) { + seekBarObserver.animationEnabled = + globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f + } + } + + /** update view with the needed UI changes when user touches seekbar. */ + private fun updateDisplayForScrubbingChange() { + mainExecutor.execute { + val isTimeVisible = canShowScrubbingTime && isScrubbing + MediaControlViewBinder.setVisibleAndAlpha( + expandedLayout, + mediaViewHolder.scrubbingTotalTimeView.id, + isTimeVisible + ) + MediaControlViewBinder.setVisibleAndAlpha( + expandedLayout, + mediaViewHolder.scrubbingElapsedTimeView.id, + isTimeVisible + ) + + MediaControlViewModel.SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.forEach { id -> + val isButtonVisible: Boolean + val notVisibleValue: Int + when (id) { + R.id.actionPrev -> { + isButtonVisible = isPrevButtonAvailable && !isTimeVisible + notVisibleValue = prevNotVisibleValue + } + R.id.actionNext -> { + isButtonVisible = isNextButtonAvailable && !isTimeVisible + notVisibleValue = nextNotVisibleValue + } + else -> { + isButtonVisible = !isTimeVisible + notVisibleValue = ConstraintSet.GONE + } + } + MediaControlViewBinder.setSemanticButtonVisibleAndAlpha( + mediaViewHolder.getAction(id), + expandedLayout, + collapsedLayout, + isButtonVisible, + notVisibleValue, + showInCollapsed = true + ) + } + + if (!metadataAnimationHandler.isRunning) { + refreshState() + } + } + } + + fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) { + if (!mediaFlags.isMediaControlsRefactorEnabled()) return + seekBarViewModel.logSeek = onSeek + onBindSeekBar.invoke(seekBarViewModel) + } + + fun setUpTurbulenceNoise() { + if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (Flags.shaderlibLoadingEffectRefactor()) { + if (!this::loadingEffect.isInitialized) { + loadingEffect = + LoadingEffect( + TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, + turbulenceNoiseAnimationConfig, + noiseDrawCallback, + stateChangedCallback + ) + } + colorSchemeTransition.loadingEffect = loadingEffect + loadingEffect.play() + mainExecutor.executeDelayed( + loadingEffect::finish, + MediaControlViewModel.TURBULENCE_NOISE_PLAY_MS_DURATION + ) + } else { + turbulenceNoiseController.play( + TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, + turbulenceNoiseAnimationConfig + ) + mainExecutor.executeDelayed( + turbulenceNoiseController::finish, + MediaControlViewModel.TURBULENCE_NOISE_PLAY_MS_DURATION + ) + } + } + /** * Obtain a measurement for a given location. This makes sure that the state is up to date and * all widgets know their location. Calling this method may create a measurement if we don't @@ -801,6 +1079,75 @@ constructor( applyImmediately = true ) } + + @VisibleForTesting + protected open fun loadAnimator( + context: Context, + animId: Int, + motionInterpolator: Interpolator?, + vararg targets: View? + ): AnimatorSet { + val animators = ArrayList<Animator>() + for (target in targets) { + val animator = AnimatorInflater.loadAnimator(context, animId) as AnimatorSet + animator.childAnimations[0].interpolator = motionInterpolator + animator.setTarget(target) + animators.add(animator) + } + val result = AnimatorSet() + result.playTogether(animators) + return result + } + + private fun createTurbulenceNoiseConfig( + loadingEffectView: LoadingEffectView, + turbulenceNoiseView: TurbulenceNoiseView, + colorSchemeTransition: ColorSchemeTransition + ): TurbulenceNoiseAnimationConfig { + val targetView: View = + if (Flags.shaderlibLoadingEffectRefactor()) { + loadingEffectView + } else { + turbulenceNoiseView + } + val width = targetView.width + val height = targetView.height + val random = Random() + return TurbulenceNoiseAnimationConfig( + gridCount = 2.14f, + TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER, + random.nextFloat(), + random.nextFloat(), + random.nextFloat(), + noiseMoveSpeedX = 0.42f, + noiseMoveSpeedY = 0f, + TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z, + // Color will be correctly updated in ColorSchemeTransition. + colorSchemeTransition.accentPrimary.currentColor, + screenColor = Color.BLACK, + width.toFloat(), + height.toFloat(), + TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS, + easeInDuration = 1350f, + easeOutDuration = 1350f, + targetView.context.resources.displayMetrics.density, + lumaMatteBlendFactor = 0.26f, + lumaMatteOverallBrightness = 0.09f, + shouldInverseNoiseLuminosity = false + ) + } + + fun setUpPrevButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) { + if (!mediaFlags.isMediaControlsRefactorEnabled()) return + isPrevButtonAvailable = isAvailable + prevNotVisibleValue = notVisibleValue + } + + fun setUpNextButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) { + if (!mediaFlags.isMediaControlsRefactorEnabled()) return + isNextButtonAvailable = isAvailable + nextNotVisibleValue = notVisibleValue + } } /** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt index 1e67a77250ee..82099e61009f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt @@ -24,7 +24,8 @@ data class MediaActionViewModel( val icon: Drawable?, val contentDescription: CharSequence?, val background: Drawable?, - val isVisible: Boolean = true, + /** whether action is visible if user is touching seekbar to change position. */ + val isVisibleWhenScrubbing: Boolean = true, val notVisibleValue: Int = ConstraintSet.GONE, val showInCollapsed: Boolean, val rebindId: Int? = null, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt index 7c599953f9b9..d74506dc2e8d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.media.controls.ui.viewmodel import android.content.Context import android.content.pm.PackageManager +import android.media.session.MediaController import android.media.session.MediaSession.Token import android.text.TextUtils import android.util.Log @@ -40,6 +41,7 @@ import com.android.systemui.monet.ColorScheme import com.android.systemui.monet.Style import com.android.systemui.res.R import com.android.systemui.util.kotlin.sample +import java.util.concurrent.Executor import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -51,6 +53,7 @@ import kotlinx.coroutines.flow.flowOn class MediaControlViewModel( @Application private val applicationContext: Context, @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundExecutor: Executor, private val interactor: MediaControlInteractor, private val logger: MediaUiEventLogger, ) { @@ -124,13 +127,15 @@ class MediaControlViewModel( } }, backgroundCover = model.artwork, - appIcon = getAppIcon(model.appIcon, model.isResume, model.packageName), + appIcon = model.appIcon, + launcherIcon = getIconFromApp(model.packageName), useGrayColorFilter = model.appIcon == null || model.isResume, artistName = model.artistName ?: "", titleName = model.songName ?: "", isExplicitVisible = model.showExplicit, + shouldAddGradient = wallpaperColors != null, colorScheme = scheme, - isTimeVisible = canShowScrubbingTimeViews(model.semanticActionButtons), + canShowTime = canShowScrubbingTimeViews(model.semanticActionButtons), playTurbulenceNoise = playTurbulenceNoise, useSemanticActions = model.semanticActionButtons != null, actionButtons = toActionViewModels(model), @@ -146,6 +151,21 @@ class MediaControlViewModel( onLongClicked = { logger.logLongPressOpen(model.uid, model.packageName, model.instanceId) }, + onSeek = { + logger.logSeek(model.uid, model.packageName, model.instanceId) + // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_CLICK_EVENT) + }, + onBindSeekbar = { seekBarViewModel -> + if (model.isResume && model.resumeProgress != null) { + seekBarViewModel.updateStaticProgress(model.resumeProgress) + } else { + backgroundExecutor.execute { + seekBarViewModel.updateController( + model.token?.let { MediaController(applicationContext, it) } + ) + } + } + } ) } @@ -278,16 +298,16 @@ class MediaControlViewModel( model: MediaControlModel, mediaAction: MediaAction, buttonId: Int, - isScrubbingTimeEnabled: Boolean + canShowScrubbingTimeViews: Boolean ): MediaActionViewModel { val showInCollapsed = SEMANTIC_ACTIONS_COMPACT.contains(buttonId) val hideWhenScrubbing = SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.contains(buttonId) - val shouldHideDueToScrubbing = isScrubbingTimeEnabled && hideWhenScrubbing + val shouldHideWhenScrubbing = canShowScrubbingTimeViews && hideWhenScrubbing return MediaActionViewModel( icon = mediaAction.icon, contentDescription = mediaAction.contentDescription, background = mediaAction.background, - isVisible = !shouldHideDueToScrubbing, + isVisibleWhenScrubbing = !shouldHideWhenScrubbing, notVisibleValue = if ( (buttonId == R.id.actionPrev && model.semanticActionButtons!!.reservePrev) || @@ -342,19 +362,6 @@ class MediaControlViewModel( action.run() } - private fun getAppIcon( - icon: android.graphics.drawable.Icon?, - isResume: Boolean, - packageName: String - ): Icon { - if (icon != null && !isResume) { - icon.loadDrawable(applicationContext)?.let { drawable -> - return Icon.Loaded(drawable, null) - } - } - return getIconFromApp(packageName) - } - private fun getIconFromApp(packageName: String): Icon { return try { Icon.Loaded(applicationContext.packageManager.getApplicationIcon(packageName), null) @@ -381,17 +388,17 @@ class MediaControlViewModel( private const val DISABLED_ALPHA = 0.38f /** Buttons to show in small player when using semantic actions */ - private val SEMANTIC_ACTIONS_COMPACT = + val SEMANTIC_ACTIONS_COMPACT = listOf(R.id.actionPlayPause, R.id.actionPrev, R.id.actionNext) /** * Buttons that should get hidden when we are scrubbing (they will be replaced with the * views showing scrubbing time) */ - private val SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING = listOf(R.id.actionPrev, R.id.actionNext) + val SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING = listOf(R.id.actionPrev, R.id.actionNext) /** Buttons to show in player when using semantic actions. */ - private val SEMANTIC_ACTIONS_ALL = + val SEMANTIC_ACTIONS_ALL = listOf( R.id.actionPlayPause, R.id.actionPrev, @@ -399,5 +406,9 @@ class MediaControlViewModel( R.id.action0, R.id.action1 ) + + const val TURBULENCE_NOISE_PLAY_MS_DURATION = 7500L + const val MEDIA_PLAYER_SCRIM_START_ALPHA = 0.25f + const val MEDIA_PLAYER_SCRIM_END_ALPHA = 1.0f } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt index 9029a65bd9ee..d1014e83ea11 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt @@ -24,13 +24,15 @@ import com.android.systemui.monet.ColorScheme data class MediaPlayerViewModel( val contentDescription: (Boolean) -> CharSequence, val backgroundCover: android.graphics.drawable.Icon?, - val appIcon: Icon, + val appIcon: android.graphics.drawable.Icon?, + val launcherIcon: Icon, val useGrayColorFilter: Boolean, val artistName: CharSequence, val titleName: CharSequence, val isExplicitVisible: Boolean, + val shouldAddGradient: Boolean, val colorScheme: ColorScheme, - val isTimeVisible: Boolean, + val canShowTime: Boolean, val playTurbulenceNoise: Boolean, val useSemanticActions: Boolean, val actionButtons: List<MediaActionViewModel?>, @@ -38,4 +40,6 @@ data class MediaPlayerViewModel( val gutsMenu: GutsViewModel, val onClicked: (Expandable) -> Unit, val onLongClicked: () -> Unit, + val onSeek: () -> Unit, + val onBindSeekbar: (SeekBarViewModel) -> Unit, ) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java index 706ac9c46be1..b43a1d23da24 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java @@ -23,6 +23,7 @@ import android.content.ContentProvider; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.HardwareRenderer; +import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.RecordingCanvas; import android.graphics.Rect; @@ -38,9 +39,11 @@ import android.util.Log; import android.view.Display; import android.view.ScrollCaptureResponse; import android.view.View; +import android.view.WindowInsets; import android.widget.ImageView; import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.view.WindowCompat; import com.android.internal.app.ChooserActivity; import com.android.internal.logging.UiEventLogger; @@ -127,6 +130,10 @@ public class LongScreenshotActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // Enable edge-to-edge explicitly. + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + setContentView(R.layout.long_screenshot); mPreview = requireViewById(R.id.preview); @@ -149,6 +156,13 @@ public class LongScreenshotActivity extends Activity { (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> updateImageDimensions()); + requireViewById(R.id.root).setOnApplyWindowInsetsListener( + (view, windowInsets) -> { + Insets insets = windowInsets.getInsets(WindowInsets.Type.systemBars()); + view.setPadding(insets.left, insets.top, insets.right, insets.bottom); + return WindowInsets.CONSUMED; + }); + Intent intent = getIntent(); mScrollCaptureResponse = intent.getParcelableExtra(EXTRA_CAPTURE_RESPONSE); mScreenshotUserHandle = intent.getParcelableExtra(EXTRA_SCREENSHOT_USER_HANDLE, diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt index 980f665ae61f..6800c6115080 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -37,6 +37,7 @@ import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorVie import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -64,6 +65,7 @@ constructor( private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, private val sceneInteractor: SceneInteractor, + unfoldTransitionInteractor: UnfoldTransitionInteractor, ) { val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = combine( @@ -106,6 +108,16 @@ constructor( val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode + /** + * The unfold transition progress. When fully-unfolded, this is `1` and fully folded, it's `0`. + */ + val unfoldTransitionProgress: StateFlow<Float> = + unfoldTransitionInteractor.unfoldProgress.stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = 1f + ) + /** Notifies that some content in the shade was clicked. */ fun onContentClicked() { if (!isClickable.value) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt index 08ed0305bd13..054116dbdfc2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt @@ -22,6 +22,8 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -41,6 +43,7 @@ constructor( val repo: DeviceBasedSatelliteRepository, iconsInteractor: MobileIconsInteractor, deviceProvisioningInteractor: DeviceProvisioningInteractor, + wifiInteractor: WifiInteractor, @Application scope: CoroutineScope, ) { /** Must be observed by any UI showing Satellite iconography */ @@ -73,6 +76,9 @@ constructor( val isDeviceProvisioned: Flow<Boolean> = deviceProvisioningInteractor.isDeviceProvisioned + val isWifiActive: Flow<Boolean> = + wifiInteractor.wifiNetwork.map { it is WifiNetworkModel.Active } + /** When all connections are considered OOS, satellite connectivity is potentially valid */ val areAllConnectionsOutOfService = if (Flags.oemEnabledSatelliteFlag()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt index 40641bed0602..a0291b81c9a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt @@ -59,9 +59,10 @@ constructor( combine( interactor.isSatelliteAllowed, interactor.isDeviceProvisioned, + interactor.isWifiActive, airplaneModeRepository.isAirplaneMode - ) { isSatelliteAllowed, isDeviceProvisioned, isAirplaneMode -> - isSatelliteAllowed && isDeviceProvisioned && !isAirplaneMode + ) { isSatelliteAllowed, isDeviceProvisioned, isWifiActive, isAirplaneMode -> + isSatelliteAllowed && isDeviceProvisioned && !isWifiActive && !isAirplaneMode } } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt index 0d3682c9a24b..fbbd2b9c5de8 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt @@ -15,9 +15,11 @@ */ package com.android.systemui.unfold.data.repository +import androidx.annotation.FloatRange import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted import com.android.systemui.util.kotlin.getOrNull import java.util.Optional @@ -42,6 +44,10 @@ interface UnfoldTransitionRepository { sealed class UnfoldTransitionStatus { /** Status that is sent when fold or unfold transition is in started state */ data object TransitionStarted : UnfoldTransitionStatus() + /** Status that is sent while fold or unfold transition is in progress */ + data class TransitionInProgress( + @FloatRange(from = 0.0, to = 1.0) val progress: Float, + ) : UnfoldTransitionStatus() /** Status that is sent when fold or unfold transition is finished */ data object TransitionFinished : UnfoldTransitionStatus() } @@ -66,6 +72,10 @@ constructor( trySend(TransitionStarted) } + override fun onTransitionProgress(progress: Float) { + trySend(TransitionInProgress(progress)) + } + override fun onTransitionFinished() { trySend(TransitionFinished) } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt index 3e2e564c307c..a8e4496d7804 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt @@ -17,10 +17,14 @@ package com.android.systemui.unfold.domain.interactor import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map /** * Contains business-logic related to fold-unfold transitions while interacting with @@ -30,6 +34,8 @@ interface UnfoldTransitionInteractor { /** Returns availability of fold/unfold transitions on the device */ val isAvailable: Boolean + val unfoldProgress: Flow<Float> + /** Suspends and waits for a fold/unfold transition to finish */ suspend fun waitForTransitionFinish() @@ -44,6 +50,11 @@ constructor(private val repository: UnfoldTransitionRepository) : UnfoldTransiti override val isAvailable: Boolean get() = repository.isAvailable + override val unfoldProgress: Flow<Float> = + repository.transitionStatus + .map { (it as? TransitionInProgress)?.progress ?: 1f } + .distinctUntilChanged() + override suspend fun waitForTransitionFinish() { repository.transitionStatus.filter { it is TransitionFinished }.first() } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java index a2499615e18c..319b61512f12 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java @@ -205,9 +205,9 @@ public class KeyguardClockSwitchControllerBaseTest extends SysuiTestCase { when(mClockRegistry.createCurrentClock()).thenReturn(mClockController); when(mClockEventController.getClock()).thenReturn(mClockController); when(mSmallClockController.getConfig()) - .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false)); + .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false, false)); when(mLargeClockController.getConfig()) - .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false)); + .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false, false)); mSliceView = new View(getContext()); when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 9d81b960336f..99b5a4b631a7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -272,9 +272,9 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro assertEquals(View.VISIBLE, mFakeDateView.getVisibility()); when(mSmallClockController.getConfig()) - .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false)); + .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false, true)); when(mLargeClockController.getConfig()) - .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false)); + .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false, true)); verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture()); listenerArgumentCaptor.getValue().onCurrentClockChanged(); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java index 11fe86277903..b2828a41c4b0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java @@ -98,7 +98,7 @@ public class KeyguardStatusViewControllerTest extends KeyguardStatusViewControll public void updatePosition_primaryClockAnimation() { ClockController mockClock = mock(ClockController.class); when(mKeyguardClockSwitchController.getClock()).thenReturn(mockClock); - when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", "", "", false, true)); + when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", "", "", false, true, false)); mController.updatePosition(10, 15, 20f, true); @@ -113,7 +113,7 @@ public class KeyguardStatusViewControllerTest extends KeyguardStatusViewControll public void updatePosition_alternateClockAnimation() { ClockController mockClock = mock(ClockController.class); when(mKeyguardClockSwitchController.getClock()).thenReturn(mockClock); - when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", "", "", true, true)); + when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", "", "", true, true, false)); mController.updatePosition(10, 15, 20f, true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt index 3f05bfae6777..9ccf2121b8d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState @@ -227,4 +228,50 @@ class KeyguardSurfaceBehindInteractorTest : SysuiTestCase() { { it == KeyguardSurfaceBehindModel(alpha = 0f) }, ) } + + @Test + fun notificationLaunchFromLockscreen_isAnimatingSurfaceTrue() = + testScope.runTest { + val isAnimatingSurface by collectLastValue(underTest.isAnimatingSurface) + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED, + ) + ) + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.FINISHED, + ) + ) + kosmos.notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(true) + runCurrent() + assertThat(isAnimatingSurface).isTrue() + } + + @Test + fun notificationLaunchFromGone_isAnimatingSurfaceFalse() = + testScope.runTest { + val isAnimatingSurface by collectLastValue(underTest.isAnimatingSurface) + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + transitionState = TransitionState.STARTED, + ) + ) + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + transitionState = TransitionState.FINISHED, + ) + ) + kosmos.notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(true) + runCurrent() + assertThat(isAnimatingSurface).isFalse() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt index 66aa572dbc48..5e3a142dc348 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt @@ -113,7 +113,8 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { id = "WEATHER_CLOCK", name = "", description = "", - useAlternateSmartspaceAODTransition = true + useAlternateSmartspaceAODTransition = true, + useCustomClockScene = true ) whenever(clock.config).thenReturn(clockConfig) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt index a73bb2cdf79a..e5d3082bb245 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt @@ -16,29 +16,54 @@ package com.android.systemui.media.controls.ui.controller +import android.animation.AnimatorSet +import android.content.Context import android.content.res.Configuration import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.RippleDrawable import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View +import android.view.ViewGroup +import android.view.animation.Interpolator +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.SeekBar +import android.widget.TextView import androidx.constraintlayout.widget.ConstraintSet +import androidx.lifecycle.LiveData import androidx.test.filters.SmallTest +import com.android.internal.widget.CachingIconView import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.ui.view.GutsViewHolder import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.view.RecommendationViewHolder +import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.res.R +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView +import com.android.systemui.surfaceeffects.ripple.MultiRippleView +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionViewState import com.android.systemui.util.animation.WidgetState +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.floatThat import org.mockito.Mock +import org.mockito.Mockito import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify @@ -55,6 +80,31 @@ class MediaViewControllerTest : SysuiTestCase() { com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context) private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0) private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0) + private val clock = FakeSystemClock() + private lateinit var mainExecutor: FakeExecutor + private lateinit var seekBar: SeekBar + private lateinit var multiRippleView: MultiRippleView + private lateinit var turbulenceNoiseView: TurbulenceNoiseView + private lateinit var loadingEffectView: LoadingEffectView + private lateinit var settings: ImageButton + private lateinit var cancel: View + private lateinit var cancelText: TextView + private lateinit var dismiss: FrameLayout + private lateinit var dismissText: TextView + private lateinit var titleText: TextView + private lateinit var artistText: TextView + private lateinit var explicitIndicator: CachingIconView + private lateinit var seamless: ViewGroup + private lateinit var seamlessButton: View + private lateinit var seamlessIcon: ImageView + private lateinit var seamlessText: TextView + private lateinit var scrubbingElapsedTimeView: TextView + private lateinit var scrubbingTotalTimeView: TextView + private lateinit var actionPlayPause: ImageButton + private lateinit var actionNext: ImageButton + private lateinit var actionPrev: ImageButton + @Mock private lateinit var seamlessBackground: RippleDrawable + @Mock private lateinit var albumView: ImageView @Mock lateinit var logger: MediaViewLogger @Mock private lateinit var mockViewState: TransitionViewState @Mock private lateinit var mockCopiedState: TransitionViewState @@ -64,6 +114,14 @@ class MediaViewControllerTest : SysuiTestCase() { @Mock private lateinit var mediaSubTitleWidgetState: WidgetState @Mock private lateinit var mediaContainerWidgetState: WidgetState @Mock private lateinit var mediaFlags: MediaFlags + @Mock private lateinit var seekBarViewModel: SeekBarViewModel + @Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress> + @Mock private lateinit var globalSettings: GlobalSettings + @Mock private lateinit var viewHolder: MediaViewHolder + @Mock private lateinit var view: TransitionLayout + @Mock private lateinit var mockAnimator: AnimatorSet + @Mock private lateinit var gutsViewHolder: GutsViewHolder + @Mock private lateinit var gutsText: TextView private val delta = 0.1F @@ -72,14 +130,30 @@ class MediaViewControllerTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) + mainExecutor = FakeExecutor(clock) mediaViewController = - MediaViewController( - context, - configurationController, - mediaHostStatesManager, - logger, - mediaFlags, - ) + object : + MediaViewController( + context, + configurationController, + mediaHostStatesManager, + logger, + seekBarViewModel, + mainExecutor, + mediaFlags, + globalSettings, + ) { + override fun loadAnimator( + context: Context, + animId: Int, + motionInterpolator: Interpolator?, + vararg targets: View? + ): AnimatorSet { + return mockAnimator + } + } + initGutsViewHolderMocks() + initMediaViewHolderMocks() } @Test @@ -299,4 +373,270 @@ class MediaViewControllerTest : SysuiTestCase() { verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } } + + @Test + fun attachPlayer_seekBarDisabled_seekBarVisibilityIsSetToInvisible() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + mediaViewController.attachPlayer(viewHolder) + getEnabledChangeListener().onEnabledChanged(enabled = true) + getEnabledChangeListener().onEnabledChanged(enabled = false) + + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar)) + .isEqualTo(ConstraintSet.INVISIBLE) + } + + @Test + fun attachPlayer_seekBarEnabled_seekBarVisible() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + mediaViewController.attachPlayer(viewHolder) + getEnabledChangeListener().onEnabledChanged(enabled = true) + + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar)) + .isEqualTo(ConstraintSet.VISIBLE) + } + + @Test + fun attachPlayer_seekBarStatusUpdate_seekBarVisibilityChanges() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + mediaViewController.attachPlayer(viewHolder) + getEnabledChangeListener().onEnabledChanged(enabled = true) + + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar)) + .isEqualTo(ConstraintSet.VISIBLE) + + getEnabledChangeListener().onEnabledChanged(enabled = false) + + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar)) + .isEqualTo(ConstraintSet.INVISIBLE) + } + + @Test + fun attachPlayer_notScrubbing_scrubbingViewsGone() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + mediaViewController.attachPlayer(viewHolder) + mediaViewController.canShowScrubbingTime = true + getScrubbingChangeListener().onScrubbingChanged(true) + getScrubbingChangeListener().onScrubbingChanged(false) + mainExecutor.runAllReady() + + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) + ) + .isEqualTo(ConstraintSet.GONE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) + ) + .isEqualTo(ConstraintSet.GONE) + } + + @Test + fun setIsScrubbing_noSemanticActions_scrubbingViewsGone() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + mediaViewController.attachPlayer(viewHolder) + mediaViewController.canShowScrubbingTime = false + getScrubbingChangeListener().onScrubbingChanged(true) + mainExecutor.runAllReady() + + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) + ) + .isEqualTo(ConstraintSet.GONE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) + ) + .isEqualTo(ConstraintSet.GONE) + } + + @Test + fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + mediaViewController.attachPlayer(viewHolder) + mediaViewController.setUpNextButtonInfo(true) + mediaViewController.setUpPrevButtonInfo(false) + getScrubbingChangeListener().onScrubbingChanged(true) + mainExecutor.runAllReady() + + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext)) + .isEqualTo(ConstraintSet.VISIBLE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) + ) + .isEqualTo(ConstraintSet.GONE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) + ) + .isEqualTo(ConstraintSet.GONE) + } + + @Test + fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + mediaViewController.attachPlayer(viewHolder) + mediaViewController.setUpNextButtonInfo(false) + mediaViewController.setUpPrevButtonInfo(true) + getScrubbingChangeListener().onScrubbingChanged(true) + mainExecutor.runAllReady() + + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev)) + .isEqualTo(ConstraintSet.VISIBLE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) + ) + .isEqualTo(ConstraintSet.GONE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) + ) + .isEqualTo(ConstraintSet.GONE) + } + + @Test + fun setIsScrubbing_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + mediaViewController.attachPlayer(viewHolder) + mediaViewController.setUpNextButtonInfo(true) + mediaViewController.setUpPrevButtonInfo(true) + mediaViewController.canShowScrubbingTime = true + getScrubbingChangeListener().onScrubbingChanged(true) + mainExecutor.runAllReady() + + // Only in expanded, we should show the scrubbing times and hide prev+next + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev)) + .isEqualTo(ConstraintSet.GONE) + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext)) + .isEqualTo(ConstraintSet.GONE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) + ) + .isEqualTo(ConstraintSet.VISIBLE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) + ) + .isEqualTo(ConstraintSet.VISIBLE) + } + + @Test + fun setIsScrubbing_trueThenFalse_reservePrevAndNextButtons() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + mediaViewController.attachPlayer(viewHolder) + mediaViewController.setUpNextButtonInfo(true, ConstraintSet.INVISIBLE) + mediaViewController.setUpPrevButtonInfo(true, ConstraintSet.INVISIBLE) + mediaViewController.canShowScrubbingTime = true + + getScrubbingChangeListener().onScrubbingChanged(true) + mainExecutor.runAllReady() + + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev)) + .isEqualTo(ConstraintSet.INVISIBLE) + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext)) + .isEqualTo(ConstraintSet.INVISIBLE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) + ) + .isEqualTo(ConstraintSet.VISIBLE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) + ) + .isEqualTo(ConstraintSet.VISIBLE) + + getScrubbingChangeListener().onScrubbingChanged(false) + mainExecutor.runAllReady() + + // Only in expanded, we should hide the scrubbing times and show prev+next + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev)) + .isEqualTo(ConstraintSet.VISIBLE) + assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext)) + .isEqualTo(ConstraintSet.VISIBLE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) + ) + .isEqualTo(ConstraintSet.GONE) + assertThat( + mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) + ) + .isEqualTo(ConstraintSet.GONE) + } + + private fun initGutsViewHolderMocks() { + settings = ImageButton(context) + cancel = View(context) + cancelText = TextView(context) + dismiss = FrameLayout(context) + dismissText = TextView(context) + whenever(gutsViewHolder.gutsText).thenReturn(gutsText) + whenever(gutsViewHolder.settings).thenReturn(settings) + whenever(gutsViewHolder.cancel).thenReturn(cancel) + whenever(gutsViewHolder.cancelText).thenReturn(cancelText) + whenever(gutsViewHolder.dismiss).thenReturn(dismiss) + whenever(gutsViewHolder.dismissText).thenReturn(dismissText) + } + + private fun initMediaViewHolderMocks() { + titleText = TextView(context) + artistText = TextView(context) + explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator } + seamless = FrameLayout(context) + seamless.foreground = seamlessBackground + seamlessButton = View(context) + seamlessIcon = ImageView(context) + seamlessText = TextView(context) + seekBar = SeekBar(context).also { it.id = R.id.media_progress_bar } + + actionPlayPause = ImageButton(context).also { it.id = R.id.actionPlayPause } + actionPrev = ImageButton(context).also { it.id = R.id.actionPrev } + actionNext = ImageButton(context).also { it.id = R.id.actionNext } + scrubbingElapsedTimeView = + TextView(context).also { it.id = R.id.media_scrubbing_elapsed_time } + scrubbingTotalTimeView = TextView(context).also { it.id = R.id.media_scrubbing_total_time } + + multiRippleView = MultiRippleView(context, null) + turbulenceNoiseView = TurbulenceNoiseView(context, null) + loadingEffectView = LoadingEffectView(context, null) + + whenever(viewHolder.player).thenReturn(view) + whenever(view.context).thenReturn(context) + whenever(viewHolder.albumView).thenReturn(albumView) + whenever(albumView.foreground).thenReturn(Mockito.mock(Drawable::class.java)) + whenever(viewHolder.titleText).thenReturn(titleText) + whenever(viewHolder.artistText).thenReturn(artistText) + whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator) + whenever(seamlessBackground.getDrawable(0)) + .thenReturn(Mockito.mock(GradientDrawable::class.java)) + whenever(viewHolder.seamless).thenReturn(seamless) + whenever(viewHolder.seamlessButton).thenReturn(seamlessButton) + whenever(viewHolder.seamlessIcon).thenReturn(seamlessIcon) + whenever(viewHolder.seamlessText).thenReturn(seamlessText) + whenever(viewHolder.seekBar).thenReturn(seekBar) + whenever(viewHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView) + whenever(viewHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView) + whenever(viewHolder.gutsViewHolder).thenReturn(gutsViewHolder) + whenever(seekBarViewModel.progress).thenReturn(seekBarData) + + // Action buttons + whenever(viewHolder.actionPlayPause).thenReturn(actionPlayPause) + whenever(viewHolder.getAction(R.id.actionNext)).thenReturn(actionNext) + whenever(viewHolder.getAction(R.id.actionPrev)).thenReturn(actionPrev) + whenever(viewHolder.getAction(R.id.actionPlayPause)).thenReturn(actionPlayPause) + + whenever(viewHolder.multiRippleView).thenReturn(multiRippleView) + whenever(viewHolder.turbulenceNoiseView).thenReturn(turbulenceNoiseView) + whenever(viewHolder.loadingEffectView).thenReturn(loadingEffectView) + } + + private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener = + withArgCaptor { + verify(seekBarViewModel).setScrubbingChangeListener(capture()) + } + + private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = withArgCaptor { + verify(seekBarViewModel).setEnabledChangeListener(capture()) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt index 56fc0c74dafa..a05a23bb8bb1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt @@ -21,7 +21,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate -import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.FakeUnfoldTransitionProvider import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -34,21 +34,19 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() { - private val progressProvider = TestUnfoldTransitionProvider() + private val progressProvider = FakeUnfoldTransitionProvider() - @Mock - private lateinit var parent: ViewGroup + @Mock private lateinit var parent: ViewGroup - @Mock - private lateinit var shouldBeAnimated: () -> Boolean + @Mock private lateinit var shouldBeAnimated: () -> Boolean private lateinit var animator: UnfoldConstantTranslateAnimator private val viewsIdToRegister get() = setOf( - ViewIdToTranslate(START_VIEW_ID, Direction.START, shouldBeAnimated), - ViewIdToTranslate(END_VIEW_ID, Direction.END, shouldBeAnimated) + ViewIdToTranslate(START_VIEW_ID, Direction.START, shouldBeAnimated), + ViewIdToTranslate(END_VIEW_ID, Direction.END, shouldBeAnimated) ) @Before @@ -122,11 +120,12 @@ class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() { progressProvider.onTransitionStarted() progressProvider.onTransitionProgress(0f) - val rtlMultiplier = if (layoutDirection == View.LAYOUT_DIRECTION_LTR) { - 1 - } else { - -1 - } + val rtlMultiplier = + if (layoutDirection == View.LAYOUT_DIRECTION_LTR) { + 1 + } else { + -1 + } list.forEach { (view, direction) -> assertEquals( (-MAX_TRANSLATION * direction * rtlMultiplier).toInt(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt index 4bfd7e3bef83..df82df842c65 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt @@ -31,7 +31,7 @@ import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON import com.android.systemui.power.shared.model.WakefulnessState.STARTING_TO_SLEEP import com.android.systemui.statusbar.policy.FakeConfigurationController -import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.FakeUnfoldTransitionProvider import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractorImpl import com.android.systemui.util.animation.data.repository.FakeAnimationStatusRepository @@ -59,7 +59,7 @@ open class HideNotificationsInteractorTest : SysuiTestCase() { private val animationStatus = FakeAnimationStatusRepository() private val configurationController = FakeConfigurationController() - private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider() + private val unfoldTransitionProgressProvider = FakeUnfoldTransitionProvider() private val powerRepository = FakePowerRepository() private val powerInteractor = PowerInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt index feff046bb708..1ec1765e2e57 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt @@ -8,7 +8,7 @@ import android.view.View import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.FakeUnfoldTransitionProvider import com.android.systemui.unfold.util.CurrentActivityTypeProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider import com.android.systemui.util.mockito.whenever @@ -23,17 +23,14 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() { - @Mock - private lateinit var windowManager: WindowManager + @Mock private lateinit var windowManager: WindowManager - @Mock - private lateinit var display: Display + @Mock private lateinit var display: Display - @Mock - private lateinit var currentActivityTypeProvider: CurrentActivityTypeProvider + @Mock private lateinit var currentActivityTypeProvider: CurrentActivityTypeProvider private val view: View = View(context) - private val progressProvider = TestUnfoldTransitionProvider() + private val progressProvider = FakeUnfoldTransitionProvider() private val scopedProvider = ScopedUnfoldTransitionProgressProvider(progressProvider) private lateinit var controller: StatusBarMoveFromCenterAnimationController diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt index 42bbe3e6fa1d..c4ab943c09df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt @@ -26,6 +26,10 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor import com.android.systemui.util.mockito.mock @@ -53,6 +57,10 @@ class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { private val deviceProvisionedRepository = FakeDeviceProvisioningRepository() private val deviceProvisioningInteractor = DeviceProvisioningInteractor(deviceProvisionedRepository) + private val connectivityRepository = FakeConnectivityRepository() + private val wifiRepository = FakeWifiRepository() + private val wifiInteractor = + WifiInteractorImpl(connectivityRepository, wifiRepository, testScope.backgroundScope) @Before fun setUp() { @@ -61,6 +69,7 @@ class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { repo, iconsInteractor, deviceProvisioningInteractor, + wifiInteractor, testScope.backgroundScope, ) } @@ -103,6 +112,7 @@ class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { repo, iconsInteractor, deviceProvisioningInteractor, + wifiInteractor, testScope.backgroundScope, ) @@ -150,6 +160,7 @@ class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { repo, iconsInteractor, deviceProvisioningInteractor, + wifiInteractor, testScope.backgroundScope, ) @@ -205,6 +216,7 @@ class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { repo, iconsInteractor, deviceProvisioningInteractor, + wifiInteractor, testScope.backgroundScope, ) @@ -337,6 +349,7 @@ class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { repo, iconsInteractor, deviceProvisioningInteractor, + wifiInteractor, testScope.backgroundScope, ) @@ -353,4 +366,28 @@ class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { // THEN the value is still false, because the flag is off assertThat(latest).isFalse() } + + @Test + fun isWifiActive_falseWhenWifiNotActive() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiActive) + + // WHEN wifi is not active + wifiRepository.setWifiNetwork(WifiNetworkModel.Invalid("test")) + + // THEN the interactor returns false due to the wifi network not being active + assertThat(latest).isFalse() + } + + @Test + fun isWifiActive_trueWhenWifiIsActive() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiActive) + + // WHEN wifi is active + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 0, level = 1)) + + // THEN the interactor returns true due to the wifi network being active + assertThat(latest).isTrue() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt index 1d6cd3774449..64f19b6c60ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt @@ -26,6 +26,10 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor import com.android.systemui.util.mockito.mock @@ -44,14 +48,18 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { private lateinit var underTest: DeviceBasedSatelliteViewModel private lateinit var interactor: DeviceBasedSatelliteInteractor private lateinit var airplaneModeRepository: FakeAirplaneModeRepository - private val repo = FakeDeviceBasedSatelliteRepository() + private val testScope = TestScope() + private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) + private val deviceProvisionedRepository = FakeDeviceProvisioningRepository() private val deviceProvisioningInteractor = DeviceProvisioningInteractor(deviceProvisionedRepository) - - private val testScope = TestScope() + private val connectivityRepository = FakeConnectivityRepository() + private val wifiRepository = FakeWifiRepository() + private val wifiInteractor = + WifiInteractorImpl(connectivityRepository, wifiRepository, testScope.backgroundScope) @Before fun setUp() { @@ -63,6 +71,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { repo, mobileIconsInteractor, deviceProvisioningInteractor, + wifiInteractor, testScope.backgroundScope, ) @@ -253,4 +262,40 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // THEN icon is null because the device is not provisioned assertThat(latest).isInstanceOf(Icon::class.java) } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun icon_wifiIsActive() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + i1.isEmergencyOnly.value = false + + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + + // GIVEN device is provisioned + deviceProvisionedRepository.setDeviceProvisioned(true) + + // GIVEN wifi network is active + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 0, level = 1)) + + // THEN icon is null because the device is connected to wifi + assertThat(latest).isNull() + + // GIVEN device loses wifi connection + wifiRepository.setWifiNetwork(WifiNetworkModel.Invalid("test")) + + // Wait for delay to be completed + advanceTimeBy(10.seconds) + + // THEN icon is set because the device lost wifi connection + assertThat(latest).isInstanceOf(Icon::class.java) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt index 28adbceda847..383f4a33857d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt @@ -86,7 +86,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { private val areAnimationEnabled = MutableStateFlow(true) private val lastWakefulnessEvent = MutableStateFlow(WakefulnessModel()) private val systemClock = FakeSystemClock() - private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider() + private val unfoldTransitionProgressProvider = FakeUnfoldTransitionProvider() private val unfoldTransitionRepository = UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider)) private val unfoldTransitionInteractor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt index b9c7e6133669..fd513c9c9235 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt @@ -35,7 +35,7 @@ import org.mockito.Mockito.verify @SmallTest class UnfoldHapticsPlayerTest : SysuiTestCase() { - private val progressProvider = TestUnfoldTransitionProvider() + private val progressProvider = FakeUnfoldTransitionProvider() private val vibrator: Vibrator = mock() private val testFoldProvider = TestFoldProvider() diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt index ba72716997e3..2955384c0bbd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.keyguard.ScreenLifecycle import com.android.systemui.unfold.util.FoldableDeviceStates import com.android.systemui.unfold.util.FoldableTestUtils import com.android.systemui.util.mockito.any +import java.util.Optional import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -37,45 +38,41 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations -import java.util.Optional @RunWith(AndroidTestingRunner::class) @SmallTest class UnfoldLatencyTrackerTest : SysuiTestCase() { - @Mock - lateinit var latencyTracker: LatencyTracker + @Mock lateinit var latencyTracker: LatencyTracker - @Mock - lateinit var deviceStateManager: DeviceStateManager + @Mock lateinit var deviceStateManager: DeviceStateManager - @Mock - lateinit var screenLifecycle: ScreenLifecycle + @Mock lateinit var screenLifecycle: ScreenLifecycle - @Captor - private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener> + @Captor private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener> - @Captor - private lateinit var screenLifecycleCaptor: ArgumentCaptor<ScreenLifecycle.Observer> + @Captor private lateinit var screenLifecycleCaptor: ArgumentCaptor<ScreenLifecycle.Observer> private lateinit var deviceStates: FoldableDeviceStates private lateinit var unfoldLatencyTracker: UnfoldLatencyTracker - private val transitionProgressProvider = TestUnfoldTransitionProvider() + private val transitionProgressProvider = FakeUnfoldTransitionProvider() @Before fun setUp() { MockitoAnnotations.initMocks(this) - unfoldLatencyTracker = UnfoldLatencyTracker( - latencyTracker, - deviceStateManager, - Optional.of(transitionProgressProvider), - context.mainExecutor, - context, - context.contentResolver, - screenLifecycle - ).apply { init() } + unfoldLatencyTracker = + UnfoldLatencyTracker( + latencyTracker, + deviceStateManager, + Optional.of(transitionProgressProvider), + context.mainExecutor, + context, + context.contentResolver, + screenLifecycle + ) + .apply { init() } deviceStates = FoldableTestUtils.findDeviceStates(context) verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture()) @@ -107,7 +104,7 @@ class UnfoldLatencyTrackerTest : SysuiTestCase() { } @Test - fun unfold_firstFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventNotPropagated() { + fun firstFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventNotPropagated() { setAnimationsEnabled(true) sendFoldEvent(folded = false) @@ -118,7 +115,7 @@ class UnfoldLatencyTrackerTest : SysuiTestCase() { } @Test - fun unfold_secondFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() { + fun secondFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() { setAnimationsEnabled(true) sendFoldEvent(folded = true) sendFoldEvent(folded = false) @@ -131,7 +128,7 @@ class UnfoldLatencyTrackerTest : SysuiTestCase() { } @Test - fun unfold_unfoldFoldUnfoldAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() { + fun unfoldFoldUnfoldAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() { setAnimationsEnabled(true) sendFoldEvent(folded = false) sendFoldEvent(folded = true) @@ -196,4 +193,4 @@ class UnfoldLatencyTrackerTest : SysuiTestCase() { durationScale.toString() ) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt index 6ec0251d41a5..0c452eb9d461 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt @@ -17,21 +17,18 @@ import org.mockito.junit.MockitoJUnit @SmallTest class UnfoldTransitionWallpaperControllerTest : SysuiTestCase() { - @Mock - private lateinit var wallpaperController: WallpaperController + @Mock private lateinit var wallpaperController: WallpaperController - private val progressProvider = TestUnfoldTransitionProvider() + private val progressProvider = FakeUnfoldTransitionProvider() - @JvmField - @Rule - val mockitoRule = MockitoJUnit.rule() + @JvmField @Rule val mockitoRule = MockitoJUnit.rule() private lateinit var unfoldWallpaperController: UnfoldTransitionWallpaperController @Before fun setup() { - unfoldWallpaperController = UnfoldTransitionWallpaperController(progressProvider, - wallpaperController) + unfoldWallpaperController = + UnfoldTransitionWallpaperController(progressProvider, wallpaperController) unfoldWallpaperController.init() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt index 2bc05fcc8166..e5f619b299a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt @@ -23,7 +23,7 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.FakeUnfoldTransitionProvider import com.android.systemui.utils.os.FakeHandler import kotlin.test.Test import kotlinx.coroutines.test.runTest @@ -34,7 +34,7 @@ import org.junit.runner.RunWith @RunWithLooper(setAsMainLooper = true) class MainThreadUnfoldTransitionProgressProviderTest : SysuiTestCase() { - private val wrappedProgressProvider = TestUnfoldTransitionProvider() + private val wrappedProgressProvider = FakeUnfoldTransitionProvider() private val fakeHandler = FakeHandler(Looper.getMainLooper()) private val listener = TestUnfoldProgressListener() diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt index d864d53fea32..70ec050afe1d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt @@ -20,7 +20,7 @@ import android.testing.TestableLooper import android.view.Surface import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.FakeUnfoldTransitionProvider import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener @@ -43,14 +43,14 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() { @Mock lateinit var rotationChangeProvider: RotationChangeProvider - private val sourceProvider = TestUnfoldTransitionProvider() + private val sourceProvider = FakeUnfoldTransitionProvider() @Mock lateinit var transitionListener: TransitionProgressListener @Captor private lateinit var rotationListenerCaptor: ArgumentCaptor<RotationListener> lateinit var progressProvider: NaturalRotationUnfoldProgressProvider - private lateinit var testableLooper : TestableLooper + private lateinit var testableLooper: TestableLooper @Before fun setUp() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt index 2f29b3bdd3b5..451bd24dd83f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt @@ -22,7 +22,7 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.FakeUnfoldTransitionProvider import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.util.mockito.any import org.junit.Before @@ -42,7 +42,7 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() { @Mock lateinit var sinkProvider: TransitionProgressListener - private val sourceProvider = TestUnfoldTransitionProvider() + private val sourceProvider = FakeUnfoldTransitionProvider() private lateinit var contentResolver: ContentResolver private lateinit var progressProvider: ScaleAwareTransitionProgressProvider @@ -132,6 +132,6 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() { durationScale.toString() ) - animatorDurationScaleListenerCaptor.value.dispatchChange(/* selfChange= */false) + animatorDurationScaleListenerCaptor.value.dispatchChange(/* selfChange= */ false) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt index 95c934e988e7..4486402f43ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt @@ -23,7 +23,7 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.FakeUnfoldTransitionProvider import com.android.systemui.unfold.progress.TestUnfoldProgressListener import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds @@ -43,7 +43,7 @@ import org.junit.runner.RunWith @RunWithLooper class ScopedUnfoldTransitionProgressProviderTest : SysuiTestCase() { - private val rootProvider = TestUnfoldTransitionProvider() + private val rootProvider = FakeUnfoldTransitionProvider() private val listener = TestUnfoldProgressListener() private val testScope = TestScope(UnconfinedTestDispatcher()) private val bgThread = diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt index f484ea04bb4f..cd4d7b54916e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt @@ -19,7 +19,7 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.FakeUnfoldTransitionProvider import com.android.systemui.unfold.progress.TestUnfoldProgressListener import com.google.common.util.concurrent.MoreExecutors import org.junit.Before @@ -32,7 +32,7 @@ import org.junit.runner.RunWith class UnfoldOnlyProgressProviderTest : SysuiTestCase() { private val listener = TestUnfoldProgressListener() - private val sourceProvider = TestUnfoldTransitionProvider() + private val sourceProvider = FakeUnfoldTransitionProvider() private val foldProvider = TestFoldProvider() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt index da2170c85fe7..2f3d3c3e0489 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.media.controls.ui.viewmodel import android.content.applicationContext +import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.media.controls.domain.pipeline.interactor.mediaControlInteractor @@ -27,6 +28,7 @@ val Kosmos.mediaControlViewModel by MediaControlViewModel( applicationContext = applicationContext, backgroundDispatcher = testDispatcher, + backgroundExecutor = fakeExecutor, interactor = mediaControlInteractor, logger = mediaUiEventLogger, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/FakeUnfoldTransitionProvider.kt index 56c624565971..94f0c44a51b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/FakeUnfoldTransitionProvider.kt @@ -2,7 +2,7 @@ package com.android.systemui.unfold import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener -class TestUnfoldTransitionProvider : UnfoldTransitionProgressProvider, TransitionProgressListener { +class FakeUnfoldTransitionProvider : UnfoldTransitionProgressProvider, TransitionProgressListener { private val listeners = mutableListOf<TransitionProgressListener>() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/UnfoldTransitionProgressProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/UnfoldTransitionProgressProviderKosmos.kt index 7c54a5707bf4..a0f5b58c1cd0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/UnfoldTransitionProgressProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/UnfoldTransitionProgressProviderKosmos.kt @@ -18,6 +18,8 @@ package com.android.systemui.unfold import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.util.mockito.mock -var Kosmos.unfoldTransitionProgressProvider by Fixture { mock<UnfoldTransitionProgressProvider>() } +val Kosmos.fakeUnfoldTransitionProgressProvider by Fixture { FakeUnfoldTransitionProvider() } + +val Kosmos.unfoldTransitionProgressProvider by + Fixture<UnfoldTransitionProgressProvider> { fakeUnfoldTransitionProgressProvider } diff --git a/packages/SystemUI/utils/Android.bp b/packages/SystemUI/utils/Android.bp new file mode 100644 index 000000000000..1ef381617c20 --- /dev/null +++ b/packages/SystemUI/utils/Android.bp @@ -0,0 +1,32 @@ +// +// Copyright (C) 2024 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 { + default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_", + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +java_library { + name: "SystemUI-shared-utils", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + static_libs: [ + "kotlin-stdlib", + "kotlinx_coroutines", + ], +} diff --git a/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/FlowConflated.kt b/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/FlowConflated.kt new file mode 100644 index 000000000000..ed97c600adec --- /dev/null +++ b/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/FlowConflated.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2024 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. + */ + +@file:OptIn(ExperimentalTypeInference::class) + +package com.android.systemui.utils.coroutines.flow + +import kotlin.experimental.ExperimentalTypeInference +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.produceIn +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] provided to + * the builder's [block] of code via [ProducerScope]. It allows elements to be produced by code that + * is running in a different context or concurrently. + * + * The resulting flow is _cold_, which means that [block] is called every time a terminal operator + * is applied to the resulting flow. + * + * This builder ensures thread-safety and context preservation, thus the provided [ProducerScope] + * can be used from any context, e.g. from a callback-based API. The resulting flow completes as + * soon as the code in the [block] completes. [awaitClose] should be used to keep the flow running, + * otherwise the channel will be closed immediately when block completes. [awaitClose] argument is + * called either when a flow consumer cancels the flow collection or when a callback-based API + * invokes [SendChannel.close] manually and is typically used to cleanup the resources after the + * completion, e.g. unregister a callback. Using [awaitClose] is mandatory in order to prevent + * memory leaks when the flow collection is cancelled, otherwise the callback may keep running even + * when the flow collector is already completed. To avoid such leaks, this method throws + * [IllegalStateException] if block returns, but the channel is not closed yet. + * + * A [conflated][conflate] channel is used. Use the [buffer] operator on the resulting flow to + * specify a user-defined value and to control what happens when data is produced faster than + * consumed, i.e. to control the back-pressure behavior. + * + * Adjacent applications of [callbackFlow], [flowOn], [buffer], and [produceIn] are always fused so + * that only one properly configured channel is used for execution. + * + * Example of usage that converts a multi-shot callback API to a flow. For single-shot callbacks use + * [suspendCancellableCoroutine]. + * + * ``` + * fun flowFrom(api: CallbackBasedApi): Flow<T> = callbackFlow { + * val callback = object : Callback { // Implementation of some callback interface + * override fun onNextValue(value: T) { + * // To avoid blocking you can configure channel capacity using + * // either buffer(Channel.CONFLATED) or buffer(Channel.UNLIMITED) to avoid overfill + * trySendBlocking(value) + * .onFailure { throwable -> + * // Downstream has been cancelled or failed, can log here + * } + * } + * override fun onApiError(cause: Throwable) { + * cancel(CancellationException("API Error", cause)) + * } + * override fun onCompleted() = channel.close() + * } + * api.register(callback) + * /* + * * Suspends until either 'onCompleted'/'onApiError' from the callback is invoked + * * or flow collector is cancelled (e.g. by 'take(1)' or because a collector's coroutine was cancelled). + * * In both cases, callback will be properly unregistered. + * */ + * awaitClose { api.unregister(callback) } + * } + * ``` + * > The callback `register`/`unregister` methods provided by an external API must be thread-safe, + * > because `awaitClose` block can be called at any time due to asynchronous nature of + * > cancellation, even concurrently with the call of the callback. + * + * This builder is to be preferred over [callbackFlow], due to the latter's default configuration of + * using an internal buffer, negatively impacting system health. + * + * @see callbackFlow + */ +fun <T> conflatedCallbackFlow( + @BuilderInference block: suspend ProducerScope<T>.() -> Unit, +): Flow<T> = callbackFlow(block).conflate() + +/** + * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] provided to + * the builder's [block] of code via [ProducerScope]. It allows elements to be produced by code that + * is running in a different context or concurrently. The resulting flow is _cold_, which means that + * [block] is called every time a terminal operator is applied to the resulting flow. + * + * This builder ensures thread-safety and context preservation, thus the provided [ProducerScope] + * can be used concurrently from different contexts. The resulting flow completes as soon as the + * code in the [block] and all its children completes. Use [awaitClose] as the last statement to + * keep it running. A more detailed example is provided in the documentation of [callbackFlow]. + * + * A [conflated][conflate] channel is used. Use the [buffer] operator on the resulting flow to + * specify a user-defined value and to control what happens when data is produced faster than + * consumed, i.e. to control the back-pressure behavior. + * + * Adjacent applications of [channelFlow], [flowOn], [buffer], and [produceIn] are always fused so + * that only one properly configured channel is used for execution. + * + * Examples of usage: + * ``` + * fun <T> Flow<T>.merge(other: Flow<T>): Flow<T> = channelFlow { + * // collect from one coroutine and send it + * launch { + * collect { send(it) } + * } + * // collect and send from this coroutine, too, concurrently + * other.collect { send(it) } + * } + * + * fun <T> contextualFlow(): Flow<T> = channelFlow { + * // send from one coroutine + * launch(Dispatchers.IO) { + * send(computeIoValue()) + * } + * // send from another coroutine, concurrently + * launch(Dispatchers.Default) { + * send(computeCpuValue()) + * } + * } + * ``` + * + * This builder is to be preferred over [channelFlow], due to the latter's default configuration of + * using an internal buffer, negatively impacting system health. + * + * @see channelFlow + */ +fun <T> conflatedChannelFlow( + @BuilderInference block: suspend ProducerScope<T>.() -> Unit, +): Flow<T> = channelFlow(block).conflate() diff --git a/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt b/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt new file mode 100644 index 000000000000..5f8c66078483 --- /dev/null +++ b/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2024 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalTypeInference::class) + +package com.android.systemui.utils.coroutines.flow + +import kotlin.experimental.ExperimentalTypeInference +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.transformLatest + +/** + * Returns a flow that emits elements from the original flow transformed by [transform] function. + * When the original flow emits a new value, computation of the [transform] block for previous value + * is cancelled. + * + * For example, the following flow: + * ``` + * flow { + * emit("a") + * delay(100) + * emit("b") + * }.mapLatest { value -> + * println("Started computing $value") + * delay(200) + * "Computed $value" + * } + * ``` + * + * will print "Started computing a" and "Started computing b", but the resulting flow will contain + * only "Computed b" value. + * + * This operator is [conflated][conflate] by default, and as such should be preferred over usage of + * [mapLatest], due to the latter's default configuration of using an internal buffer, negatively + * impacting system health. + * + * @see mapLatest + */ +fun <T, R> Flow<T>.mapLatestConflated(@BuilderInference transform: suspend (T) -> R): Flow<R> = + mapLatest(transform).conflate() + +/** + * Returns a flow that switches to a new flow produced by [transform] function every time the + * original flow emits a value. When the original flow emits a new value, the previous flow produced + * by `transform` block is cancelled. + * + * For example, the following flow: + * ``` + * flow { + * emit("a") + * delay(100) + * emit("b") + * }.flatMapLatest { value -> + * flow { + * emit(value) + * delay(200) + * emit(value + "_last") + * } + * } + * ``` + * + * produces `a b b_last` + * + * This operator is [conflated][conflate] by default, and as such should be preferred over usage of + * [flatMapLatest], due to the latter's default configuration of using an internal buffer, + * negatively impacting system health. + * + * @see flatMapLatest + */ +fun <T, R> Flow<T>.flatMapLatestConflated( + @BuilderInference transform: suspend (T) -> Flow<R>, +): Flow<R> = flatMapLatest(transform).conflate() + +/** + * Returns a flow that produces element by [transform] function every time the original flow emits a + * value. When the original flow emits a new value, the previous `transform` block is cancelled, + * thus the name `transformLatest`. + * + * For example, the following flow: + * ``` + * flow { + * emit("a") + * delay(100) + * emit("b") + * }.transformLatest { value -> + * emit(value) + * delay(200) + * emit(value + "_last") + * } + * ``` + * + * produces `a b b_last`. + * + * This operator is [conflated][conflate] by default, and as such should be preferred over usage of + * [transformLatest], due to the latter's default configuration of using an internal buffer, + * negatively impacting system health. + * + * @see transformLatest + */ +fun <T, R> Flow<T>.transformLatestConflated( + @BuilderInference transform: suspend FlowCollector<R>.(T) -> Unit, +): Flow<R> = transformLatest(transform).conflate() diff --git a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java index 23e269a67283..cbbce1a74e94 100644 --- a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java +++ b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java @@ -19,6 +19,7 @@ package com.android.wallpaperbackup; import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; +import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_INELIGIBLE; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_METADATA; @@ -39,6 +40,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; +import android.graphics.BitmapFactory; import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManager; @@ -109,22 +111,16 @@ public class WallpaperBackupAgent extends BackupAgent { static final String LOCK_WALLPAPER_STAGE = "wallpaper-lock-stage"; @VisibleForTesting static final String WALLPAPER_INFO_STAGE = "wallpaper-info-stage"; - @VisibleForTesting static final String WALLPAPER_BACKUP_DEVICE_INFO_STAGE = "wallpaper-backup-device-info-stage"; - static final String EMPTY_SENTINEL = "empty"; static final String QUOTA_SENTINEL = "quota"; - // Shared preferences constants. static final String PREFS_NAME = "wbprefs.xml"; static final String SYSTEM_GENERATION = "system_gen"; static final String LOCK_GENERATION = "lock_gen"; - /** - * An approximate area threshold to compare device dimension similarity - */ - static final int AREA_THRESHOLD = 50; // TODO (b/327637867): determine appropriate threshold + static final float DEFAULT_ACCEPTABLE_PARALLAX = 0.2f; // If this file exists, it means we exceeded our quota last time private File mQuotaFile; @@ -336,7 +332,6 @@ public class WallpaperBackupAgent extends BackupAgent { mEventLogger.onSystemImageWallpaperBackupFailed(error); } - private void backupLockWallpaperFileIfItExists(SharedPreferences sharedPrefs, boolean lockChanged, int lockGeneration, FullBackupDataOutput data) throws IOException { final File lockImageStage = new File(getFilesDir(), LOCK_WALLPAPER_STAGE); @@ -409,6 +404,16 @@ public class WallpaperBackupAgent extends BackupAgent { } } + private static String readText(TypedXmlPullParser parser) + throws IOException, XmlPullParserException { + String result = ""; + if (parser.next() == XmlPullParser.TEXT) { + result = parser.getText(); + parser.nextTag(); + } + return result; + } + @VisibleForTesting // fullBackupFile is final, so we intercept backups here in tests. protected void backupFile(File file, FullBackupDataOutput data) { @@ -438,18 +443,10 @@ public class WallpaperBackupAgent extends BackupAgent { boolean lockImageStageExists = lockImageStage.exists(); try { - // Parse the device dimensions of the source device and compare with target to - // to identify whether we need to skip the remainder of the restore process + // Parse the device dimensions of the source device Pair<Point, Point> sourceDeviceDimensions = parseDeviceDimensions( deviceDimensionsStage); - Point targetDeviceDimensions = getScreenDimensions(); - if (sourceDeviceDimensions != null && targetDeviceDimensions != null - && isSourceDeviceSignificantlySmallerThanTarget(sourceDeviceDimensions.first, - targetDeviceDimensions)) { - Slog.d(TAG, "The source device is significantly smaller than target"); - } - // First parse the live component name so that we know for logging if we care about // logging errors with the image restore. ComponentName wpService = parseWallpaperComponent(infoStage, "wp"); @@ -466,9 +463,10 @@ public class WallpaperBackupAgent extends BackupAgent { // to back up the original image on the source device, or there was no user-supplied // wallpaper image present. if (lockImageStageExists) { - restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK); + restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK, + sourceDeviceDimensions); } - restoreFromStage(imageStage, infoStage, "wp", sysWhich); + restoreFromStage(imageStage, infoStage, "wp", sysWhich, sourceDeviceDimensions); // And reset to the wallpaper service we should be using if (mLockHasLiveComponent) { @@ -543,16 +541,6 @@ public class WallpaperBackupAgent extends BackupAgent { } } - private static String readText(TypedXmlPullParser parser) - throws IOException, XmlPullParserException { - String result = ""; - if (parser.next() == XmlPullParser.TEXT) { - result = parser.getText(); - parser.nextTag(); - } - return result; - } - @VisibleForTesting void updateWallpaperComponent(ComponentName wpService, int which) throws IOException { @@ -578,10 +566,13 @@ public class WallpaperBackupAgent extends BackupAgent { } } - private void restoreFromStage(File stage, File info, String hintTag, int which) + private void restoreFromStage(File stage, File info, String hintTag, int which, + Pair<Point, Point> sourceDeviceDimensions) throws IOException { if (stage.exists()) { if (multiCrop()) { + // TODO(b/332937943): implement offset adjustment by manually adjusting crop to + // adhere to device aspect ratio SparseArray<Rect> cropHints = parseCropHints(info, hintTag); if (cropHints != null) { Slog.i(TAG, "Got restored wallpaper; applying which=" + which @@ -601,7 +592,6 @@ public class WallpaperBackupAgent extends BackupAgent { } return; } - // Parse the restored info file to find the crop hint. Note that this currently // relies on a priori knowledge of the wallpaper info file schema. Rect cropHint = parseCropHint(info, hintTag); @@ -609,8 +599,33 @@ public class WallpaperBackupAgent extends BackupAgent { Slog.i(TAG, "Got restored wallpaper; applying which=" + which + "; cropHint = " + cropHint); try (FileInputStream in = new FileInputStream(stage)) { - mWallpaperManager.setStream(in, cropHint.isEmpty() ? null : cropHint, true, - which); + + if (sourceDeviceDimensions != null && sourceDeviceDimensions.first != null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + ParcelFileDescriptor pdf = ParcelFileDescriptor.open(stage, MODE_READ_ONLY); + BitmapFactory.decodeFileDescriptor(pdf.getFileDescriptor(), + null, options); + Point bitmapSize = new Point(options.outWidth, options.outHeight); + Point sourceDeviceSize = new Point(sourceDeviceDimensions.first.x, + sourceDeviceDimensions.first.y); + Point targetDeviceDimensions = getScreenDimensions(); + + // TODO: for now we handle only the case where the target device has smaller + // aspect ratio than the source device i.e. the target device is more narrow + // than the source device + if (isTargetMoreNarrowThanSource(targetDeviceDimensions, + sourceDeviceSize)) { + Rect adjustedCrop = findNewCropfromOldCrop(cropHint, + sourceDeviceDimensions.first, true, targetDeviceDimensions, + bitmapSize, true); + + cropHint.set(adjustedCrop); + } + } + + mWallpaperManager.setStream(in, cropHint.isEmpty() ? null : cropHint, + true, which); // And log the success if ((which & FLAG_SYSTEM) > 0) { @@ -629,6 +644,209 @@ public class WallpaperBackupAgent extends BackupAgent { } } + /** + * This method computes the crop of the stored wallpaper to preserve its center point as the + * user had set it in the previous device. + * + * The algorithm involves first computing the original crop of the user (without parallax). Then + * manually adjusting the user's original crop to respect the current device's aspect ratio + * (thereby preserving the center point). Then finally, adding any leftover image real-estate + * (i.e. space left over on the horizontal axis) to add parallax effect. Parallax is only added + * if was present in the old device's settings. + * + */ + private Rect findNewCropfromOldCrop(Rect oldCrop, Point oldDisplaySize, boolean oldRtl, + Point newDisplaySize, Point bitmapSize, boolean newRtl) { + Rect cropWithoutParallax = withoutParallax(oldCrop, oldDisplaySize, oldRtl, bitmapSize); + oldCrop = oldCrop.isEmpty() ? new Rect(0, 0, bitmapSize.x, bitmapSize.y) : oldCrop; + float oldParallaxAmount = ((float) oldCrop.width() / cropWithoutParallax.width()) - 1; + + Rect newCropWithSameCenterWithoutParallax = sameCenter(newDisplaySize, bitmapSize, + cropWithoutParallax); + + Rect newCrop = newCropWithSameCenterWithoutParallax; + + // calculate the amount of left-over space there is in the image after adjusting the crop + // from the above operation i.e. in a rtl configuration, this is the remaining space in the + // image after subtracting the new crop's right edge coordinate from the image itself, and + // for ltr, its just the new crop's left edge coordinate (as it's the distance from the + // beginning of the image) + int widthAvailableForParallaxOnTheNewDevice = + (newRtl) ? newCrop.left : bitmapSize.x - newCrop.right; + + // calculate relatively how much this available space is as a fraction of the total cropped + // image + float availableParallaxAmount = + (float) widthAvailableForParallaxOnTheNewDevice / newCrop.width(); + + float minAcceptableParallax = Math.min(DEFAULT_ACCEPTABLE_PARALLAX, oldParallaxAmount); + + if (DEBUG) { + Slog.d(TAG, "- cropWithoutParallax: " + cropWithoutParallax); + Slog.d(TAG, "- oldParallaxAmount: " + oldParallaxAmount); + Slog.d(TAG, "- newCropWithSameCenterWithoutParallax: " + + newCropWithSameCenterWithoutParallax); + Slog.d(TAG, "- widthAvailableForParallaxOnTheNewDevice: " + + widthAvailableForParallaxOnTheNewDevice); + Slog.d(TAG, "- availableParallaxAmount: " + availableParallaxAmount); + Slog.d(TAG, "- minAcceptableParallax: " + minAcceptableParallax); + Slog.d(TAG, "- oldCrop: " + oldCrop); + Slog.d(TAG, "- oldDisplaySize: " + oldDisplaySize); + Slog.d(TAG, "- oldRtl: " + oldRtl); + Slog.d(TAG, "- newDisplaySize: " + newDisplaySize); + Slog.d(TAG, "- bitmapSize: " + bitmapSize); + Slog.d(TAG, "- newRtl: " + newRtl); + } + if (availableParallaxAmount >= minAcceptableParallax) { + // but in any case, don't put more parallax than the amount of the old device + float parallaxToAdd = Math.min(availableParallaxAmount, oldParallaxAmount); + + int widthToAddForParallax = (int) (newCrop.width() * parallaxToAdd); + if (DEBUG) { + Slog.d(TAG, "- parallaxToAdd: " + parallaxToAdd); + Slog.d(TAG, "- widthToAddForParallax: " + widthToAddForParallax); + } + if (newRtl) { + newCrop.left -= widthToAddForParallax; + } else { + newCrop.right += widthToAddForParallax; + } + } + return newCrop; + } + + /** + * This method computes the original crop of the user without parallax. + * + * NOTE: When the user sets the wallpaper with a specific crop, there may additional image added + * to the crop to support parallax. In order to determine the user's actual crop the parallax + * must be removed if it exists. + */ + Rect withoutParallax(Rect crop, Point displaySize, boolean rtl, Point bitmapSize) { + // in the case an image's crop is not set, we assume the image itself is cropped + if (crop.isEmpty()) { + crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + } + + if (DEBUG) { + Slog.w(TAG, "- crop: " + crop); + } + + Rect adjustedCrop = new Rect(crop); + float suggestedDisplayRatio = (float) displaySize.x / displaySize.y; + + // here we calculate the width of the wallpaper image such that it has the same aspect ratio + // as the given display i.e. the width of the image on a single page of the device without + // parallax (i.e. displaySize will correspond to the display the crop was originally set on) + int wallpaperWidthWithoutParallax = (int) (0.5f + (float) displaySize.x * crop.height() + / displaySize.y); + // subtracting wallpaperWidthWithoutParallax from the wallpaper crop gives the amount of + // parallax added + int widthToRemove = Math.max(0, crop.width() - wallpaperWidthWithoutParallax); + + if (DEBUG) { + Slog.d(TAG, "- adjustedCrop: " + adjustedCrop); + Slog.d(TAG, "- suggestedDisplayRatio: " + suggestedDisplayRatio); + Slog.d(TAG, "- wallpaperWidthWithoutParallax: " + wallpaperWidthWithoutParallax); + Slog.d(TAG, "- widthToRemove: " + widthToRemove); + } + if (rtl) { + adjustedCrop.left += widthToRemove; + } else { + adjustedCrop.right -= widthToRemove; + } + + if (DEBUG) { + Slog.d(TAG, "- adjustedCrop: " + crop); + } + return adjustedCrop; + } + + /** + * This method computes a new crop based on the given crop in order to preserve the center point + * of the given crop on the provided displaySize. This is only for the case where the device + * displaySize has a smaller aspect ratio than the cropped image. + * + * NOTE: If the width to height ratio is less in the device display than cropped image + * this means the aspect ratios are off and there will be distortions in the image + * if the image is applied to the current display (i.e. the image will be skewed -> + * pixels in the image will not align correctly with the same pixels in the image that are + * above them) + */ + Rect sameCenter(Point displaySize, Point bitmapSize, Rect crop) { + + // in the case an image's crop is not set, we assume the image itself is cropped + if (crop.isEmpty()) { + crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + } + + float screenRatio = (float) displaySize.x / displaySize.y; + float cropRatio = (float) crop.width() / crop.height(); + + Rect adjustedCrop = new Rect(crop); + + if (screenRatio < cropRatio) { + // the screen is more narrow than the image, and as such, the image will need to be + // zoomed in till it fits in the vertical axis. Due to this, we need to manually adjust + // the image's crop in order for it to fit into the screen without having the framework + // do it (since the framework left aligns the image after zooming) + + // Calculate the height of the adjusted wallpaper crop so it respects the aspect ratio + // of the device. To calculate the height, we will use the width of the current crop. + // This is so we find the largest height possible which also respects the device aspect + // ratio. + int heightToAdd = (int) (0.5f + crop.width() / screenRatio - crop.height()); + + // Calculate how much extra image space available that can be used to adjust + // the crop. If this amount is less than heightToAdd, from above, then that means we + // can't use heightToAdd. Instead we will need to use the maximum possible height, which + // is the height of the original bitmap. NOTE: the bitmap height may be different than + // the crop. + // since there is no guarantee to have height available on both sides + // (e.g. the available height might be fully at the bottom), grab the minimum + int availableHeight = 2 * Math.min(crop.top, bitmapSize.y - crop.bottom); + int actualHeightToAdd = Math.min(heightToAdd, availableHeight); + + // half of the additional height is added to the top and bottom of the crop + adjustedCrop.top -= actualHeightToAdd / 2 + actualHeightToAdd % 2; + adjustedCrop.bottom += actualHeightToAdd / 2; + + // Calculate the width of the adjusted crop. Initially we used the fixed width of the + // crop to calculate the heightToAdd, but since this height may be invalid (based on + // the calculation above) we calculate the width again instead of using the fixed width, + // using the adjustedCrop's updated height. + int widthToRemove = (int) (0.5f + crop.width() - adjustedCrop.height() * screenRatio); + + // half of the additional width is subtracted from the left and right side of the crop + int widthToRemoveLeft = widthToRemove / 2; + int widthToRemoveRight = widthToRemove / 2 + widthToRemove % 2; + + adjustedCrop.left += widthToRemoveLeft; + adjustedCrop.right -= widthToRemoveRight; + + if (DEBUG) { + Slog.d(TAG, "cropRatio: " + cropRatio); + Slog.d(TAG, "screenRatio: " + screenRatio); + Slog.d(TAG, "heightToAdd: " + heightToAdd); + Slog.d(TAG, "actualHeightToAdd: " + actualHeightToAdd); + Slog.d(TAG, "availableHeight: " + availableHeight); + Slog.d(TAG, "widthToRemove: " + widthToRemove); + Slog.d(TAG, "adjustedCrop: " + adjustedCrop); + } + + return adjustedCrop; + } + + return adjustedCrop; + } + + private boolean isTargetMoreNarrowThanSource(Point targetDisplaySize, Point srcDisplaySize) { + float targetScreenRatio = (float) targetDisplaySize.x / targetDisplaySize.y; + float srcScreenRatio = (float) srcDisplaySize.x / srcDisplaySize.y; + + return (targetScreenRatio < srcScreenRatio); + } + private void logRestoreErrorIfNoLiveComponent(int which, String error) { if (mSystemHasLiveComponent) { return; @@ -644,6 +862,7 @@ public class WallpaperBackupAgent extends BackupAgent { mEventLogger.onLockImageWallpaperRestoreFailed(error); } } + private Rect parseCropHint(File wallpaperInfo, String sectionTag) { Rect cropHint = new Rect(); try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { @@ -681,7 +900,7 @@ public class WallpaperBackupAgent extends BackupAgent { if (type != XmlPullParser.START_TAG) continue; String tag = parser.getName(); if (!sectionTag.equals(tag)) continue; - for (Pair<Integer, String> pair: List.of( + for (Pair<Integer, String> pair : List.of( new Pair<>(WallpaperManager.PORTRAIT, "Portrait"), new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"), new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"), @@ -907,22 +1126,6 @@ public class WallpaperBackupAgent extends BackupAgent { return internalDisplays; } - /** - * This method compares the source and target dimensions, and returns true if there is a - * significant difference in area between them and the source dimensions are smaller than the - * target dimensions. - * - * @param sourceDimensions is the dimensions of the source device - * @param targetDimensions is the dimensions of the target device - */ - @VisibleForTesting - boolean isSourceDeviceSignificantlySmallerThanTarget(Point sourceDimensions, - Point targetDimensions) { - int rawAreaDelta = (targetDimensions.x * targetDimensions.y) - - (sourceDimensions.x * sourceDimensions.y); - return rawAreaDelta > AREA_THRESHOLD; - } - @VisibleForTesting boolean isDeviceInRestore() { try { diff --git a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java index ec9223c7d667..3ecdf3f101a5 100644 --- a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java +++ b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java @@ -59,7 +59,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.graphics.Point; import android.graphics.Rect; import android.os.FileUtils; import android.os.ParcelFileDescriptor; @@ -841,26 +840,6 @@ public class WallpaperBackupAgentTest { testParseCropHints(testMap); } - @Test - public void test_sourceDimensionsAreLargerThanTarget() { - // source device is larger than target, expecting to get false - Point sourceDimensions = new Point(2208, 1840); - Point targetDimensions = new Point(1080, 2092); - boolean isSourceSmaller = mWallpaperBackupAgent - .isSourceDeviceSignificantlySmallerThanTarget(sourceDimensions, targetDimensions); - assertThat(isSourceSmaller).isEqualTo(false); - } - - @Test - public void test_sourceDimensionsMuchSmallerThanTarget() { - // source device is smaller than target, expecting to get true - Point sourceDimensions = new Point(1080, 2092); - Point targetDimensions = new Point(2208, 1840); - boolean isSourceSmaller = mWallpaperBackupAgent - .isSourceDeviceSignificantlySmallerThanTarget(sourceDimensions, targetDimensions); - assertThat(isSourceSmaller).isEqualTo(true); - } - private void testParseCropHints(Map<Integer, Rect> testMap) throws Exception { assumeTrue(multiCrop()); mockRestoredStaticWallpaperFile(testMap); diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 3f3ff4a46edf..3a384065217e 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -5188,11 +5188,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState String[] exception = resultData.getStringArray( CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION); if (exception != null && exception.length >= 2) { + String errType = exception[0]; + String errMsg = exception[1]; Slog.w(TAG, "Credman bottom sheet from pinned " - + "entry failed with: + " + exception[0] + " , " - + exception[1]); + + "entry failed with: + " + errType + " , " + + errMsg); sendCredentialManagerResponseToApp(/*response=*/ null, - new GetCredentialException(exception[0], exception[1]), + new GetCredentialException(errType, errMsg), mAutofillId); } } else { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 6b33199ec230..96f525a55660 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1135,8 +1135,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (!isCurrentUser) { return; } - mSettings = queryInputMethodServicesInternal(mContext, userId, - newAdditionalSubtypeMap, DirectBootAwareness.AUTO); + mSettings = newSettings; postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); boolean changed = false; @@ -1540,9 +1539,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // and user switch would not happen at that time. resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_USER); - final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, - newUserId, AdditionalSubtypeMapRepository.get(newUserId), DirectBootAwareness.AUTO); - InputMethodSettingsRepository.put(newUserId, newSettings); + final InputMethodSettings newSettings = InputMethodSettingsRepository.get(newUserId); mSettings = newSettings; postInputMethodSettingUpdatedLocked(initialUserSwitch /* resetDefaultEnabledIme */); if (TextUtils.isEmpty(mSettings.getSelectedInputMethod())) { diff --git a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java index bd73cb6544f0..1938642ef396 100644 --- a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java +++ b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java @@ -23,9 +23,15 @@ import static android.media.AudioAttributes.USAGE_NOTIFICATION; import android.app.Notification; import android.app.NotificationChannel; +import android.compat.annotation.ChangeId; +import android.compat.annotation.LoggingOnly; import android.content.Context; import android.media.AudioAttributes; +import android.os.Binder; +import android.os.RemoteException; +import android.os.ServiceManager; import android.util.Slog; +import com.android.internal.compat.IPlatformCompat; /** * Stores the latest notification channel information for this notification @@ -34,14 +40,26 @@ public class NotificationChannelExtractor implements NotificationSignalExtractor private static final String TAG = "ChannelExtractor"; private static final boolean DBG = false; + /** + * Corrects audio attributes for notifications based on characteristics of the notifications. + */ + @ChangeId + @LoggingOnly + static final long RESTRICT_AUDIO_ATTRIBUTES = 331793339L; + private RankingConfig mConfig; private Context mContext; + private IPlatformCompat mPlatformCompat; public void initialize(Context ctx, NotificationUsageStats usageStats) { mContext = ctx; if (DBG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + "."); } + public void setCompatChangeLogger(IPlatformCompat platformCompat) { + mPlatformCompat = platformCompat; + } + public RankingReconsideration process(NotificationRecord record) { if (record == null || record.getNotification() == null) { if (DBG) Slog.d(TAG, "skipping empty notification"); @@ -80,6 +98,7 @@ public class NotificationChannelExtractor implements NotificationSignalExtractor } if (updateAttributes) { + reportAudioAttributesChanged(record.getUid()); NotificationChannel clone = record.getChannel().copy(); clone.setSound(clone.getSound(), new AudioAttributes.Builder(attributes) .setUsage(USAGE_NOTIFICATION) @@ -91,6 +110,17 @@ public class NotificationChannelExtractor implements NotificationSignalExtractor return null; } + private void reportAudioAttributesChanged(int uid) { + final long id = Binder.clearCallingIdentity(); + try { + mPlatformCompat.reportChangeByUid(RESTRICT_AUDIO_ATTRIBUTES, uid); + } catch (RemoteException e) { + Slog.e(TAG, "Unexpected exception while reporting to changecompat", e); + } finally { + Binder.restoreCallingIdentity(id); + } + } + @Override public void setConfig(RankingConfig config) { mConfig = config; diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 8075ae0e4d61..6f27f6b7be6c 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2508,12 +2508,8 @@ public class NotificationManagerService extends SystemService { mAppOps, mUserProfiles, mShowReviewPermissionsNotification); - mRankingHelper = new RankingHelper(getContext(), - mRankingHandler, - mPreferencesHelper, - mZenModeHelper, - mUsageStats, - extractorNames); + mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper, + mZenModeHelper, mUsageStats, extractorNames, mPlatformCompat); mSnoozeHelper = snoozeHelper; mGroupHelper = groupHelper; mHistoryManager = historyManager; diff --git a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java index 24c1d5966020..f0358d1e1d8c 100644 --- a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java +++ b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java @@ -17,6 +17,7 @@ package com.android.server.notification; import android.content.Context; +import com.android.internal.compat.IPlatformCompat; /** * Extracts signals that will be useful to the {@link NotificationComparator} and caches them @@ -52,4 +53,6 @@ public interface NotificationSignalExtractor { * DND. */ void setZenHelper(ZenModeHelper helper); + + default void setCompatChangeLogger(IPlatformCompat platformCompat){}; } diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 50ca984dcf57..461bd9c0663b 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -1387,8 +1387,7 @@ public class PreferencesHelper implements RankingConfig { public void updateFixedImportance(List<UserInfo> users) { for (UserInfo user : users) { List<PackageInfo> packages = mPm.getInstalledPackagesAsUser( - PackageManager.PackageInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY), - user.getUserHandle().getIdentifier()); + 0, user.getUserHandle().getIdentifier()); for (PackageInfo pi : packages) { boolean fixed = mPermissionHelper.isPermissionFixed( pi.packageName, user.getUserHandle().getIdentifier()); diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java index 68e0eaaf31cd..77568015fe79 100644 --- a/services/core/java/com/android/server/notification/RankingHelper.java +++ b/services/core/java/com/android/server/notification/RankingHelper.java @@ -15,6 +15,9 @@ */ package com.android.server.notification; +import static android.app.Flags.restrictAudioAttributesAlarm; +import static android.app.Flags.restrictAudioAttributesCall; +import static android.app.Flags.restrictAudioAttributesMedia; import static android.app.Flags.sortSectionByTime; import static android.app.NotificationManager.IMPORTANCE_MIN; import static android.text.TextUtils.formatSimple; @@ -27,6 +30,7 @@ import android.util.ArrayMap; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import com.android.internal.compat.IPlatformCompat; import com.android.tools.r8.keepanno.annotations.KeepItemKind; import com.android.tools.r8.keepanno.annotations.KeepTarget; import com.android.tools.r8.keepanno.annotations.UsesReflection; @@ -56,7 +60,8 @@ public class RankingHelper { methodName = "<init>") }) public RankingHelper(Context context, RankingHandler rankingHandler, RankingConfig config, - ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames) { + ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames, + IPlatformCompat platformCompat) { mContext = context; mRankingHandler = rankingHandler; if (sortSectionByTime()) { @@ -75,6 +80,10 @@ public class RankingHelper { extractor.initialize(mContext, usageStats); extractor.setConfig(config); extractor.setZenHelper(zenHelper); + if (restrictAudioAttributesAlarm() || restrictAudioAttributesMedia() + || restrictAudioAttributesCall()) { + extractor.setCompatChangeLogger(platformCompat); + } mSignalExtractors[i] = extractor; } catch (ClassNotFoundException e) { Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e); diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index d060c7ca3034..54cb9c9a9a9b 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -4885,7 +4885,6 @@ public class BatteryStatsImpl extends BatteryStats { if (type == WAKE_TYPE_PARTIAL) { // Only care about partial wake locks, since full wake locks // will be canceled when the user puts the screen to sleep. - aggregateLastWakeupUptimeLocked(elapsedRealtimeMs, uptimeMs); if (historyName == null) { historyName = name; } @@ -5205,20 +5204,14 @@ public class BatteryStatsImpl extends BatteryStats { } @GuardedBy("this") - void aggregateLastWakeupUptimeLocked(long elapsedRealtimeMs, long uptimeMs) { + public void noteWakeupReasonLocked(String reason, long elapsedRealtimeMs, long uptimeMs) { if (mLastWakeupReason != null) { long deltaUptimeMs = uptimeMs - mLastWakeupUptimeMs; SamplingTimer timer = getWakeupReasonTimerLocked(mLastWakeupReason); timer.add(deltaUptimeMs * 1000, 1, elapsedRealtimeMs); // time in in microseconds mFrameworkStatsLogger.kernelWakeupReported(deltaUptimeMs * 1000, mLastWakeupReason, mLastWakeupElapsedTimeMs); - mLastWakeupReason = null; } - } - - @GuardedBy("this") - public void noteWakeupReasonLocked(String reason, long elapsedRealtimeMs, long uptimeMs) { - aggregateLastWakeupUptimeLocked(elapsedRealtimeMs, uptimeMs); mHistory.recordWakeupEvent(elapsedRealtimeMs, uptimeMs, reason); mLastWakeupReason = reason; mLastWakeupUptimeMs = uptimeMs; diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index ed88b5a7c449..143605ac7320 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -2446,6 +2446,9 @@ public class WindowManagerService extends IWindowManager.Stub ProtoLog.i(WM_DEBUG_SCREEN_ON, "Relayout %s: oldVis=%d newVis=%d. %s", win, oldVisibility, viewVisibility, new RuntimeException().fillInStackTrace()); + if (becameVisible) { + onWindowVisible(win); + } win.setDisplayLayoutNeeded(); win.mGivenInsetsPending = (flags & WindowManagerGlobal.RELAYOUT_INSETS_PENDING) != 0; @@ -10168,7 +10171,7 @@ public class WindowManagerService extends IWindowManager.Stub * Called to notify WMS that the specified window has become visible. This shows a Toast if the * window is deemed to hold sensitive content. */ - void onWindowVisible(@NonNull WindowState w) { + private void onWindowVisible(@NonNull WindowState w) { showToastIfBlockingScreenCapture(w); } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index d3baedc6e2a1..2fcee50e6f85 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -28,7 +28,6 @@ import static android.graphics.GraphicsProtos.dumpPointProto; import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.PowerManager.DRAW_WAKE_LOCK; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.permission.flags.Flags.sensitiveContentImprovements; import static android.view.SurfaceControl.Transaction; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME; @@ -2139,9 +2138,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } } setDisplayLayoutNeeded(); - if (sensitiveContentImprovements() && visible) { - mWmService.onWindowVisible(this); - } } } diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java index dedb687cff22..b1673e2c4c3c 100644 --- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java @@ -26,6 +26,7 @@ import android.credentials.CredentialManager; import android.credentials.CredentialProviderInfo; import android.credentials.GetCandidateCredentialsException; import android.credentials.GetCandidateCredentialsResponse; +import android.credentials.GetCredentialException; import android.credentials.GetCredentialRequest; import android.credentials.GetCredentialResponse; import android.credentials.IGetCandidateCredentialsCallback; @@ -159,24 +160,26 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ public void onFinalErrorReceived(ComponentName componentName, String errorType, String message) { Slog.d(TAG, "onFinalErrorReceived"); + if (GetCredentialException.TYPE_USER_CANCELED.equals(errorType)) { + Slog.d(TAG, "User canceled but session is not being terminated"); + return; + } respondToFinalReceiverWithFailureAndFinish(errorType, message); } @Override public void onUiCancellation(boolean isUserCancellation) { - String exception = GetCandidateCredentialsException.TYPE_USER_CANCELED; - String message = "User cancelled the selector"; - if (!isUserCancellation) { - exception = GetCandidateCredentialsException.TYPE_INTERRUPTED; - message = "The UI was interrupted - please try again."; - } - mRequestSessionMetric.collectFrameworkException(exception); - respondToFinalReceiverWithFailureAndFinish(exception, message); + Slog.d(TAG, "User canceled but session is not being terminated"); } private void respondToFinalReceiverWithFailureAndFinish( String exception, String message ) { + if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { + Slog.w(TAG, "Request has already been completed. This is strange."); + return; + } + if (mAutofillCallback != null) { Bundle resultData = new Bundle(); resultData.putStringArray( @@ -221,6 +224,19 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ public void onFinalResponseReceived(ComponentName componentName, GetCredentialResponse response) { Slog.d(TAG, "onFinalResponseReceived"); + if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { + Slog.w(TAG, "Request has already been completed. This is strange."); + return; + } + respondToFinalReceiverWithResponseAndFinish(response); + } + + private void respondToFinalReceiverWithResponseAndFinish(GetCredentialResponse response) { + if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { + Slog.w(TAG, "Request has already been completed. This is strange."); + return; + } + if (this.mAutofillCallback != null) { Slog.d(TAG, "onFinalResponseReceived sending through final receiver"); Bundle resultData = new Bundle(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java index ad25d76e2db7..770712a191fd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java @@ -26,14 +26,18 @@ import static android.media.AudioAttributes.USAGE_NOTIFICATION; import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; import static android.media.AudioAttributes.USAGE_UNKNOWN; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; +import static com.android.server.notification.NotificationChannelExtractor.RESTRICT_AUDIO_ATTRIBUTES; import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Flags; @@ -43,12 +47,14 @@ import android.app.PendingIntent; import android.app.Person; import android.media.AudioAttributes; import android.net.Uri; +import android.os.RemoteException; import android.os.UserHandle; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.service.notification.StatusBarNotification; +import com.android.internal.compat.IPlatformCompat; import com.android.server.UiServiceTestCase; import org.junit.Before; @@ -60,6 +66,8 @@ import org.mockito.MockitoAnnotations; public class NotificationChannelExtractorTest extends UiServiceTestCase { @Mock RankingConfig mConfig; + @Mock + IPlatformCompat mPlatformCompat; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @@ -73,6 +81,7 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { mExtractor = new NotificationChannelExtractor(); mExtractor.setConfig(mConfig); mExtractor.initialize(mContext, null); + mExtractor.setCompatChangeLogger(mPlatformCompat); } private NotificationRecord getRecord(NotificationChannel channel, Notification n) { @@ -82,7 +91,7 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { } @Test - public void testExtractsUpdatedConversationChannel() { + public void testExtractsUpdatedConversationChannel() throws RemoteException { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW); final Notification n = new Notification.Builder(getContext()) .setContentTitle("foo") @@ -101,7 +110,7 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { } @Test - public void testInvalidShortcutFlagEnabled_looksUpCorrectNonChannel() { + public void testInvalidShortcutFlagEnabled_looksUpCorrectNonChannel() throws RemoteException { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW); final Notification n = new Notification.Builder(getContext()) .setContentTitle("foo") @@ -122,7 +131,7 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { } @Test - public void testInvalidShortcutFlagDisabled_looksUpCorrectChannel() { + public void testInvalidShortcutFlagDisabled_looksUpCorrectChannel() throws RemoteException { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW); final Notification n = new Notification.Builder(getContext()) .setContentTitle("foo") @@ -143,7 +152,7 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { @Test @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_CALL) - public void testAudioAttributes_callStyleCanUseCallUsage() { + public void testAudioAttributes_callStyleCanUseCallUsage() throws RemoteException { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH); channel.setSound(Uri.EMPTY, new AudioAttributes.Builder() .setUsage(USAGE_NOTIFICATION_RINGTONE) @@ -162,11 +171,12 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { assertThat(mExtractor.process(r)).isNull(); assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION_RINGTONE); assertThat(r.getChannel()).isEqualTo(channel); + verify(mPlatformCompat, never()).reportChangeByUid(anyLong(), anyInt()); } @Test @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_CALL) - public void testAudioAttributes_nonCallStyleCannotUseCallUsage() { + public void testAudioAttributes_nonCallStyleCannotUseCallUsage() throws RemoteException { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH); channel.setSound(Uri.EMPTY, new AudioAttributes.Builder() .setUsage(USAGE_NOTIFICATION_RINGTONE) @@ -180,13 +190,14 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { assertThat(mExtractor.process(r)).isNull(); // instance updated assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION); + verify(mPlatformCompat).reportChangeByUid(RESTRICT_AUDIO_ATTRIBUTES, r.getUid()); // in-memory channel unchanged assertThat(channel.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION_RINGTONE); } @Test @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_ALARM) - public void testAudioAttributes_alarmCategoryCanUseAlarmUsage() { + public void testAudioAttributes_alarmCategoryCanUseAlarmUsage() throws RemoteException { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH); channel.setSound(Uri.EMPTY, new AudioAttributes.Builder() .setUsage(USAGE_ALARM) @@ -201,11 +212,12 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { assertThat(mExtractor.process(r)).isNull(); assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_ALARM); assertThat(r.getChannel()).isEqualTo(channel); + verify(mPlatformCompat, never()).reportChangeByUid(anyLong(), anyInt()); } @Test @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_ALARM) - public void testAudioAttributes_nonAlarmCategoryCannotUseAlarmUsage() { + public void testAudioAttributes_nonAlarmCategoryCannotUseAlarmUsage() throws RemoteException { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH); channel.setSound(Uri.EMPTY, new AudioAttributes.Builder() .setUsage(USAGE_ALARM) @@ -219,13 +231,14 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { assertThat(mExtractor.process(r)).isNull(); // instance updated assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION); + verify(mPlatformCompat).reportChangeByUid(RESTRICT_AUDIO_ATTRIBUTES, r.getUid()); // in-memory channel unchanged assertThat(channel.getAudioAttributes().getUsage()).isEqualTo(USAGE_ALARM); } @Test @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_MEDIA) - public void testAudioAttributes_noMediaUsage() { + public void testAudioAttributes_noMediaUsage() throws RemoteException { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH); channel.setSound(Uri.EMPTY, new AudioAttributes.Builder() .setUsage(USAGE_MEDIA) @@ -239,13 +252,14 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { assertThat(mExtractor.process(r)).isNull(); // instance updated assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION); + verify(mPlatformCompat).reportChangeByUid(RESTRICT_AUDIO_ATTRIBUTES, r.getUid()); // in-memory channel unchanged assertThat(channel.getAudioAttributes().getUsage()).isEqualTo(USAGE_MEDIA); } @Test @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_MEDIA) - public void testAudioAttributes_noUnknownUsage() { + public void testAudioAttributes_noUnknownUsage() throws RemoteException { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH); channel.setSound(Uri.EMPTY, new AudioAttributes.Builder() .setUsage(USAGE_UNKNOWN) @@ -259,6 +273,7 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { assertThat(mExtractor.process(r)).isNull(); // instance updated assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION); + verify(mPlatformCompat).reportChangeByUid(RESTRICT_AUDIO_ATTRIBUTES, r.getUid()); // in-memory channel unchanged assertThat(channel.getAudioAttributes().getUsage()).isEqualTo(USAGE_UNKNOWN); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index aeeca2ae86f5..5033a380fa4d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -3981,7 +3981,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { pm.applicationInfo = new ApplicationInfo(); pm.applicationInfo.uid = UID_O; List<PackageInfo> packages = ImmutableList.of(pm); - when(mPm.getInstalledPackagesAsUser(any(), anyInt())).thenReturn(packages); + when(mPm.getInstalledPackagesAsUser(eq(0), anyInt())).thenReturn(packages); mHelper.updateFixedImportance(users); assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O)); @@ -4097,7 +4097,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { pm.applicationInfo = new ApplicationInfo(); pm.applicationInfo.uid = UID_O; List<PackageInfo> packages = ImmutableList.of(pm); - when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages); + when(mPm.getInstalledPackagesAsUser(0, 0)).thenReturn(packages); mHelper.updateFixedImportance(users); assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false) @@ -4120,7 +4120,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { pm.applicationInfo = new ApplicationInfo(); pm.applicationInfo.uid = UID_O; List<PackageInfo> packages = ImmutableList.of(pm); - when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages); + when(mPm.getInstalledPackagesAsUser(0, 0)).thenReturn(packages); mHelper.updateFixedImportance(users); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); @@ -4309,7 +4309,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { pm.applicationInfo = new ApplicationInfo(); pm.applicationInfo.uid = UID_O; List<PackageInfo> packages = ImmutableList.of(pm); - when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages); + when(mPm.getInstalledPackagesAsUser(0, 0)).thenReturn(packages); mHelper.updateFixedImportance(users); ArraySet<String> toRemove = new ArraySet<>(); @@ -4341,7 +4341,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { pm.applicationInfo = new ApplicationInfo(); pm.applicationInfo.uid = UID_O; List<PackageInfo> packages = ImmutableList.of(pm); - when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages); + when(mPm.getInstalledPackagesAsUser(0, 0)).thenReturn(packages); mHelper.updateFixedImportance(users); assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O)); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java index ad420f6bf502..527001df995f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java @@ -55,6 +55,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.compat.IPlatformCompat; import com.android.server.UiServiceTestCase; import org.junit.Before; @@ -155,7 +156,8 @@ public class RankingHelperTest extends UiServiceTestCase { NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper, - mUsageStats, new String[] {ImportanceExtractor.class.getName()}); + mUsageStats, new String[] {ImportanceExtractor.class.getName()}, + mock(IPlatformCompat.class)); mNotiGroupGSortA = new Notification.Builder(mContext, TEST_CHANNEL_ID) .setContentTitle("A") |