diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-06-15 10:18:49 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-06-15 10:18:49 +0000 |
commit | f4fe78cee3015fc8bee644a580dc935e28b6431d (patch) | |
tree | 5d5ab5b75e521a32cd3cebe20209d9af46887f5a /src | |
parent | 22278c22053e18c26f995917810c5b6a0543ff89 (diff) | |
parent | c0b3a6241e521188758e361d21a4a693b49a1c66 (diff) |
Snap for 10323517 from c0b3a6241e521188758e361d21a4a693b49a1c66 to t-keystone-qcom-release
Change-Id: I2631b96e4cc1b448e2d561f21c2f188e13f7f6f6
Diffstat (limited to 'src')
91 files changed, 4486 insertions, 689 deletions
diff --git a/src/com/android/wallpaper/asset/LiveWallpaperThumbAsset.java b/src/com/android/wallpaper/asset/LiveWallpaperThumbAsset.java index 1f4fb674..08486a12 100755 --- a/src/com/android/wallpaper/asset/LiveWallpaperThumbAsset.java +++ b/src/com/android/wallpaper/asset/LiveWallpaperThumbAsset.java @@ -21,16 +21,23 @@ import android.content.res.AssetFileDescriptor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; import android.net.Uri; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.widget.ImageView; import androidx.annotation.WorkerThread; +import com.android.wallpaper.module.DrawableLayerResolver; +import com.android.wallpaper.module.InjectorProvider; + import com.bumptech.glide.Glide; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.MultiTransformation; @@ -59,6 +66,7 @@ public class LiveWallpaperThumbAsset extends Asset { protected final Context mContext; protected final android.app.WallpaperInfo mInfo; + protected final DrawableLayerResolver mLayerResolver; // The content Uri of thumbnail protected Uri mUri; private Drawable mThumbnailDrawable; @@ -66,6 +74,7 @@ public class LiveWallpaperThumbAsset extends Asset { public LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info) { mContext = context.getApplicationContext(); mInfo = info; + mLayerResolver = InjectorProvider.getInjector().getDrawableLayerResolver(); } public LiveWallpaperThumbAsset(Context context, android.app.WallpaperInfo info, Uri uri) { @@ -114,7 +123,25 @@ public class LiveWallpaperThumbAsset extends Asset { @Override public void decodeRawDimensions(Activity unused, DimensionsReceiver receiver) { - receiver.onDimensionsDecoded(null); + // TODO(b/277166654): Reuse the logic for all thumb asset decoding + sExecutorService.execute(() -> { + Bitmap result = null; + Drawable thumb = mInfo.loadThumbnail(mContext.getPackageManager()); + if (thumb instanceof BitmapDrawable) { + result = ((BitmapDrawable) thumb).getBitmap(); + } else if (thumb instanceof LayerDrawable) { + Drawable layer = mLayerResolver.resolveLayer((LayerDrawable) thumb); + if (layer instanceof BitmapDrawable) { + result = ((BitmapDrawable) layer).getBitmap(); + } + } + final Bitmap lr = result; + new Handler(Looper.getMainLooper()).post( + () -> + receiver.onDimensionsDecoded( + lr == null ? null : new Point(lr.getWidth(), lr.getHeight())) + ); + }); } @Override diff --git a/src/com/android/wallpaper/config/BaseFlags.kt b/src/com/android/wallpaper/config/BaseFlags.kt index 37f3d6e0..ffd76c69 100644 --- a/src/com/android/wallpaper/config/BaseFlags.kt +++ b/src/com/android/wallpaper/config/BaseFlags.kt @@ -16,24 +16,55 @@ package com.android.wallpaper.config import android.content.Context -import android.os.SystemProperties +import com.android.systemui.shared.customization.data.content.CustomizationProviderClient import com.android.systemui.shared.customization.data.content.CustomizationProviderClientImpl import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking abstract class BaseFlags { + var customizationProviderClient: CustomizationProviderClient? = null open fun isStagingBackdropContentEnabled() = false - open fun isEnableWallpaperEffect() = false - fun isMonochromaticFlagEnabled() = - SystemProperties.getBoolean("persist.sysui.monochromatic", false) - open fun isEnableEffectOnMultiplePanel() = false - open fun isFullscreenWallpaperPreview() = false - fun isUseRevampedUi(context: Context): Boolean { - return runBlocking { CustomizationProviderClientImpl(context, Dispatchers.IO).queryFlags() } + open fun isWallpaperEffectEnabled() = false + open fun isFullscreenWallpaperPreviewEnabled(context: Context): Boolean { + return runBlocking { getCustomizationProviderClient(context).queryFlags() } + .firstOrNull { flag -> + flag.name == Contract.FlagsTable.FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW + } + ?.value == true + } + fun isUseRevampedUiEnabled(context: Context): Boolean { + return runBlocking { getCustomizationProviderClient(context).queryFlags() } .firstOrNull { flag -> flag.name == Contract.FlagsTable.FLAG_NAME_REVAMPED_WALLPAPER_UI } ?.value == true } + fun isCustomClocksEnabled(context: Context): Boolean { + return runBlocking { getCustomizationProviderClient(context).queryFlags() } + .firstOrNull { flag -> + flag.name == Contract.FlagsTable.FLAG_NAME_CUSTOM_CLOCKS_ENABLED + } + ?.value == true + } + fun isMonochromaticThemeEnabled(context: Context): Boolean { + return runBlocking { getCustomizationProviderClient(context).queryFlags() } + .firstOrNull { flag -> flag.name == Contract.FlagsTable.FLAG_NAME_MONOCHROMATIC_THEME } + ?.value == true + } + + fun isAIWallpaperEnabled(context: Context): Boolean { + return runBlocking { getCustomizationProviderClient(context).queryFlags() } + .firstOrNull { flag -> + flag.name == Contract.FlagsTable.FLAG_NAME_WALLPAPER_PICKER_UI_FOR_AIWP + } + ?.value == true + } + + private fun getCustomizationProviderClient(context: Context): CustomizationProviderClient { + return customizationProviderClient + ?: CustomizationProviderClientImpl(context, Dispatchers.IO).also { + customizationProviderClient = it + } + } } diff --git a/src/com/android/wallpaper/effects/EffectsController.java b/src/com/android/wallpaper/effects/EffectsController.java index 8c745718..c1e9a145 100644 --- a/src/com/android/wallpaper/effects/EffectsController.java +++ b/src/com/android/wallpaper/effects/EffectsController.java @@ -15,6 +15,7 @@ */ package com.android.wallpaper.effects; +import android.content.Context; import android.net.Uri; import android.os.Bundle; @@ -22,6 +23,11 @@ import android.os.Bundle; * Utility class to provide methods to generate effects for the wallpaper. */ public abstract class EffectsController { + public static final int ERROR_ORIGINAL_DESTROY_CONTROLLER = -16; + public static final int ERROR_ORIGINAL_FINISH_ONGOING_SERVICE = -8; + public static final int ERROR_ORIGINAL_SERVICE_DISCONNECT = -4; + public static final int ERROR_ORIGINAL_TIME_OUT = -2; + public static final int RESULT_ORIGINAL_UNKNOWN = -1; public static final int RESULT_SUCCESS = 0; public static final int RESULT_ERROR_TRY_ANOTHER_PHOTO = 1; @@ -76,6 +82,14 @@ public abstract class EffectsController { } /** + * Triggers the effect. + * + * @param context the context + */ + public void triggerEffect(Context context) { + } + + /** * Interface to listen to different key moments of the connection with the Effects Service. */ public interface EffectsServiceListener { @@ -91,4 +105,14 @@ public abstract class EffectsController { void onEffectFinished(EffectEnumInterface effect, Bundle bundle, int error, int originalStatusCode, String errorMessage); } + + /** + * Gets whether the effect triggering is successful or not. + * + * @return whether the effect triggering is successful or not. + */ + public boolean isEffectTriggered() { + return false; + } + } diff --git a/src/com/android/wallpaper/model/Category.java b/src/com/android/wallpaper/model/Category.java index a182ea4e..beb349fc 100755 --- a/src/com/android/wallpaper/model/Category.java +++ b/src/com/android/wallpaper/model/Category.java @@ -154,6 +154,13 @@ public abstract class Category { return false; } + /** + * Returns whether this category supports content that can be added or removed dynamically. + */ + public boolean supportsWallpaperSetUpdates() { + return false; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Category)) return false; diff --git a/src/com/android/wallpaper/model/CustomizationSectionController.java b/src/com/android/wallpaper/model/CustomizationSectionController.java deleted file mode 100644 index 58ef178b..00000000 --- a/src/com/android/wallpaper/model/CustomizationSectionController.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.wallpaper.model; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import com.android.wallpaper.picker.SectionView; - -/** - * The interface for the behavior of section in the customization picker. - * - * @param <T> the {@link SectionView} to create for the section - */ -public interface CustomizationSectionController<T extends SectionView> { - - /** Interface for customization section navigation. */ - interface CustomizationSectionNavigationController { - /** Navigates to the given {@code fragment}. */ - void navigateTo(Fragment fragment); - - /** Navigates to a {@code fragment} that maps to the given destination ID. */ - void navigateTo(String destinationId); - } - - /** Returns {@code true} if the customization section is available. */ - boolean isAvailable(@Nullable Context context); - - /** - * Returns a newly created {@link SectionView} for the section. - * - * @param context The {@link Context} to inflate view. - * @param isOnLockScreen Whether we are on the lock screen. - */ - default T createView(Context context, boolean isOnLockScreen) { - return createView(context); - } - - /** - * Returns a newly created {@link SectionView} for the section. - * - * @param context the {@link Context} to inflate view - */ - T createView(Context context); - - /** Saves the view state for configuration changes. */ - default void onSaveInstanceState(Bundle savedInstanceState) {} - - /** Releases the controller. */ - default void release() {} - - /** Gets called when the section gets transitioned out. */ - default void onTransitionOut() {} - - /** Notifies when the screen was switched. */ - default void onScreenSwitched(boolean isOnLockScreen) {} -} diff --git a/src/com/android/wallpaper/model/CustomizationSectionController.kt b/src/com/android/wallpaper/model/CustomizationSectionController.kt new file mode 100644 index 00000000..76c140e3 --- /dev/null +++ b/src/com/android/wallpaper/model/CustomizationSectionController.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wallpaper.model + +import android.content.Context +import android.os.Bundle +import androidx.fragment.app.Fragment +import com.android.wallpaper.picker.SectionView + +/** + * The interface for the behavior of section in the customization picker. + * + * @param <T> the [SectionView] to create for the section </T> + */ +interface CustomizationSectionController<T : SectionView> { + /** Interface for customization section navigation. */ + interface CustomizationSectionNavigationController { + /** Navigates to the given `fragment`. */ + fun navigateTo(fragment: Fragment?) + + /** Navigates to a `fragment` that maps to the given destination ID. */ + fun navigateTo(destinationId: String?) + } + + data class ViewCreationParams( + /** Whether the view is being created in the context of the lock screen tab of the UI. */ + val isOnLockScreen: Boolean = false, + /** + * Whether the view is being created in the context of a bunch of "connected" sections that + * are laid out side-by-side in a horizontal layout. + */ + val isConnectedHorizontallyToOtherSections: Boolean = false, + ) + + /** + * It means that the creation of the controller can be expensive and we should avoid recreation + * in conditions like the user switching between the home and lock screen. + */ + @JvmDefault + fun shouldRetainInstanceWhenSwitchingTabs(): Boolean { + return false + } + + /** Returns `true` if the customization section is available. */ + fun isAvailable(context: Context): Boolean + + /** + * Returns a newly created [SectionView] for the section. + * + * @param context The [Context] to inflate view. + * @param params Parameters for the creation of the view. + */ + @JvmDefault + fun createView(context: Context, params: ViewCreationParams): T { + return createView(context) + } + + /** + * Returns a newly created [SectionView] for the section. + * + * @param context the [Context] to inflate view + */ + fun createView(context: Context): T + + /** Saves the view state for configuration changes. */ + @JvmDefault fun onSaveInstanceState(savedInstanceState: Bundle) = Unit + + /** Releases the controller. */ + @JvmDefault fun release() = Unit + + /** Gets called when the section gets transitioned out. */ + @JvmDefault fun onTransitionOut() = Unit + + /** Notifies when the screen was switched. */ + @JvmDefault fun onScreenSwitched(isOnLockScreen: Boolean) = Unit +} diff --git a/src/com/android/wallpaper/model/LiveWallpaperInfo.java b/src/com/android/wallpaper/model/LiveWallpaperInfo.java index 47bf89af..bc1f3668 100755 --- a/src/com/android/wallpaper/model/LiveWallpaperInfo.java +++ b/src/com/android/wallpaper/model/LiveWallpaperInfo.java @@ -446,4 +446,14 @@ public class LiveWallpaperInfo extends WallpaperInfo { public String getWallpaperId() { return mInfo.getServiceName(); } + + /** + * Returns true if this wallpaper is currently applied. + */ + public boolean isApplied(android.app.WallpaperInfo currentWallpaper) { + return getWallpaperComponent() != null + && currentWallpaper != null + && TextUtils.equals(getWallpaperComponent().getServiceName(), + currentWallpaper.getServiceName()); + } } diff --git a/src/com/android/wallpaper/model/LiveWallpaperMetadata.java b/src/com/android/wallpaper/model/LiveWallpaperMetadata.java new file mode 100644 index 00000000..6b0dd3a8 --- /dev/null +++ b/src/com/android/wallpaper/model/LiveWallpaperMetadata.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wallpaper.model; + +import android.app.WallpaperInfo; + +import androidx.annotation.Nullable; + +import java.util.List; + +/** + * Live wallpaper-specific wrapper for user-facing wallpaper metadata. + */ +public class LiveWallpaperMetadata extends WallpaperMetadata { + public LiveWallpaperMetadata(android.app.WallpaperInfo wallpaperComponent) { + super(null, null, 0, 0, null, null, wallpaperComponent); + } + + @Override + public List<String> getAttributions() { + throw new UnsupportedOperationException("Not implemented for live wallpapers"); + } + + @Override + public String getActionUrl() { + throw new UnsupportedOperationException("Not implemented for live wallpapers"); + } + + @Override + public int getActionLabelRes() { + throw new UnsupportedOperationException("Not implemented for live wallpapers"); + } + + @Override + public int getActionIconRes() { + throw new UnsupportedOperationException("Not implemented for live wallpapers"); + } + + @Override + public String getCollectionId() { + throw new UnsupportedOperationException("Not implemented for live wallpapers"); + } + + @Nullable + @Override + public String getBackingFileName() { + throw new UnsupportedOperationException("Not implemented for live wallpapers"); + } + + @Override + public WallpaperInfo getWallpaperComponent() { + return mWallpaperComponent; + } +} diff --git a/src/com/android/wallpaper/model/SetWallpaperViewModel.java b/src/com/android/wallpaper/model/SetWallpaperViewModel.java index efd55d39..fcba06ee 100644 --- a/src/com/android/wallpaper/model/SetWallpaperViewModel.java +++ b/src/com/android/wallpaper/model/SetWallpaperViewModel.java @@ -47,7 +47,7 @@ public class SetWallpaperViewModel extends ViewModel { SetWallpaperViewModel viewModel = provider.get(SetWallpaperViewModel.class); return new SetWallpaperCallback() { @Override - public void onSuccess(WallpaperInfo wallpaperInfo) { + public void onSuccess(WallpaperInfo wallpaperInfo, @Destination int destination) { Log.d(TAG, "SetWallpaperCallback success"); viewModel.mStatus.setValue(SetWallpaperStatus.SUCCESS); } diff --git a/src/com/android/wallpaper/model/WallpaperColorsViewModel.kt b/src/com/android/wallpaper/model/WallpaperColorsViewModel.kt index 3d5b7715..8fc2a919 100644 --- a/src/com/android/wallpaper/model/WallpaperColorsViewModel.kt +++ b/src/com/android/wallpaper/model/WallpaperColorsViewModel.kt @@ -16,19 +16,52 @@ package com.android.wallpaper.model import android.app.WallpaperColors +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow -/** ViewModel class to keep track of WallpaperColors for the current wallpaper */ -class WallpaperColorsViewModel : ViewModel() { +/** + * ViewModel class to keep track of WallpaperColors for the current wallpaper + * + * TODO (b/269451870): Rename to WallpaperColorsRepository + */ +class WallpaperColorsViewModel { - /** WallpaperColors for the currently set home wallpaper */ - val homeWallpaperColors: MutableLiveData<WallpaperColors> by lazy { + /** + * WallpaperColors exposed as live data to allow Java integration + * + * TODO (b/262924584): Remove after ColorSectionController2 & ColorCustomizationManager refactor + */ + private val _homeWallpaperColorsLiveData: MutableLiveData<WallpaperColors> by lazy { MutableLiveData<WallpaperColors>() } + val homeWallpaperColorsLiveData: LiveData<WallpaperColors> = _homeWallpaperColorsLiveData + private val _lockWallpaperColorsLiveData: MutableLiveData<WallpaperColors> by lazy { + MutableLiveData<WallpaperColors>() + } + val lockWallpaperColorsLiveData: LiveData<WallpaperColors> = _lockWallpaperColorsLiveData + private val _homeWallpaperColors = MutableStateFlow<WallpaperColors?>(null) + /** WallpaperColors for the currently set home wallpaper */ + val homeWallpaperColors: StateFlow<WallpaperColors?> = _homeWallpaperColors.asStateFlow() + + private val _lockWallpaperColors = MutableStateFlow<WallpaperColors?>(null) /** WallpaperColors for the currently set lock wallpaper */ - val lockWallpaperColors: MutableLiveData<WallpaperColors> by lazy { - MutableLiveData<WallpaperColors>() + val lockWallpaperColors: StateFlow<WallpaperColors?> = _lockWallpaperColors.asStateFlow() + + fun setHomeWallpaperColors(colors: WallpaperColors?) { + _homeWallpaperColors.value = colors + if (colors != _homeWallpaperColorsLiveData.value) { + _homeWallpaperColorsLiveData.value = colors + } + } + + fun setLockWallpaperColors(colors: WallpaperColors?) { + _lockWallpaperColors.value = colors + if (colors != _lockWallpaperColorsLiveData.value) { + _lockWallpaperColorsLiveData.value = colors + } } } diff --git a/src/com/android/wallpaper/model/WallpaperInfo.java b/src/com/android/wallpaper/model/WallpaperInfo.java index 0fc0d275..761e62f6 100755 --- a/src/com/android/wallpaper/model/WallpaperInfo.java +++ b/src/com/android/wallpaper/model/WallpaperInfo.java @@ -88,6 +88,13 @@ public abstract class WallpaperInfo implements Parcelable { } /** + * Returns the content description for this wallpaper, or null if none exists. + */ + public String getContentDescription(Context context) { + return null; + } + + /** * @return The available attributions for this wallpaper, as a list of strings. These represent * the author / website or any other attribution required to be displayed for this wallpaper * regarding authorship, ownership, etc. diff --git a/src/com/android/wallpaper/model/WallpaperMetadata.java b/src/com/android/wallpaper/model/WallpaperMetadata.java index cd24796f..2e6b2734 100755 --- a/src/com/android/wallpaper/model/WallpaperMetadata.java +++ b/src/com/android/wallpaper/model/WallpaperMetadata.java @@ -32,7 +32,7 @@ public class WallpaperMetadata { private final String mActionUrl; private final String mCollectionId; private final String mBackingFileName; - private final android.app.WallpaperInfo mWallpaperComponent; + protected final android.app.WallpaperInfo mWallpaperComponent; @StringRes private final int mActionLabelRes; @DrawableRes private final int mActionIconRes; @@ -101,6 +101,6 @@ public class WallpaperMetadata { * describes an image wallpaper. */ public WallpaperInfo getWallpaperComponent() { - return mWallpaperComponent; + throw new UnsupportedOperationException("Not implemented for static wallpapers"); } } diff --git a/src/com/android/wallpaper/model/WallpaperSectionController.java b/src/com/android/wallpaper/model/WallpaperSectionController.java index cf07e76c..d61330e2 100644 --- a/src/com/android/wallpaper/model/WallpaperSectionController.java +++ b/src/com/android/wallpaper/model/WallpaperSectionController.java @@ -26,6 +26,8 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.graphics.RenderEffect; +import android.graphics.Shader.TileMode; import android.net.Uri; import android.os.Bundle; import android.provider.Settings; @@ -46,6 +48,7 @@ import androidx.core.widget.ContentLoadingProgressBar; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; import androidx.lifecycle.OnLifecycleEvent; import com.android.wallpaper.R; @@ -63,6 +66,7 @@ import com.android.wallpaper.picker.WorkspaceSurfaceHolderCallback; import com.android.wallpaper.util.DisplayUtils; import com.android.wallpaper.util.PreviewUtils; import com.android.wallpaper.util.ResourceUtils; +import com.android.wallpaper.util.VideoWallpaperUtils; import com.android.wallpaper.util.WallpaperConnection; import com.android.wallpaper.util.WallpaperSurfaceCallback; import com.android.wallpaper.widget.LockScreenPreviewer; @@ -70,7 +74,9 @@ import com.android.wallpaper.widget.LockScreenPreviewer; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; -/** The class to control the wallpaper section view. */ +/** + * The class to control the wallpaper section view. + */ public class WallpaperSectionController implements CustomizationSectionController<WallpaperSectionView>, LifecycleObserver { @@ -85,12 +91,15 @@ public class WallpaperSectionController implements private WorkspaceSurfaceHolderCallback mWorkspaceSurfaceCallback; private SurfaceView mHomeWallpaperSurface; private WallpaperSurfaceCallback mHomeWallpaperSurfaceCallback; + private ImageView mHomeFadeInScrim; private SurfaceView mLockWallpaperSurface; private WallpaperSurfaceCallback mLockWallpaperSurfaceCallback; private CardView mLockscreenPreviewCard; private ViewGroup mLockPreviewContainer; + private ImageView mLockFadeInScrim; private ContentLoadingProgressBar mLockscreenPreviewProgress; - private WallpaperConnection mWallpaperConnection; + private WallpaperConnection mHomeWallpaperConnection; + private WallpaperConnection mLockWallpaperConnection; // The wallpaper information which is currently shown on the home preview. private WallpaperInfo mHomePreviewWallpaperInfo; @@ -104,7 +113,8 @@ public class WallpaperSectionController implements private final LifecycleOwner mLifecycleOwner; private final PermissionRequester mPermissionRequester; private final WallpaperColorsViewModel mWallpaperColorsViewModel; - private final WorkspaceViewModel mWorkspaceViewModel; + @Nullable + private final LiveData<Boolean> mOnThemingChanged; private final CustomizationSectionNavigationController mSectionNavigationController; private final WallpaperPreviewNavigator mWallpaperPreviewNavigator; private final Bundle mSavedInstanceState; @@ -112,7 +122,7 @@ public class WallpaperSectionController implements public WallpaperSectionController(Activity activity, LifecycleOwner lifecycleOwner, PermissionRequester permissionRequester, WallpaperColorsViewModel colorsViewModel, - WorkspaceViewModel workspaceViewModel, + @Nullable LiveData<Boolean> onThemingChanged, CustomizationSectionNavigationController sectionNavigationController, WallpaperPreviewNavigator wallpaperPreviewNavigator, Bundle savedInstanceState, @@ -122,7 +132,7 @@ public class WallpaperSectionController implements mPermissionRequester = permissionRequester; mAppContext = mActivity.getApplicationContext(); mWallpaperColorsViewModel = colorsViewModel; - mWorkspaceViewModel = workspaceViewModel; + mOnThemingChanged = onThemingChanged; mSectionNavigationController = sectionNavigationController; mWallpaperPreviewNavigator = wallpaperPreviewNavigator; mSavedInstanceState = savedInstanceState; @@ -132,27 +142,26 @@ public class WallpaperSectionController implements @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @MainThread public void onResume() { - refreshCurrentWallpapers(/* forceRefresh= */ mSavedInstanceState == null); - if (mWallpaperConnection != null) { - mWallpaperConnection.setVisibility(true); - } + refreshCurrentWallpapers(/* forceRefresh= */ true); + updateLivePreviewVisibility(true); } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @MainThread public void onPause() { - if (mWallpaperConnection != null) { - mWallpaperConnection.setVisibility(false); - } + updateLivePreviewVisibility(false); } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) @MainThread public void onStop() { - if (mWallpaperConnection != null) { - mWallpaperConnection.disconnect(); - mWallpaperConnection = null; - } + disconnectHomeLiveWallpaper(); + disconnectLockLiveWallpaper(); + } + + @Override + public boolean shouldRetainInstanceWhenSwitchingTabs() { + return true; } @Override @@ -174,6 +183,7 @@ public class WallpaperSectionController implements new PreviewUtils( mAppContext, mAppContext.getString(R.string.grid_control_metadata_name))); mHomeWallpaperSurface = mHomePreviewCard.findViewById(R.id.wallpaper_surface); + mHomeFadeInScrim = mHomePreviewCard.findViewById(R.id.wallpaper_fadein_scrim); Future<ColorInfo> colorFuture = CompletableFuture.completedFuture( new ColorInfo(/* wallpaperColors= */ null, @@ -183,7 +193,7 @@ public class WallpaperSectionController implements mHomeWallpaperSurface, colorFuture, () -> { if (mHomePreviewWallpaperInfo != null) { maybeLoadThumbnail(mHomePreviewWallpaperInfo, mHomeWallpaperSurfaceCallback, - mDisplayUtils.isOnWallpaperDisplay(mActivity)); + mDisplayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(mActivity), true); } }); @@ -194,11 +204,12 @@ public class WallpaperSectionController implements R.id.wallpaper_preview_spinner); mLockscreenPreviewCard.findViewById(R.id.workspace_surface).setVisibility(View.GONE); mLockWallpaperSurface = mLockscreenPreviewCard.findViewById(R.id.wallpaper_surface); + mLockFadeInScrim = mLockscreenPreviewCard.findViewById(R.id.wallpaper_fadein_scrim); mLockWallpaperSurfaceCallback = new WallpaperSurfaceCallback(mActivity, mLockscreenPreviewCard, mLockWallpaperSurface, colorFuture, () -> { if (mLockPreviewWallpaperInfo != null) { maybeLoadThumbnail(mLockPreviewWallpaperInfo, mLockWallpaperSurfaceCallback, - mDisplayUtils.isOnWallpaperDisplay(mActivity)); + mDisplayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(mActivity), false); } }); mLockPreviewContainer = mLockscreenPreviewCard.findViewById( @@ -218,14 +229,39 @@ public class WallpaperSectionController implements wallpaperSectionView.findViewById(R.id.wallpaper_picker_entry).setOnClickListener( v -> mSectionNavigationController.navigateTo(new CategorySelectorFragment())); - mWorkspaceViewModel.getUpdateWorkspace().observe(mLifecycleOwner, update -> - updateWorkspacePreview(mWorkspaceSurface, mWorkspaceSurfaceCallback, - mWallpaperColorsViewModel.getHomeWallpaperColors().getValue()) - ); + if (mOnThemingChanged != null) { + mOnThemingChanged.observe(mLifecycleOwner, update -> + updateWorkspacePreview(mWorkspaceSurface, mWorkspaceSurfaceCallback, + mWallpaperColorsViewModel.getHomeWallpaperColors().getValue()) + ); + } return wallpaperSectionView; } + private void updateLivePreviewVisibility(boolean visible) { + if (mHomeWallpaperConnection != null) { + mHomeWallpaperConnection.setVisibility(visible); + } + if (mLockWallpaperConnection != null) { + mLockWallpaperConnection.setVisibility(visible); + } + } + + private void disconnectHomeLiveWallpaper() { + if (mHomeWallpaperConnection != null) { + mHomeWallpaperConnection.disconnect(); + mHomeWallpaperConnection = null; + } + } + + private void disconnectLockLiveWallpaper() { + if (mLockWallpaperConnection != null) { + mLockWallpaperConnection.disconnect(); + mLockWallpaperConnection = null; + } + } + private void updateWorkspacePreview(SurfaceView workspaceSurface, WorkspaceSurfaceHolderCallback callback, @Nullable WallpaperColors colors) { // Reattach SurfaceView to trigger #surfaceCreated to update preview for different option. @@ -234,7 +270,9 @@ public class WallpaperSectionController implements parent.removeView(workspaceSurface); if (callback != null) { callback.resetLastSurface(); + callback.setHideBottomRow(false); callback.setWallpaperColors(colors); + callback.maybeRenderPreview(); } parent.addView(workspaceSurface, viewIndex); } @@ -374,6 +412,18 @@ public class WallpaperSectionController implements } onLockWallpaperColorsChanged(lockColors); + + // If we need to do the scrim fade, show the scrim first. + if (VideoWallpaperUtils.needsFadeIn(mHomePreviewWallpaperInfo)) { + mHomeFadeInScrim.animate().cancel(); + mHomeFadeInScrim.setAlpha(1f); + mHomeFadeInScrim.setVisibility(View.VISIBLE); + } + if (VideoWallpaperUtils.needsFadeIn(mLockPreviewWallpaperInfo)) { + mLockFadeInScrim.animate().cancel(); + mLockFadeInScrim.setAlpha(1f); + mLockFadeInScrim.setVisibility(View.VISIBLE); + } }, forceRefresh); } @@ -394,15 +444,24 @@ public class WallpaperSectionController implements // Load thumb regardless of live wallpaper to make sure we have a placeholder while // the live wallpaper initializes in that case. maybeLoadThumbnail(wallpaperInfo, surfaceCallback, - mDisplayUtils.isOnWallpaperDisplay(mActivity)); - - if (isHomeWallpaper) { - if (mWallpaperConnection != null) { - mWallpaperConnection.disconnect(); - mWallpaperConnection = null; + mDisplayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(mActivity), isHomeWallpaper); + + WallpaperManager wallpaperManager = WallpaperManager.getInstance(mActivity); + if (wallpaperManager.isLockscreenLiveWallpaperEnabled()) { + if (isHomeWallpaper) { + disconnectHomeLiveWallpaper(); + } else { + disconnectLockLiveWallpaper(); } if (wallpaperInfo instanceof LiveWallpaperInfo) { - setUpLiveWallpaperPreview(wallpaperInfo); + setUpLiveWallpaperPreview(wallpaperInfo, isHomeWallpaper); + } + } else { + if (isHomeWallpaper) { + disconnectHomeLiveWallpaper(); + if (wallpaperInfo instanceof LiveWallpaperInfo) { + setUpLiveWallpaperPreviewLegacy(wallpaperInfo); + } } } @@ -415,13 +474,19 @@ public class WallpaperSectionController implements @NonNull private Asset maybeLoadThumbnail(WallpaperInfo wallpaperInfo, - WallpaperSurfaceCallback surfaceCallback, boolean offsetToStart) { - ImageView imageView = surfaceCallback.getHomeImageWallpaper(); + WallpaperSurfaceCallback surfaceCallback, boolean offsetToStart, boolean isHome) { + ImageView liveThumbnailView = isHome ? mHomeFadeInScrim : mLockFadeInScrim; + ImageView imageView = VideoWallpaperUtils.needsFadeIn(wallpaperInfo) ? liveThumbnailView + : surfaceCallback.getHomeImageWallpaper(); Asset thumbAsset = wallpaperInfo.getThumbAsset(mAppContext); // Respect offsetToStart only for CurrentWallpaperAssetVN otherwise true. offsetToStart = !(thumbAsset instanceof CurrentWallpaperAssetVN) || offsetToStart; thumbAsset = new BitmapCachingAsset(mAppContext, thumbAsset); if (imageView != null && imageView.getDrawable() == null) { + if (VideoWallpaperUtils.needsFadeIn(wallpaperInfo)) { + imageView.setRenderEffect( + RenderEffect.createBlurEffect(50f, 50f, TileMode.CLAMP)); + } thumbAsset.loadPreviewImage(mActivity, imageView, ResourceUtils.getColorAttr(mActivity, android.R.attr.colorSecondary), offsetToStart); @@ -434,7 +499,7 @@ public class WallpaperSectionController implements mWallpaperColorsViewModel.getHomeWallpaperColors().getValue())) { return; } - mWallpaperColorsViewModel.getHomeWallpaperColors().setValue(wallpaperColors); + mWallpaperColorsViewModel.setHomeWallpaperColors(wallpaperColors); } private void onLockWallpaperColorsChanged(WallpaperColors wallpaperColors) { @@ -442,20 +507,69 @@ public class WallpaperSectionController implements mWallpaperColorsViewModel.getLockWallpaperColors().getValue())) { return; } - mWallpaperColorsViewModel.getLockWallpaperColors().setValue(wallpaperColors); + mWallpaperColorsViewModel.setLockWallpaperColors(wallpaperColors); if (mLockScreenPreviewer != null) { mLockScreenPreviewer.setColor(wallpaperColors); } } - private void setUpLiveWallpaperPreview(WallpaperInfo homeWallpaper) { + private void setUpLiveWallpaperPreview(WallpaperInfo wallpaper, boolean isHomeWallpaper) { + if (!isActivityAlive() || !WallpaperConnection.isPreviewAvailable()) { + return; + } + + final boolean isHomeBoth = (mHomePreviewWallpaperInfo == mLockPreviewWallpaperInfo); + if (isHomeBoth && !isHomeWallpaper) { + // If home and lock are the same the preview is handled by mirroring the home preview, + // so the lock preview is a no-op. + return; + } + + final SurfaceView mainSurface = + isHomeWallpaper ? mHomeWallpaperSurface : mLockWallpaperSurface; + final SurfaceView mirrorSurface = isHomeBoth ? mLockWallpaperSurface : null; + final WallpaperConnection connection = new WallpaperConnection( + getWallpaperIntent(wallpaper.getWallpaperComponent()), mActivity, + new WallpaperConnection.WallpaperConnectionListener() { + @Override + public void onWallpaperColorsChanged(WallpaperColors colors, int displayId) { + if (isHomeWallpaper) { + onHomeWallpaperColorsChanged(colors); + if (isHomeBoth && mLockScreenPreviewer != null) { + mLockScreenPreviewer.setColor(colors); + onLockWallpaperColorsChanged(colors); + } + } else { + onLockWallpaperColorsChanged(colors); + } + } + }, + mainSurface, mirrorSurface); + + connection.setVisibility(true); + if (isHomeWallpaper) { + mHomeWallpaperConnection = connection; + } else { + mLockWallpaperConnection = connection; + } + mainSurface.post(() -> { + if (mHomeWallpaperConnection != null && !mHomeWallpaperConnection.connect()) { + mHomeWallpaperConnection = null; + } + if (mLockWallpaperConnection != null && !mLockWallpaperConnection.connect()) { + mLockWallpaperConnection = null; + } + }); + } + + private void setUpLiveWallpaperPreviewLegacy(WallpaperInfo homeWallpaper) { if (!isActivityAlive()) { return; } if (WallpaperConnection.isPreviewAvailable()) { final boolean isLockLive = mLockPreviewWallpaperInfo instanceof LiveWallpaperInfo; - mWallpaperConnection = new WallpaperConnection( + mHomeWallpaperConnection = new WallpaperConnection( getWallpaperIntent(homeWallpaper.getWallpaperComponent()), mActivity, new WallpaperConnection.WallpaperConnectionListener() { @Override @@ -467,13 +581,29 @@ public class WallpaperSectionController implements } onHomeWallpaperColorsChanged(colors); } + + @Override + public void onEngineShown() { + if (VideoWallpaperUtils.needsFadeIn(homeWallpaper)) { + mHomeFadeInScrim.animate().alpha(0.0f) + .setDuration(VideoWallpaperUtils.TRANSITION_MILLIS) + .withEndAction(() -> mHomeFadeInScrim.setVisibility( + View.INVISIBLE)); + if (isLockLive) { + mLockFadeInScrim.animate().alpha(0.0f) + .setDuration(VideoWallpaperUtils.TRANSITION_MILLIS) + .withEndAction(() -> mLockFadeInScrim.setVisibility( + View.INVISIBLE)); + } + } + } }, mHomeWallpaperSurface, isLockLive ? mLockWallpaperSurface : null); - mWallpaperConnection.setVisibility(true); + mHomeWallpaperConnection.setVisibility(true); mHomeWallpaperSurface.post(() -> { - if (mWallpaperConnection != null && !mWallpaperConnection.connect()) { - mWallpaperConnection = null; + if (mHomeWallpaperConnection != null && !mHomeWallpaperConnection.connect()) { + mHomeWallpaperConnection = null; } }); } @@ -500,6 +630,7 @@ public class WallpaperSectionController implements return !mActivity.isDestroyed() && !mActivity.isFinishing(); } + // TODO(b/276439056) Remove these animations as they have no effect private void fadeWallpaperPreview(boolean isFadeIn, int duration) { setupFade(mHomePreviewCard, mHomePreviewProgress, duration, isFadeIn); setupFade(mLockscreenPreviewCard, mLockscreenPreviewProgress, duration, isFadeIn); diff --git a/src/com/android/wallpaper/module/CustomizationSections.java b/src/com/android/wallpaper/module/CustomizationSections.java index 66ae64d1..5fa62f76 100644 --- a/src/com/android/wallpaper/module/CustomizationSections.java +++ b/src/com/android/wallpaper/module/CustomizationSections.java @@ -11,7 +11,8 @@ import com.android.wallpaper.model.CustomizationSectionController.CustomizationS import com.android.wallpaper.model.PermissionRequester; import com.android.wallpaper.model.WallpaperColorsViewModel; import com.android.wallpaper.model.WallpaperPreviewNavigator; -import com.android.wallpaper.model.WorkspaceViewModel; +import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor; +import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperQuickSwitchViewModel; import com.android.wallpaper.util.DisplayUtils; import java.util.List; @@ -26,24 +27,27 @@ public interface CustomizationSections { } /** + * Currently protected under BaseFlags.isUseRevampedUi() flag. + * * Gets a new instance of the section controller list for the given {@link Screen}. * * Note that the section views will be displayed by the list ordering. * * <p>Don't keep the section controllers as singleton since they contain views. */ - List<CustomizationSectionController<?>> getSectionControllersForScreen( + List<CustomizationSectionController<?>> getRevampedUISectionControllersForScreen( Screen screen, FragmentActivity activity, LifecycleOwner lifecycleOwner, WallpaperColorsViewModel wallpaperColorsViewModel, - WorkspaceViewModel workspaceViewModel, PermissionRequester permissionRequester, WallpaperPreviewNavigator wallpaperPreviewNavigator, CustomizationSectionNavigationController sectionNavigationController, @Nullable Bundle savedInstanceState, CurrentWallpaperInfoFactory wallpaperInfoFactory, - DisplayUtils displayUtils); + DisplayUtils displayUtils, + WallpaperQuickSwitchViewModel wallpaperQuickSwitchViewModel, + WallpaperInteractor wallpaperInteractor); /** * Gets a new instance of the section controller list. @@ -56,7 +60,6 @@ public interface CustomizationSections { FragmentActivity activity, LifecycleOwner lifecycleOwner, WallpaperColorsViewModel wallpaperColorsViewModel, - WorkspaceViewModel workspaceViewModel, PermissionRequester permissionRequester, WallpaperPreviewNavigator wallpaperPreviewNavigator, CustomizationSectionNavigationController sectionNavigationController, diff --git a/src/com/android/wallpaper/module/DefaultCurrentWallpaperInfoFactory.java b/src/com/android/wallpaper/module/DefaultCurrentWallpaperInfoFactory.java index 9e2787fd..c9f9b267 100755 --- a/src/com/android/wallpaper/module/DefaultCurrentWallpaperInfoFactory.java +++ b/src/com/android/wallpaper/module/DefaultCurrentWallpaperInfoFactory.java @@ -15,12 +15,14 @@ */ package com.android.wallpaper.module; +import android.app.WallpaperManager; import android.content.Context; import androidx.annotation.Nullable; import com.android.wallpaper.compat.WallpaperManagerCompat; import com.android.wallpaper.model.CurrentWallpaperInfoVN; +import com.android.wallpaper.model.LiveWallpaperMetadata; import com.android.wallpaper.model.WallpaperInfo; import com.android.wallpaper.module.WallpaperPreferences.PresentationMode; @@ -33,6 +35,7 @@ public class DefaultCurrentWallpaperInfoFactory implements CurrentWallpaperInfoF private final Context mAppContext; private final WallpaperRefresher mWallpaperRefresher; private final LiveWallpaperInfoFactory mLiveWallpaperInfoFactory; + private final WallpaperManager mWallpaperManager; // Cached copies of the currently-set WallpaperInfo(s) and presentation mode. private WallpaperInfo mHomeWallpaper; @@ -46,6 +49,7 @@ public class DefaultCurrentWallpaperInfoFactory implements CurrentWallpaperInfoF Injector injector = InjectorProvider.getInjector(); mWallpaperRefresher = injector.getWallpaperRefresher(mAppContext); mLiveWallpaperInfoFactory = injector.getLiveWallpaperInfoFactory(mAppContext); + mWallpaperManager = WallpaperManager.getInstance(context); } @Override @@ -68,7 +72,10 @@ public class DefaultCurrentWallpaperInfoFactory implements CurrentWallpaperInfoF mWallpaperRefresher.refresh( (homeWallpaperMetadata, lockWallpaperMetadata, presentationMode) -> { WallpaperInfo homeWallpaper; - if (homeWallpaperMetadata.getWallpaperComponent() == null) { + if (homeWallpaperMetadata instanceof LiveWallpaperMetadata) { + homeWallpaper = mLiveWallpaperInfoFactory.getLiveWallpaperInfo( + homeWallpaperMetadata.getWallpaperComponent()); + } else { homeWallpaper = new CurrentWallpaperInfoVN( homeWallpaperMetadata.getAttributions(), homeWallpaperMetadata.getActionUrl(), @@ -76,21 +83,24 @@ public class DefaultCurrentWallpaperInfoFactory implements CurrentWallpaperInfoF homeWallpaperMetadata.getActionIconRes(), homeWallpaperMetadata.getCollectionId(), WallpaperManagerCompat.FLAG_SYSTEM); - } else { - homeWallpaper = mLiveWallpaperInfoFactory.getLiveWallpaperInfo( - homeWallpaperMetadata.getWallpaperComponent()); } WallpaperInfo lockWallpaper = null; if (lockWallpaperMetadata != null) { - lockWallpaper = new CurrentWallpaperInfoVN( - lockWallpaperMetadata.getAttributions(), - lockWallpaperMetadata.getActionUrl(), - lockWallpaperMetadata.getActionLabelRes(), - lockWallpaperMetadata.getActionIconRes(), - lockWallpaperMetadata.getCollectionId(), - WallpaperManagerCompat.FLAG_LOCK); + + if (lockWallpaperMetadata instanceof LiveWallpaperMetadata) { + lockWallpaper = mLiveWallpaperInfoFactory.getLiveWallpaperInfo( + lockWallpaperMetadata.getWallpaperComponent()); + } else { + lockWallpaper = new CurrentWallpaperInfoVN( + lockWallpaperMetadata.getAttributions(), + lockWallpaperMetadata.getActionUrl(), + lockWallpaperMetadata.getActionLabelRes(), + lockWallpaperMetadata.getActionIconRes(), + lockWallpaperMetadata.getCollectionId(), + WallpaperManagerCompat.FLAG_LOCK); + } } mHomeWallpaper = homeWallpaper; diff --git a/src/com/android/wallpaper/module/DefaultWallpaperPersister.java b/src/com/android/wallpaper/module/DefaultWallpaperPersister.java index 89cc09b3..fdfc9be7 100755 --- a/src/com/android/wallpaper/module/DefaultWallpaperPersister.java +++ b/src/com/android/wallpaper/module/DefaultWallpaperPersister.java @@ -15,6 +15,9 @@ */ package com.android.wallpaper.module; +import static android.app.WallpaperManager.FLAG_LOCK; +import static android.app.WallpaperManager.FLAG_SYSTEM; + import android.annotation.SuppressLint; import android.app.Activity; import android.app.WallpaperColors; @@ -448,7 +451,8 @@ public class DefaultWallpaperPersister implements WallpaperPersister { int offsetY = Math.max(0, -(screenSize.y / 2 - scaledCenter.y)); Rect cropRect = WallpaperCropUtils.calculateCropRect(mAppContext, minWallpaperZoom, - wallpaperSize, defaultCropSurfaceSize, screenSize, offsetX, offsetY); + wallpaperSize, defaultCropSurfaceSize, screenSize, offsetX, + offsetY, /* cropExtraWidth= */ true); Rect scaledCropRect = new Rect( (int) Math.floor((float) cropRect.left / minWallpaperZoom), @@ -467,9 +471,9 @@ public class DefaultWallpaperPersister implements WallpaperPersister { int wallpaperId = setBitmapToWallpaperManagerCompat(wallpaperBitmap, /* allowBackup */ false, whichWallpaper); if (wallpaperId > 0) { - mWallpaperPreferences.storeLatestHomeWallpaper(String.valueOf(wallpaperId), - attributions, actionUrl, collectionId, wallpaperBitmap, - WallpaperColors.fromBitmap(wallpaperBitmap)); + mWallpaperPreferences.storeLatestWallpaper(whichWallpaper, + String.valueOf(wallpaperId), attributions, actionUrl, collectionId, + wallpaperBitmap, WallpaperColors.fromBitmap(wallpaperBitmap)); } return wallpaperId; } @@ -532,7 +536,7 @@ public class DefaultWallpaperPersister implements WallpaperPersister { } @Override - public void onLiveWallpaperSet() { + public void onLiveWallpaperSet(@Destination int destination) { android.app.WallpaperInfo currentWallpaperComponent = mWallpaperManager.getWallpaperInfo(); android.app.WallpaperInfo previewedWallpaperComponent = mWallpaperInfoInPreview != null ? mWallpaperInfoInPreview.getWallpaperComponent() : null; @@ -541,13 +545,14 @@ public class DefaultWallpaperPersister implements WallpaperPersister { // WallpaperInfo which was last previewed, then do nothing and nullify last previewed // wallpaper. if (currentWallpaperComponent == null || previewedWallpaperComponent == null - || !currentWallpaperComponent.getPackageName() - .equals(previewedWallpaperComponent.getPackageName())) { + || !currentWallpaperComponent.getServiceName() + .equals(previewedWallpaperComponent.getServiceName())) { mWallpaperInfoInPreview = null; return; } - setLiveWallpaperMetadata(); + setLiveWallpaperMetadata(mWallpaperInfoInPreview, mWallpaperInfoInPreview.getEffectNames(), + destination); } /** @@ -572,30 +577,29 @@ public class DefaultWallpaperPersister implements WallpaperPersister { return isLockWallpaperSet; } - /** - * Sets the live wallpaper's metadata on SharedPreferences. - */ - private void setLiveWallpaperMetadata() { - android.app.WallpaperInfo previewedWallpaperComponent = - mWallpaperInfoInPreview.getWallpaperComponent(); + @Override + public void setLiveWallpaperMetadata(WallpaperInfo wallpaperInfo, String effects, + @Destination int destination) { + android.app.WallpaperInfo component = wallpaperInfo.getWallpaperComponent(); + + if (destination == WallpaperPersister.DEST_HOME_SCREEN + || destination == WallpaperPersister.DEST_BOTH) { + mWallpaperPreferences.clearHomeWallpaperMetadata(); + mWallpaperPreferences.setHomeWallpaperServiceName(component.getServiceName()); + mWallpaperPreferences.setHomeWallpaperEffects(effects); + + // Since rotation affects home screen only, disable it when setting home live wp + mWallpaperPreferences.setWallpaperPresentationMode( + WallpaperPreferences.PRESENTATION_MODE_STATIC); + mWallpaperPreferences.clearDailyRotations(); + } - mWallpaperPreferences.clearHomeWallpaperMetadata(); - // NOTE: We explicitly do not also clear the lock wallpaper metadata. Since the user may - // have set the live wallpaper on the home screen only, we leave the lock wallpaper metadata - // intact. If the user has set the live wallpaper for both home and lock screens, then the - // WallpaperRefresher will pick up on that and update the preferences later. - mWallpaperPreferences - .setHomeWallpaperAttributions(mWallpaperInfoInPreview.getAttributions(mAppContext)); - mWallpaperPreferences.setHomeWallpaperPackageName( - previewedWallpaperComponent.getPackageName()); - mWallpaperPreferences.setHomeWallpaperServiceName( - previewedWallpaperComponent.getServiceName()); - mWallpaperPreferences.setHomeWallpaperCollectionId( - mWallpaperInfoInPreview.getCollectionId(mAppContext)); - mWallpaperPreferences.setWallpaperPresentationMode( - WallpaperPreferences.PRESENTATION_MODE_STATIC); - mWallpaperPreferences.clearDailyRotations(); - mWallpaperPreferences.setWallpaperEffects(mWallpaperInfoInPreview.getEffectNames()); + if (destination == WallpaperPersister.DEST_LOCK_SCREEN + || destination == WallpaperPersister.DEST_BOTH) { + mWallpaperPreferences.clearLockWallpaperMetadata(); + mWallpaperPreferences.setLockWallpaperServiceName(component.getServiceName()); + mWallpaperPreferences.setLockWallpaperEffects(effects); + } } private class SetWallpaperTask extends AsyncTask<Void, Void, Boolean> { @@ -725,7 +729,7 @@ public class DefaultWallpaperPersister implements WallpaperPersister { } if (isSuccess) { - mCallback.onSuccess(mWallpaper); + mCallback.onSuccess(mWallpaper, mDestination); mWallpaperChangedNotifier.notifyWallpaperChanged(); } else { mCallback.onError(null /* throwable */); @@ -774,7 +778,7 @@ public class DefaultWallpaperPersister implements WallpaperPersister { private void setImageWallpaperMetadata(@Destination int destination, int wallpaperId) { if (destination == DEST_HOME_SCREEN || destination == DEST_BOTH) { mWallpaperPreferences.clearHomeWallpaperMetadata(); - mWallpaperPreferences.setWallpaperEffects(null); + mWallpaperPreferences.setHomeWallpaperEffects(null); setImageWallpaperHomeMetadata(wallpaperId); // Reset presentation mode to STATIC if an individual wallpaper is set to the @@ -820,7 +824,7 @@ public class DefaultWallpaperPersister implements WallpaperPersister { mWallpaperPreferences.setHomeWallpaperCollectionId( mWallpaper.getCollectionId(mAppContext)); mWallpaperPreferences.setHomeWallpaperRemoteId(mWallpaper.getWallpaperId()); - mWallpaperPreferences.storeLatestHomeWallpaper( + mWallpaperPreferences.storeLatestWallpaper(FLAG_SYSTEM, TextUtils.isEmpty(mWallpaper.getWallpaperId()) ? String.valueOf(bitmapHash) : mWallpaper.getWallpaperId(), mWallpaper, mBitmap, colors); @@ -843,42 +847,46 @@ public class DefaultWallpaperPersister implements WallpaperPersister { // because WallpaperManager-generated IDs are specific to a physical device and // cannot be used to identify a wallpaper image on another device after restore is // complete. - saveLockWallpaperHashCode(); + Bitmap lockBitmap = getLockWallpaperBitmap(); + if (lockBitmap != null) { + saveLockWallpaperHashCode(lockBitmap); + mWallpaperPreferences.storeLatestWallpaper(FLAG_LOCK, + TextUtils.isEmpty(mWallpaper.getWallpaperId()) + ? String.valueOf(mWallpaperPreferences.getLockWallpaperHashCode()) + : mWallpaper.getWallpaperId(), + mWallpaper, lockBitmap, WallpaperColors.fromBitmap(lockBitmap)); + } } - private void saveLockWallpaperHashCode() { - Bitmap lockBitmap = null; - + private Bitmap getLockWallpaperBitmap() { ParcelFileDescriptor parcelFd = mWallpaperManagerCompat.getWallpaperFile( WallpaperManagerCompat.FLAG_LOCK); if (parcelFd == null) { - return; + return null; } - InputStream fileStream = null; - try { - fileStream = new FileInputStream(parcelFd.getFileDescriptor()); - lockBitmap = BitmapFactory.decodeStream(fileStream); - parcelFd.close(); + try (InputStream fileStream = new FileInputStream(parcelFd.getFileDescriptor())) { + return BitmapFactory.decodeStream(fileStream); } catch (IOException e) { - Log.e(TAG, "IO exception when closing the file descriptor."); + Log.e(TAG, "IO exception when closing the file stream.", e); + return null; } finally { - if (fileStream != null) { - try { - fileStream.close(); - } catch (IOException e) { - Log.e(TAG, - "IO exception when closing the input stream for the lock screen " - + "WP."); - } + try { + parcelFd.close(); + } catch (IOException e) { + Log.e(TAG, "IO exception when closing the file descriptor.", e); } } + } + private long saveLockWallpaperHashCode(Bitmap lockBitmap) { if (lockBitmap != null) { long bitmapHash = BitmapUtils.generateHashCode(lockBitmap); mWallpaperPreferences.setLockWallpaperHashCode(bitmapHash); + return bitmapHash; } + return 0; } } } diff --git a/src/com/android/wallpaper/module/DefaultWallpaperPreferences.java b/src/com/android/wallpaper/module/DefaultWallpaperPreferences.java index c2cd07b4..6f945a90 100755 --- a/src/com/android/wallpaper/module/DefaultWallpaperPreferences.java +++ b/src/com/android/wallpaper/module/DefaultWallpaperPreferences.java @@ -47,6 +47,7 @@ import java.util.TimeZone; * Default implementation that writes to and reads from SharedPreferences. */ public class DefaultWallpaperPreferences implements WallpaperPreferences { + public static final String PREFS_NAME = "wallpaper"; public static final String NO_BACKUP_PREFS_NAME = "wallpaper-nobackup"; @@ -154,10 +155,6 @@ public class DefaultWallpaperPreferences implements WallpaperPreferences { editor.putInt(NoBackupKeys.KEY_NUM_DAYS_DAILY_ROTATION_NOT_ATTEMPTED, mSharedPrefs.getInt(NoBackupKeys.KEY_NUM_DAYS_DAILY_ROTATION_NOT_ATTEMPTED, 0)); } - if (mSharedPrefs.contains(NoBackupKeys.KEY_HOME_WALLPAPER_PACKAGE_NAME)) { - editor.putString(NoBackupKeys.KEY_HOME_WALLPAPER_PACKAGE_NAME, - mSharedPrefs.getString(NoBackupKeys.KEY_HOME_WALLPAPER_PACKAGE_NAME, null)); - } if (mSharedPrefs.contains(NoBackupKeys.KEY_HOME_WALLPAPER_SERVICE_NAME)) { editor.putString(NoBackupKeys.KEY_HOME_WALLPAPER_SERVICE_NAME, mSharedPrefs.getString(NoBackupKeys.KEY_HOME_WALLPAPER_SERVICE_NAME, null)); @@ -325,7 +322,7 @@ public class DefaultWallpaperPreferences implements WallpaperPreferences { .apply(); mNoBackupPrefs.edit() - .remove(NoBackupKeys.KEY_HOME_WALLPAPER_PACKAGE_NAME) + .remove(NoBackupKeys.KEY_HOME_WALLPAPER_SERVICE_NAME) .remove(NoBackupKeys.KEY_HOME_WALLPAPER_MANAGER_ID) .remove(NoBackupKeys.KEY_HOME_WALLPAPER_REMOTE_ID) .remove(NoBackupKeys.KEY_HOME_WALLPAPER_SERVICE_NAME) @@ -335,19 +332,6 @@ public class DefaultWallpaperPreferences implements WallpaperPreferences { } @Override - public String getHomeWallpaperPackageName() { - return mNoBackupPrefs.getString( - NoBackupKeys.KEY_HOME_WALLPAPER_PACKAGE_NAME, null); - } - - @Override - public void setHomeWallpaperPackageName(String packageName) { - mNoBackupPrefs.edit().putString( - NoBackupKeys.KEY_HOME_WALLPAPER_PACKAGE_NAME, packageName) - .apply(); - } - - @Override public String getHomeWallpaperServiceName() { return mNoBackupPrefs.getString( NoBackupKeys.KEY_HOME_WALLPAPER_SERVICE_NAME, null); @@ -542,6 +526,17 @@ public class DefaultWallpaperPreferences implements WallpaperPreferences { } @Override + public String getLockWallpaperServiceName() { + return mNoBackupPrefs.getString(NoBackupKeys.KEY_LOCK_WALLPAPER_SERVICE_NAME, null); + } + + @Override + public void setLockWallpaperServiceName(String serviceName) { + mNoBackupPrefs.edit().putString(NoBackupKeys.KEY_LOCK_WALLPAPER_SERVICE_NAME, serviceName) + .apply(); + } + + @Override public void addDailyRotation(long timestamp) { String jsonString = mNoBackupPrefs.getString( NoBackupKeys.KEY_DAILY_ROTATION_TIMESTAMPS, "[]"); @@ -942,19 +937,32 @@ public class DefaultWallpaperPreferences implements WallpaperPreferences { setLockWallpaperCollectionId(collectionId); setLockWallpaperRemoteId(wallpaperId); } - setWallpaperEffects(null); + setHomeWallpaperEffects(null); + } + + @Override + public String getHomeWallpaperEffects() { + return mNoBackupPrefs.getString( + NoBackupKeys.KEY_HOME_WALLPAPER_EFFECTS, null); + } + + @Override + public void setHomeWallpaperEffects(String effects) { + mNoBackupPrefs.edit().putString( + NoBackupKeys.KEY_HOME_WALLPAPER_EFFECTS, effects) + .apply(); } @Override - public String getWallpaperEffects() { + public String getLockWallpaperEffects() { return mNoBackupPrefs.getString( - NoBackupKeys.KEY_WALLPAPER_EFFECTS, null); + NoBackupKeys.KEY_LOCK_WALLPAPER_EFFECTS, null); } @Override - public void setWallpaperEffects(String effects) { + public void setLockWallpaperEffects(String effects) { mNoBackupPrefs.edit().putString( - NoBackupKeys.KEY_WALLPAPER_EFFECTS, effects) + NoBackupKeys.KEY_LOCK_WALLPAPER_EFFECTS, effects) .apply(); } diff --git a/src/com/android/wallpaper/module/DefaultWallpaperRefresher.java b/src/com/android/wallpaper/module/DefaultWallpaperRefresher.java index 3363d8ea..3c870280 100755 --- a/src/com/android/wallpaper/module/DefaultWallpaperRefresher.java +++ b/src/com/android/wallpaper/module/DefaultWallpaperRefresher.java @@ -15,6 +15,9 @@ */ package com.android.wallpaper.module; +import static com.android.wallpaper.compat.WallpaperManagerCompat.FLAG_LOCK; +import static com.android.wallpaper.compat.WallpaperManagerCompat.FLAG_SYSTEM; + import android.annotation.SuppressLint; import android.app.WallpaperManager; import android.content.Context; @@ -29,6 +32,7 @@ import android.util.Log; import com.android.wallpaper.R; import com.android.wallpaper.asset.BitmapUtils; import com.android.wallpaper.compat.WallpaperManagerCompat; +import com.android.wallpaper.model.LiveWallpaperMetadata; import com.android.wallpaper.model.WallpaperMetadata; import java.io.FileInputStream; @@ -44,6 +48,7 @@ import java.util.List; */ @SuppressLint("ServiceCast") public class DefaultWallpaperRefresher implements WallpaperRefresher { + private static final String TAG = "DefaultWPRefresher"; private final Context mAppContext; @@ -77,12 +82,13 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { */ private class GetWallpaperMetadataAsyncTask extends AsyncTask<Void, Void, List<WallpaperMetadata>> { + private final RefreshListener mListener; private final WallpaperManagerCompat mWallpaperManagerCompat; private long mCurrentHomeWallpaperHashCode; private long mCurrentLockWallpaperHashCode; - private String mSystemWallpaperPackageName; + private String mSystemWallpaperServiceName; @SuppressLint("ServiceCast") public GetWallpaperMetadataAsyncTask(RefreshListener listener) { @@ -95,18 +101,17 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { protected List<WallpaperMetadata> doInBackground(Void... unused) { List<WallpaperMetadata> wallpaperMetadatas = new ArrayList<>(); - if (!isHomeScreenMetadataCurrent() || isHomeScreenAttributionsEmpty()) { + boolean isHomeScreenStatic = mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM) == null; + if (!isHomeScreenMetadataCurrent() || (isHomeScreenStatic + && isHomeScreenAttributionsEmpty())) { mWallpaperPreferences.clearHomeWallpaperMetadata(); setFallbackHomeScreenWallpaperMetadata(); } - boolean isLockScreenWallpaperCurrentlySet = mWallpaperStatusChecker.isLockWallpaperSet( - mAppContext); + boolean isLockScreenWallpaperCurrentlySet = + mWallpaperStatusChecker.isLockWallpaperSet(mAppContext); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N - || !isLockScreenWallpaperCurrentlySet) { - - // Return only home metadata if pre-N device or lock screen wallpaper is not explicitly set. + if (mWallpaperManager.getWallpaperInfo() == null) { wallpaperMetadatas.add(new WallpaperMetadata( mWallpaperPreferences.getHomeWallpaperAttributions(), mWallpaperPreferences.getHomeWallpaperActionUrl(), @@ -114,32 +119,40 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { mWallpaperPreferences.getHomeWallpaperActionIconRes(), mWallpaperPreferences.getHomeWallpaperCollectionId(), mWallpaperPreferences.getHomeWallpaperBackingFileName(), - mWallpaperManager.getWallpaperInfo())); + null)); + } else { + wallpaperMetadatas.add( + new LiveWallpaperMetadata(mWallpaperManager.getWallpaperInfo())); + } + + // Return only home metadata if pre-N device or lock screen wallpaper is not explicitly + // set. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N + || !isLockScreenWallpaperCurrentlySet) { return wallpaperMetadatas; } - if (!isLockScreenMetadataCurrent() || isLockScreenAttributionsEmpty()) { + boolean isLockScreenStatic = mWallpaperManager.getWallpaperInfo(FLAG_LOCK) == null; + if (!isLockScreenMetadataCurrent() || (isLockScreenStatic + && isLockScreenAttributionsEmpty())) { mWallpaperPreferences.clearLockWallpaperMetadata(); setFallbackLockScreenWallpaperMetadata(); } - wallpaperMetadatas.add(new WallpaperMetadata( - mWallpaperPreferences.getHomeWallpaperAttributions(), - mWallpaperPreferences.getHomeWallpaperActionUrl(), - mWallpaperPreferences.getHomeWallpaperActionLabelRes(), - mWallpaperPreferences.getHomeWallpaperActionIconRes(), - mWallpaperPreferences.getHomeWallpaperCollectionId(), - mWallpaperPreferences.getHomeWallpaperBackingFileName(), - mWallpaperManager.getWallpaperInfo())); - - wallpaperMetadatas.add(new WallpaperMetadata( - mWallpaperPreferences.getLockWallpaperAttributions(), - mWallpaperPreferences.getLockWallpaperActionUrl(), - mWallpaperPreferences.getLockWallpaperActionLabelRes(), - mWallpaperPreferences.getLockWallpaperActionIconRes(), - mWallpaperPreferences.getLockWallpaperCollectionId(), - mWallpaperPreferences.getLockWallpaperBackingFileName(), - null /* wallpaperComponent */)); + if (mWallpaperManager.getWallpaperInfo(FLAG_LOCK) == null + || !mWallpaperManager.isLockscreenLiveWallpaperEnabled()) { + wallpaperMetadatas.add(new WallpaperMetadata( + mWallpaperPreferences.getLockWallpaperAttributions(), + mWallpaperPreferences.getLockWallpaperActionUrl(), + mWallpaperPreferences.getLockWallpaperActionLabelRes(), + mWallpaperPreferences.getLockWallpaperActionIconRes(), + mWallpaperPreferences.getLockWallpaperCollectionId(), + mWallpaperPreferences.getLockWallpaperBackingFileName(), + null)); + } else { + wallpaperMetadatas.add(new LiveWallpaperMetadata( + mWallpaperManager.getWallpaperInfo(FLAG_LOCK))); + } return wallpaperMetadatas; } @@ -147,8 +160,9 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { @Override protected void onPostExecute(List<WallpaperMetadata> metadatas) { if (metadatas.size() > 2) { - Log.e(TAG, "Got more than 2 WallpaperMetadata objects - only home and (optionally) lock " - + "are permitted."); + Log.e(TAG, + "Got more than 2 WallpaperMetadata objects - only home and (optionally) " + + "lock are permitted."); return; } @@ -157,27 +171,30 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { } /** - * Sets fallback wallpaper attributions to WallpaperPreferences when the saved metadata did not - * match the system wallpaper. For live wallpapers, loads the label (title) but for image - * wallpapers loads a generic title string. + * Sets fallback wallpaper attributions to WallpaperPreferences when the saved metadata did + * not match the system wallpaper. For live wallpapers, loads the label (title) but for + * image wallpapers loads a generic title string. */ private void setFallbackHomeScreenWallpaperMetadata() { android.app.WallpaperInfo wallpaperComponent = mWallpaperManager.getWallpaperInfo(); if (wallpaperComponent == null) { // Image wallpaper mWallpaperPreferences.setHomeWallpaperAttributions( - Arrays.asList(mAppContext.getResources().getString(R.string.fallback_wallpaper_title))); + Arrays.asList(mAppContext.getResources() + .getString(R.string.fallback_wallpaper_title))); - // Set wallpaper ID if at least N or set a hash code if an earlier version of Android. + // Set wallpaper ID if at least N or set a hash code if an earlier version of + // Android. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mWallpaperPreferences.setHomeWallpaperManagerId(mWallpaperManagerCompat.getWallpaperId( - WallpaperManagerCompat.FLAG_SYSTEM)); + mWallpaperPreferences.setHomeWallpaperManagerId( + mWallpaperManagerCompat.getWallpaperId(FLAG_SYSTEM)); } else { - mWallpaperPreferences.setHomeWallpaperHashCode(getCurrentHomeWallpaperHashCode()); + mWallpaperPreferences.setHomeWallpaperHashCode( + getCurrentHomeWallpaperHashCode()); } } else { // Live wallpaper mWallpaperPreferences.setHomeWallpaperAttributions(Arrays.asList( wallpaperComponent.loadLabel(mAppContext.getPackageManager()).toString())); - mWallpaperPreferences.setHomeWallpaperPackageName(mSystemWallpaperPackageName); + mWallpaperPreferences.setHomeWallpaperServiceName(mSystemWallpaperServiceName); } mWallpaperPreferences.setWallpaperPresentationMode( WallpaperPreferences.PRESENTATION_MODE_STATIC); @@ -185,14 +202,15 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { /** * Sets fallback lock screen wallpaper attributions to WallpaperPreferences. This should be - * called when the saved lock screen wallpaper metadata does not match the currently set lock - * screen wallpaper. + * called when the saved lock screen wallpaper metadata does not match the currently set + * lock screen wallpaper. */ private void setFallbackLockScreenWallpaperMetadata() { mWallpaperPreferences.setLockWallpaperAttributions( - Arrays.asList(mAppContext.getResources().getString(R.string.fallback_wallpaper_title))); + Arrays.asList(mAppContext.getResources() + .getString(R.string.fallback_wallpaper_title))); mWallpaperPreferences.setLockWallpaperId(mWallpaperManagerCompat.getWallpaperId( - WallpaperManagerCompat.FLAG_LOCK)); + FLAG_LOCK)); } /** @@ -209,7 +227,8 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { * Returns whether the home screen attributions saved in WallpaperPreferences is empty. */ private boolean isHomeScreenAttributionsEmpty() { - List<String> homeScreenAttributions = mWallpaperPreferences.getHomeWallpaperAttributions(); + List<String> homeScreenAttributions = + mWallpaperPreferences.getHomeWallpaperAttributions(); return homeScreenAttributions.get(0) == null && homeScreenAttributions.get(1) == null && homeScreenAttributions.get(2) == null; @@ -217,13 +236,15 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { private long getCurrentHomeWallpaperHashCode() { if (mCurrentHomeWallpaperHashCode == 0) { - BitmapDrawable wallpaperDrawable = (BitmapDrawable) mWallpaperManagerCompat.getDrawable(); - Bitmap wallpaperBitmap = wallpaperDrawable.getBitmap(); - mCurrentHomeWallpaperHashCode = BitmapUtils.generateHashCode(wallpaperBitmap); - - // Manually request that WallpaperManager loses its reference to the current wallpaper - // bitmap, which can occupy a large memory allocation for the lifetime of the app. - mWallpaperManager.forgetLoadedWallpaper(); + BitmapDrawable wallpaperDrawable = (BitmapDrawable) + mWallpaperManagerCompat.getDrawable(); + Bitmap wallpaperBitmap = wallpaperDrawable.getBitmap(); + mCurrentHomeWallpaperHashCode = BitmapUtils.generateHashCode(wallpaperBitmap); + + // Manually request that WallpaperManager loses its reference to the current + // wallpaper bitmap, which can occupy a large memory allocation for the lifetime of + // the app. + mWallpaperManager.forgetLoadedWallpaper(); } return mCurrentHomeWallpaperHashCode; } @@ -245,7 +266,7 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { Bitmap lockBitmap = null; ParcelFileDescriptor pfd = mWallpaperManagerCompat.getWallpaperFile( - WallpaperManagerCompat.FLAG_LOCK); + FLAG_LOCK); // getWallpaperFile returns null if the lock screen isn't explicitly set, so need this // check. if (pfd != null) { @@ -262,7 +283,8 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { try { fileStream.close(); } catch (IOException e) { - Log.e(TAG, "IO exception when closing input stream for lock screen WP."); + Log.e(TAG, + "IO exception when closing input stream for lock screen WP."); } } } @@ -278,25 +300,25 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { private boolean isHomeScreenImageWallpaperCurrent() { long savedBitmapHash = mWallpaperPreferences.getHomeWallpaperHashCode(); - // Use WallpaperManager IDs to check same-ness of image wallpaper on N+ versions of Android - // only when there is no saved bitmap hash code (which could be leftover from a previous build - // of the app that did not use wallpaper IDs). + // Use WallpaperManager IDs to check same-ness of image wallpaper on N+ versions of + // Android only when there is no saved bitmap hash code (which could be leftover from a + // previous build of the app that did not use wallpaper IDs). if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && savedBitmapHash == 0) { return mWallpaperPreferences.getHomeWallpaperManagerId() - == mWallpaperManagerCompat.getWallpaperId(WallpaperManagerCompat.FLAG_SYSTEM); + == mWallpaperManagerCompat.getWallpaperId(FLAG_SYSTEM); } return savedBitmapHash == getCurrentHomeWallpaperHashCode(); } /** - * Returns whether the live wallpaper set to the system's home screen matches the metadata in - * WallpaperPreferences. + * Returns whether the live wallpaper set to the system's home screen matches the metadata + * in WallpaperPreferences. */ private boolean isHomeScreenLiveWallpaperCurrent() { - mSystemWallpaperPackageName = mWallpaperManager.getWallpaperInfo().getPackageName(); - String homeWallpaperPackageName = mWallpaperPreferences.getHomeWallpaperPackageName(); - return mSystemWallpaperPackageName.equals(homeWallpaperPackageName); + mSystemWallpaperServiceName = mWallpaperManager.getWallpaperInfo().getServiceName(); + String homeWallpaperServiceName = mWallpaperPreferences.getHomeWallpaperServiceName(); + return mSystemWallpaperServiceName.equals(homeWallpaperServiceName); } /** @@ -304,18 +326,42 @@ public class DefaultWallpaperRefresher implements WallpaperRefresher { * current lock screen wallpaper. */ private boolean isLockScreenMetadataCurrent() { - // Check for lock wallpaper image same-ness only when there is no stored lock wallpaper hash - // code. Otherwise if there is a lock wallpaper hash code stored in + return (mWallpaperManager.getWallpaperInfo(FLAG_LOCK) == null) + ? isLockScreenImageWallpaperCurrent() + : isLockScreenLiveWallpaperCurrent(); + } + + /** + * Returns whether the image wallpaper set for the lock screen matches the metadata in + * WallpaperPreferences. + */ + private boolean isLockScreenImageWallpaperCurrent() { + // Check for lock wallpaper image same-ness only when there is no stored lock wallpaper + // hash code. Otherwise if there is a lock wallpaper hash code stored in // {@link WallpaperPreferences}, then check hash codes. long savedLockWallpaperHash = mWallpaperPreferences.getLockWallpaperHashCode(); - return (savedLockWallpaperHash == 0) - ? mWallpaperPreferences.getLockWallpaperId() - == mWallpaperManagerCompat.getWallpaperId(WallpaperManagerCompat.FLAG_LOCK) - : savedLockWallpaperHash == getCurrentLockWallpaperHashCode(); + if (savedLockWallpaperHash == 0) { + return mWallpaperPreferences.getLockWallpaperId() + == mWallpaperManagerCompat.getWallpaperId(FLAG_LOCK); + } else { + return savedLockWallpaperHash == getCurrentLockWallpaperHashCode(); + } } /** + * Returns whether the live wallpaper for the home screen matches the metadata in + * WallpaperPreferences. + */ + private boolean isLockScreenLiveWallpaperCurrent() { + String currentServiceName = mWallpaperManager.getWallpaperInfo(FLAG_LOCK) + .getServiceName(); + String storedServiceName = mWallpaperPreferences.getLockWallpaperServiceName(); + return currentServiceName.equals(storedServiceName); + } + + + /** * Returns whether the lock screen attributions saved in WallpaperPreferences are empty. */ private boolean isLockScreenAttributionsEmpty() { diff --git a/src/com/android/wallpaper/module/Injector.kt b/src/com/android/wallpaper/module/Injector.kt index bcbf0e59..37b2c20a 100755 --- a/src/com/android/wallpaper/module/Injector.kt +++ b/src/com/android/wallpaper/module/Injector.kt @@ -15,20 +15,22 @@ */ package com.android.wallpaper.module -import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri +import androidx.activity.ComponentActivity import androidx.fragment.app.Fragment import com.android.wallpaper.compat.WallpaperManagerCompat import com.android.wallpaper.config.BaseFlags import com.android.wallpaper.effects.EffectsController -import com.android.wallpaper.effects.EffectsController.EffectsServiceListener import com.android.wallpaper.model.CategoryProvider +import com.android.wallpaper.model.WallpaperColorsViewModel import com.android.wallpaper.model.WallpaperInfo import com.android.wallpaper.monitor.PerformanceMonitor import com.android.wallpaper.network.Requester import com.android.wallpaper.picker.PreviewFragment +import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor +import com.android.wallpaper.picker.customization.domain.interactor.WallpaperSnapshotRestorer import com.android.wallpaper.picker.undo.domain.interactor.SnapshotRestorer import com.android.wallpaper.picker.undo.domain.interactor.UndoInteractor import com.android.wallpaper.util.DisplayUtils @@ -46,7 +48,7 @@ interface Injector { fun getCurrentWallpaperInfoFactory(context: Context): CurrentWallpaperInfoFactory - fun getCustomizationSections(activity: Activity): CustomizationSections + fun getCustomizationSections(activity: ComponentActivity): CustomizationSections fun getDeepLinkRedirectIntent(context: Context, uri: Uri): Intent @@ -56,11 +58,11 @@ interface Injector { fun getDrawableLayerResolver(): DrawableLayerResolver - fun getEffectsController(context: Context, listener: EffectsServiceListener): EffectsController? + fun getEffectsController(context: Context): EffectsController? fun getExploreIntentChecker(context: Context): ExploreIntentChecker - fun getIndividualPickerFragment(collectionId: String): Fragment + fun getIndividualPickerFragment(context: Context, collectionId: String): Fragment fun getLiveWallpaperInfoFactory(context: Context): LiveWallpaperInfoFactory @@ -114,4 +116,10 @@ interface Injector { // Empty because we don't support undoing in WallpaperPicker2. return HashMap() } + + fun getWallpaperInteractor(context: Context): WallpaperInteractor + + fun getWallpaperSnapshotRestorer(context: Context): WallpaperSnapshotRestorer + + fun getWallpaperColorsViewModel(): WallpaperColorsViewModel } diff --git a/src/com/android/wallpaper/module/LargeScreenMultiPanesChecker.kt b/src/com/android/wallpaper/module/LargeScreenMultiPanesChecker.kt index 7e42d2da..5b4e5ec6 100644 --- a/src/com/android/wallpaper/module/LargeScreenMultiPanesChecker.kt +++ b/src/com/android/wallpaper/module/LargeScreenMultiPanesChecker.kt @@ -39,6 +39,7 @@ class LargeScreenMultiPanesChecker : MultiPanesChecker { override fun getMultiPanesIntent(intent: Intent): Intent { return Intent(ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY).apply { + intent.extras?.let { putExtras(it) } putExtra(EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY, VALUE_HIGHLIGHT_MENU) putExtra( EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI, diff --git a/src/com/android/wallpaper/module/WallpaperPersister.java b/src/com/android/wallpaper/module/WallpaperPersister.java index cfb3cab9..9b5cae82 100755 --- a/src/com/android/wallpaper/module/WallpaperPersister.java +++ b/src/com/android/wallpaper/module/WallpaperPersister.java @@ -167,14 +167,26 @@ public interface WallpaperPersister { * Saves attributions to WallpaperPreferences for the last previewed wallpaper if it has an * {@link android.app.WallpaperInfo} component matching the one currently set to the * {@link android.app.WallpaperManager}. + * + * @param destination Live wallpaper destination (home/lock/both) + */ + void onLiveWallpaperSet(@Destination int destination); + + /** + * Updates lie wallpaper metadata by persisting them to SharedPreferences. + * + * @param wallpaperInfo Wallpaper model for the live wallpaper + * @param effects Comma-separate list of effect (see {@link WallpaperInfo#getEffectNames}) + * @param destination Live wallpaper destination (home/lock/both) */ - void onLiveWallpaperSet(); + void setLiveWallpaperMetadata(WallpaperInfo wallpaperInfo, String effects, + @Destination int destination); /** * Interface for tracking success or failure of set wallpaper operations. */ interface SetWallpaperCallback { - void onSuccess(WallpaperInfo wallpaperInfo); + void onSuccess(WallpaperInfo wallpaperInfo, @Destination int destination); void onError(@Nullable Throwable throwable); } @@ -215,4 +227,20 @@ public interface WallpaperPersister { throw new AssertionError("Unknown @Destination"); } } + + /** + * Converts a set of {@link SetWallpaperFlags} to the corresponding {@link Destination}. + */ + @Destination + static int flagsToDestination(@SetWallpaperFlags int flags) { + if (flags == (FLAG_SYSTEM | FLAG_LOCK)) { + return DEST_BOTH; + } else if (flags == FLAG_SYSTEM) { + return DEST_HOME_SCREEN; + } else if (flags == FLAG_LOCK) { + return DEST_LOCK_SCREEN; + } else { + throw new AssertionError("Unknown @SetWallpaperFlags value"); + } + } } diff --git a/src/com/android/wallpaper/module/WallpaperPicker2Injector.kt b/src/com/android/wallpaper/module/WallpaperPicker2Injector.kt index d97e4bea..db560c4d 100755 --- a/src/com/android/wallpaper/module/WallpaperPicker2Injector.kt +++ b/src/com/android/wallpaper/module/WallpaperPicker2Injector.kt @@ -15,18 +15,18 @@ */ package com.android.wallpaper.module -import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import androidx.activity.ComponentActivity import androidx.fragment.app.Fragment import com.android.wallpaper.compat.WallpaperManagerCompat import com.android.wallpaper.config.BaseFlags import com.android.wallpaper.effects.EffectsController -import com.android.wallpaper.effects.EffectsController.EffectsServiceListener import com.android.wallpaper.model.CategoryProvider import com.android.wallpaper.model.LiveWallpaperInfo +import com.android.wallpaper.model.WallpaperColorsViewModel import com.android.wallpaper.model.WallpaperInfo import com.android.wallpaper.monitor.PerformanceMonitor import com.android.wallpaper.network.Requester @@ -35,13 +35,20 @@ import com.android.wallpaper.picker.CustomizationPickerActivity import com.android.wallpaper.picker.ImagePreviewFragment import com.android.wallpaper.picker.LivePreviewFragment import com.android.wallpaper.picker.PreviewFragment +import com.android.wallpaper.picker.customization.data.content.WallpaperClientImpl +import com.android.wallpaper.picker.customization.data.repository.WallpaperRepository +import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor +import com.android.wallpaper.picker.customization.domain.interactor.WallpaperSnapshotRestorer import com.android.wallpaper.picker.individual.IndividualPickerFragment import com.android.wallpaper.picker.undo.data.repository.UndoRepository import com.android.wallpaper.picker.undo.domain.interactor.UndoInteractor +import com.android.wallpaper.settings.data.repository.SecureSettingsRepository +import com.android.wallpaper.settings.data.repository.SecureSettingsRepositoryImpl import com.android.wallpaper.util.DisplayUtils +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -open class WallpaperPicker2Injector : Injector { +open class WallpaperPicker2Injector() : Injector { private var alarmManagerWrapper: AlarmManagerWrapper? = null private var bitmapCropper: BitmapCropper? = null private var categoryProvider: CategoryProvider? = null @@ -67,6 +74,10 @@ open class WallpaperPicker2Injector : Injector { private var wallpaperStatusChecker: WallpaperStatusChecker? = null private var flags: BaseFlags? = null private var undoInteractor: UndoInteractor? = null + private var wallpaperInteractor: WallpaperInteractor? = null + private var wallpaperSnapshotRestorer: WallpaperSnapshotRestorer? = null + private var secureSettingsRepository: SecureSettingsRepository? = null + private var wallpaperColorsViewModel: WallpaperColorsViewModel? = null @Synchronized override fun getAlarmManagerWrapper(context: Context): AlarmManagerWrapper { @@ -94,8 +105,7 @@ open class WallpaperPicker2Injector : Injector { } } - override fun getCustomizationSections(activity: Activity): CustomizationSections { - + override fun getCustomizationSections(activity: ComponentActivity): CustomizationSections { return customizationSections ?: WallpaperPickerSections().also { customizationSections = it } } @@ -123,7 +133,6 @@ open class WallpaperPicker2Injector : Injector { override fun getEffectsController( context: Context, - listener: EffectsServiceListener ): EffectsController? { return null } @@ -136,8 +145,7 @@ open class WallpaperPicker2Injector : Injector { } } - @Synchronized - override fun getIndividualPickerFragment(collectionId: String): Fragment { + override fun getIndividualPickerFragment(context: Context, collectionId: String): Fragment { return IndividualPickerFragment.newInstance(collectionId) } @@ -275,6 +283,42 @@ open class WallpaperPicker2Injector : Injector { } } + override fun getWallpaperInteractor(context: Context): WallpaperInteractor { + return wallpaperInteractor + ?: WallpaperInteractor( + repository = + WallpaperRepository( + scope = GlobalScope, + client = WallpaperClientImpl(context = context), + backgroundDispatcher = Dispatchers.IO, + ), + ) + .also { wallpaperInteractor = it } + } + + override fun getWallpaperSnapshotRestorer(context: Context): WallpaperSnapshotRestorer { + return wallpaperSnapshotRestorer + ?: WallpaperSnapshotRestorer( + scope = GlobalScope, + interactor = getWallpaperInteractor(context), + ) + .also { wallpaperSnapshotRestorer = it } + } + + protected fun getSecureSettingsRepository(context: Context): SecureSettingsRepository { + return secureSettingsRepository + ?: SecureSettingsRepositoryImpl( + contentResolver = context.contentResolver, + backgroundDispatcher = Dispatchers.IO, + ) + .also { secureSettingsRepository = it } + } + + override fun getWallpaperColorsViewModel(): WallpaperColorsViewModel { + return wallpaperColorsViewModel + ?: WallpaperColorsViewModel().also { wallpaperColorsViewModel = it } + } + companion object { /** * When this injector is overridden, this is the minimal value that should be used by diff --git a/src/com/android/wallpaper/module/WallpaperPickerSections.java b/src/com/android/wallpaper/module/WallpaperPickerSections.java index 16782ce3..a77bf2e0 100644 --- a/src/com/android/wallpaper/module/WallpaperPickerSections.java +++ b/src/com/android/wallpaper/module/WallpaperPickerSections.java @@ -12,8 +12,10 @@ import com.android.wallpaper.model.PermissionRequester; import com.android.wallpaper.model.WallpaperColorsViewModel; import com.android.wallpaper.model.WallpaperPreviewNavigator; import com.android.wallpaper.model.WallpaperSectionController; -import com.android.wallpaper.model.WorkspaceViewModel; +import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor; import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewSectionController; +import com.android.wallpaper.picker.customization.ui.section.WallpaperQuickSwitchSectionController; +import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperQuickSwitchViewModel; import com.android.wallpaper.util.DisplayUtils; import java.util.ArrayList; @@ -23,18 +25,19 @@ import java.util.List; public final class WallpaperPickerSections implements CustomizationSections { @Override - public List<CustomizationSectionController<?>> getSectionControllersForScreen( + public List<CustomizationSectionController<?>> getRevampedUISectionControllersForScreen( Screen screen, FragmentActivity activity, LifecycleOwner lifecycleOwner, WallpaperColorsViewModel wallpaperColorsViewModel, - WorkspaceViewModel workspaceViewModel, PermissionRequester permissionRequester, WallpaperPreviewNavigator wallpaperPreviewNavigator, CustomizationSectionNavigationController sectionNavigationController, @Nullable Bundle savedInstanceState, CurrentWallpaperInfoFactory wallpaperInfoFactory, - DisplayUtils displayUtils) { + DisplayUtils displayUtils, + WallpaperQuickSwitchViewModel wallpaperQuickSwitchViewModel, + WallpaperInteractor wallpaperInteractor) { List<CustomizationSectionController<?>> sectionControllers = new ArrayList<>(); sectionControllers.add( @@ -44,7 +47,15 @@ public final class WallpaperPickerSections implements CustomizationSections { screen, wallpaperInfoFactory, wallpaperColorsViewModel, - displayUtils)); + displayUtils, + sectionNavigationController, + wallpaperInteractor)); + sectionControllers.add( + new WallpaperQuickSwitchSectionController( + screen, + wallpaperQuickSwitchViewModel, + lifecycleOwner, + sectionNavigationController)); return sectionControllers; } @@ -54,7 +65,6 @@ public final class WallpaperPickerSections implements CustomizationSections { FragmentActivity activity, LifecycleOwner lifecycleOwner, WallpaperColorsViewModel wallpaperColorsViewModel, - WorkspaceViewModel workspaceViewModel, PermissionRequester permissionRequester, WallpaperPreviewNavigator wallpaperPreviewNavigator, CustomizationSectionNavigationController sectionNavigationController, @@ -62,10 +72,17 @@ public final class WallpaperPickerSections implements CustomizationSections { DisplayUtils displayUtils) { List<CustomizationSectionController<?>> sectionControllers = new ArrayList<>(); - sectionControllers.add(new WallpaperSectionController( - activity, lifecycleOwner, permissionRequester, wallpaperColorsViewModel, - workspaceViewModel, sectionNavigationController, wallpaperPreviewNavigator, - savedInstanceState, displayUtils)); + sectionControllers.add( + new WallpaperSectionController( + activity, + lifecycleOwner, + permissionRequester, + wallpaperColorsViewModel, + null, + sectionNavigationController, + wallpaperPreviewNavigator, + savedInstanceState, + displayUtils)); return sectionControllers; } diff --git a/src/com/android/wallpaper/module/WallpaperPreferenceKeys.java b/src/com/android/wallpaper/module/WallpaperPreferenceKeys.java index 4b0e5da2..12929bcc 100755 --- a/src/com/android/wallpaper/module/WallpaperPreferenceKeys.java +++ b/src/com/android/wallpaper/module/WallpaperPreferenceKeys.java @@ -73,9 +73,10 @@ public class WallpaperPreferenceKeys { "num_days_daily_rotation_failed"; String KEY_NUM_DAYS_DAILY_ROTATION_NOT_ATTEMPTED = "num_days_daily_rotation_not_attempted"; - String KEY_HOME_WALLPAPER_PACKAGE_NAME = "home_wallpaper_package_name"; String KEY_HOME_WALLPAPER_SERVICE_NAME = "home_wallpaper_service_name"; + String KEY_LOCK_WALLPAPER_SERVICE_NAME = "lock_wallpaper_service_name"; String KEY_PREVIEW_WALLPAPER_COLOR_ID = "preview_wallpaper_color_id"; - String KEY_WALLPAPER_EFFECTS = "wallpaper_effects"; + String KEY_HOME_WALLPAPER_EFFECTS = "home_wallpaper_effects"; + String KEY_LOCK_WALLPAPER_EFFECTS = "lock_wallpaper_effects"; } } diff --git a/src/com/android/wallpaper/module/WallpaperPreferences.java b/src/com/android/wallpaper/module/WallpaperPreferences.java index 184972b1..30bcd72b 100755 --- a/src/com/android/wallpaper/module/WallpaperPreferences.java +++ b/src/com/android/wallpaper/module/WallpaperPreferences.java @@ -17,6 +17,7 @@ package com.android.wallpaper.module; import android.annotation.TargetApi; import android.app.WallpaperColors; +import android.app.WallpaperManager.SetWallpaperFlags; import android.graphics.Bitmap; import android.os.Build; @@ -140,16 +141,6 @@ public interface WallpaperPreferences { void setHomeWallpaperHashCode(long hashCode); /** - * Gets the home wallpaper's package name, which is present for live wallpapers. - */ - String getHomeWallpaperPackageName(); - - /** - * Sets the home wallpaper's package name, which is present for live wallpapers. - */ - void setHomeWallpaperPackageName(String packageName); - - /** * Gets the home wallpaper's service name, which is present for live wallpapers. */ String getHomeWallpaperServiceName(); @@ -184,6 +175,18 @@ public interface WallpaperPreferences { void setHomeWallpaperRemoteId(String wallpaperRemoteId); /** + * Gets the home wallpaper's effects. + */ + String getHomeWallpaperEffects(); + + /** + * Sets the home wallpaper's effects to SharedPreferences. + * + * @param wallpaperEffects The wallpaper effects. + */ + void setHomeWallpaperEffects(String wallpaperEffects); + + /** * Returns the lock wallpaper's action URL or null if there is none. */ String getLockWallpaperActionUrl(); @@ -262,6 +265,16 @@ public interface WallpaperPreferences { void setLockWallpaperHashCode(long hashCode); /** + * Gets the lock wallpaper's service name, which is present for live wallpapers. + */ + String getLockWallpaperServiceName(); + + /** + * Sets the lock wallpaper's service name, which is present for live wallpapers. + */ + void setLockWallpaperServiceName(String serviceName); + + /** * Gets the lock wallpaper's ID, which is provided by WallpaperManager for static wallpapers. */ @TargetApi(Build.VERSION_CODES.N) @@ -286,6 +299,18 @@ public interface WallpaperPreferences { void setLockWallpaperRemoteId(String wallpaperRemoteId); /** + * Gets the lock wallpaper's effects. + */ + String getLockWallpaperEffects(); + + /** + * Sets the lock wallpaper's effects to SharedPreferences. + * + * @param wallpaperEffects The wallpaper effects. + */ + void setLockWallpaperEffects(String wallpaperEffects); + + /** * Persists the timestamp of a daily wallpaper rotation that just occurred. */ void addDailyRotation(long timestamp); @@ -505,18 +530,6 @@ public interface WallpaperPreferences { String wallpaperId); /** - * Gets the wallpaper's effects. - */ - String getWallpaperEffects(); - - /** - * Sets the wallpaper's effects to SharedPreferences. - * - * @param wallpaperEffects The wallpaper effects. - */ - void setWallpaperEffects(String wallpaperEffects); - - /** * The possible wallpaper presentation modes, i.e., either "static" or "rotating". */ @IntDef({ @@ -545,29 +558,33 @@ public interface WallpaperPreferences { /** * Stores the given live wallpaper in the recent wallpapers list + * @param which flag indicating the wallpaper destination * @param wallpaperId unique identifier for this wallpaper * @param wallpaper {@link LiveWallpaperInfo} for the applied wallpaper * @param colors WallpaperColors to be used as placeholder for quickswitching */ - default void storeLatestHomeWallpaper(String wallpaperId, + default void storeLatestWallpaper(@SetWallpaperFlags int which, String wallpaperId, @NonNull LiveWallpaperInfo wallpaper, WallpaperColors colors) { // Do nothing in the default case. } /** * Stores the given static wallpaper data in the recent wallpapers list. + * @param which flag indicating the wallpaper destination * @param wallpaperId unique identifier for this wallpaper * @param wallpaper {@link WallpaperInfo} for the applied wallpaper * @param croppedWallpaperBitmap wallpaper bitmap exactly as applied to WallaperManager * @param colors WallpaperColors to be used as placeholder for quickswitching */ - default void storeLatestHomeWallpaper(String wallpaperId, @NonNull WallpaperInfo wallpaper, + default void storeLatestWallpaper(@SetWallpaperFlags int which, String wallpaperId, + @NonNull WallpaperInfo wallpaper, @NonNull Bitmap croppedWallpaperBitmap, WallpaperColors colors) { // Do nothing in the default case. } /** * Stores the given static wallpaper data in the recent wallpapers list. + * @param which flag indicating the wallpaper destination * @param wallpaperId unique identifier for this wallpaper * @param attributions List of attribution items. * @param actionUrl The action or "explore" URL for the wallpaper. @@ -575,7 +592,9 @@ public interface WallpaperPreferences { * @param croppedWallpaperBitmap wallpaper bitmap exactly as applied to WallaperManager * @param colors {@link WallpaperColors} to be used as placeholder for quickswitching */ - default void storeLatestHomeWallpaper(String wallpaperId, List<String> attributions, + default void storeLatestWallpaper( + @SetWallpaperFlags int which, + String wallpaperId, List<String> attributions, String actionUrl, String collectionId, @NonNull Bitmap croppedWallpaperBitmap, WallpaperColors colors) { // Do nothing in the default case. diff --git a/src/com/android/wallpaper/module/WallpaperSetter.java b/src/com/android/wallpaper/module/WallpaperSetter.java index 6924a078..b52cfd90 100644 --- a/src/com/android/wallpaper/module/WallpaperSetter.java +++ b/src/com/android/wallpaper/module/WallpaperSetter.java @@ -172,10 +172,11 @@ public class WallpaperSetter { wallpaper, wallpaperAsset, cropRect, wallpaperScale, destination, new SetWallpaperCallback() { @Override - public void onSuccess(WallpaperInfo wallpaperInfo) { + public void onSuccess(WallpaperInfo wallpaperInfo, + @Destination int destination) { onWallpaperApplied(wallpaper, containerActivity); if (callback != null) { - callback.onSuccess(wallpaper); + callback.onSuccess(wallpaper, destination); } } @@ -197,21 +198,25 @@ public class WallpaperSetter { // wallpaper and restore after setting the wallpaper finishes. saveAndLockScreenOrientationIfNeeded(activity); - if (destination == WallpaperPersister.DEST_LOCK_SCREEN) { + WallpaperManager wallpaperManager = WallpaperManager.getInstance(activity); + if (destination == WallpaperPersister.DEST_LOCK_SCREEN + && !wallpaperManager.isLockscreenLiveWallpaperEnabled()) { throw new IllegalArgumentException( - "Live wallpaper cannot be applied on lock screen only"); + "Live wallpaper cannot be applied on lock screen only"); } - WallpaperManager wallpaperManager = WallpaperManager.getInstance(activity); + setWallpaperComponent(wallpaperManager, wallpaper, destination); wallpaperManager.setWallpaperOffsetSteps(0.5f /* xStep */, 0.0f /* yStep */); wallpaperManager.setWallpaperOffsets( activity.getWindow().getDecorView().getRootView().getWindowToken(), 0.5f /* xOffset */, 0.0f /* yOffset */); - mPreferences.storeLatestHomeWallpaper(wallpaper.getWallpaperId(), wallpaper, colors); + mPreferences.storeLatestWallpaper(WallpaperPersister.destinationToFlags(destination), + wallpaper.getWallpaperId(), wallpaper, colors); onWallpaperApplied(wallpaper, activity); if (callback != null) { - callback.onSuccess(wallpaper); + callback.onSuccess(wallpaper, destination); } + mWallpaperPersister.onLiveWallpaperSet(destination); } catch (RuntimeException | IOException e) { onWallpaperApplyError(e, activity); if (callback != null) { @@ -233,7 +238,8 @@ public class WallpaperSetter { wallpaperManager.setWallpaperComponent( wallpaper.getWallpaperComponent().getComponent()); } - if (destination == WallpaperPersister.DEST_BOTH) { + if (!wallpaperManager.isLockscreenLiveWallpaperEnabled() + && destination == WallpaperPersister.DEST_BOTH) { wallpaperManager.clear(FLAG_LOCK); } } @@ -257,14 +263,16 @@ public class WallpaperSetter { } WallpaperManager wallpaperManager = WallpaperManager.getInstance(context); setWallpaperComponent(wallpaperManager, wallpaper, destination); - mPreferences.storeLatestHomeWallpaper(wallpaper.getWallpaperId(), wallpaper, - colors != null ? colors : + mPreferences.storeLatestWallpaper(WallpaperPersister.destinationToFlags(destination), + wallpaper.getWallpaperId(), + wallpaper, colors != null ? colors : WallpaperColors.fromBitmap(wallpaper.getThumbAsset(context) .getLowResBitmap(context))); // Not call onWallpaperApplied() as no UI is presented. if (callback != null) { - callback.onSuccess(wallpaper); + callback.onSuccess(wallpaper, destination); } + mWallpaperPersister.onLiveWallpaperSet(destination); } catch (RuntimeException | IOException e) { // Not call onWallpaperApplyError() as no UI is presented. if (callback != null) { @@ -353,6 +361,15 @@ public class WallpaperSetter { } }; + WallpaperManager wallpaperManager = WallpaperManager.getInstance(activity); + SetWallpaperDialogFragment setWallpaperDialog = new SetWallpaperDialogFragment(); + setWallpaperDialog.setTitleResId(titleResId); + setWallpaperDialog.setListener(listenerWrapper); + if (wallpaperManager.isLockscreenLiveWallpaperEnabled()) { + setWallpaperDialog.show(fragmentManager, TAG_SET_WALLPAPER_DIALOG_FRAGMENT); + return; + } + WallpaperStatusChecker wallpaperStatusChecker = InjectorProvider.getInjector().getWallpaperStatusChecker(); boolean isLiveWallpaperSet = @@ -361,9 +378,6 @@ public class WallpaperSetter { boolean isBuiltIn = !isLiveWallpaperSet && !wallpaperStatusChecker.isHomeStaticWallpaperSet(activity); - SetWallpaperDialogFragment setWallpaperDialog = new SetWallpaperDialogFragment(); - setWallpaperDialog.setTitleResId(titleResId); - setWallpaperDialog.setListener(listenerWrapper); if ((isLiveWallpaperSet || isBuiltIn) && !wallpaperStatusChecker.isLockWallpaperSet(activity)) { if (isLiveWallpaper) { diff --git a/src/com/android/wallpaper/picker/CategorySelectorFragment.java b/src/com/android/wallpaper/picker/CategorySelectorFragment.java index 2b6c71dc..fe36592e 100644 --- a/src/com/android/wallpaper/picker/CategorySelectorFragment.java +++ b/src/com/android/wallpaper/picker/CategorySelectorFragment.java @@ -47,6 +47,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.wallpaper.R; import com.android.wallpaper.asset.Asset; +import com.android.wallpaper.effects.EffectsController; import com.android.wallpaper.model.Category; import com.android.wallpaper.model.CategoryProvider; import com.android.wallpaper.model.LiveWallpaperInfo; @@ -299,6 +300,11 @@ public class CategorySelectorFragment extends AppbarFragment { eventLogger.logCategorySelected(mCategory.getCollectionId()); if (mCategory.supportsCustomPhotos()) { + EffectsController effectsController = + InjectorProvider.getInjector().getEffectsController(getContext()); + if (!effectsController.isEffectTriggered()) { + effectsController.triggerEffect(getContext()); + } getCategorySelectorFragmentHost().requestCustomPhotoPicker( new MyPhotosStarter.PermissionChangedListener() { @Override diff --git a/src/com/android/wallpaper/picker/CustomizationPickerActivity.java b/src/com/android/wallpaper/picker/CustomizationPickerActivity.java index 2351c72e..3f8c2d03 100644 --- a/src/com/android/wallpaper/picker/CustomizationPickerActivity.java +++ b/src/com/android/wallpaper/picker/CustomizationPickerActivity.java @@ -52,7 +52,6 @@ import com.android.wallpaper.picker.AppbarFragment.AppbarFragmentHost; import com.android.wallpaper.picker.CategorySelectorFragment.CategorySelectorFragmentHost; import com.android.wallpaper.picker.MyPhotosStarter.PermissionChangedListener; import com.android.wallpaper.picker.individual.IndividualPickerFragment.IndividualPickerFragmentHost; -import com.android.wallpaper.picker.undo.domain.interactor.UndoInteractor; import com.android.wallpaper.util.ActivityUtils; import com.android.wallpaper.util.DeepLinkUtils; import com.android.wallpaper.util.LaunchUtils; @@ -79,7 +78,7 @@ public class CustomizationPickerActivity extends FragmentActivity implements App private BottomActionBar mBottomActionBar; private boolean mIsSafeToCommitFragmentTransaction; - @Nullable private UndoInteractor mUndoInteractor; + private boolean mIsUseRevampedUi; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -109,7 +108,9 @@ public class CustomizationPickerActivity extends FragmentActivity implements App // See go/pdr-edge-to-edge-guide. WindowCompat.setDecorFitsSystemWindows(getWindow(), isSUWMode(this)); - final boolean isUseRevampedUi = injector.getFlags().isUseRevampedUi(this); + mIsUseRevampedUi = injector.getFlags().isUseRevampedUiEnabled(this); + final boolean startFromLockScreen = getIntent() == null + || !ActivityUtils.isLaunchedFromLauncher(getIntent()); Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container); if (fragment == null) { @@ -122,17 +123,30 @@ public class CustomizationPickerActivity extends FragmentActivity implements App // Switch to the target fragment. switchFragment(isWallpaperOnlyMode(getIntent()) - ? new WallpaperOnlyFragment() - : CustomizationPickerFragment.newInstance(isUseRevampedUi)); + ? WallpaperOnlyFragment.newInstance(mIsUseRevampedUi) + : CustomizationPickerFragment.newInstance( + mIsUseRevampedUi, startFromLockScreen)); + + // Cache the categories, but only if we're not restoring state (b/276767415). + mDelegate.prefetchCategories(); } - if (isUseRevampedUi) { - mUndoInteractor = injector.getUndoInteractor(this); - mUndoInteractor.startSession(); + if (savedInstanceState == null) { + // We only want to start a new undo session if this activity is brand-new. A non-new + // activity will have a non-null savedInstanceState. + if (mIsUseRevampedUi) { + injector.getUndoInteractor(this).startSession(); + } } final Intent intent = getIntent(); final String navigationDestination = intent.getStringExtra(EXTRA_DESTINATION); + // Consume the destination and commit the intent back so the OS doesn't revert to the same + // destination when we change color or wallpaper (which causes the activity to be + // recreated). + intent.removeExtra(EXTRA_DESTINATION); + setIntent(intent); + final String deepLinkCollectionId = DeepLinkUtils.getCollectionId(intent); if (!TextUtils.isEmpty(navigationDestination)) { @@ -147,10 +161,9 @@ public class CustomizationPickerActivity extends FragmentActivity implements App // Wallpaper Collection deep link case switchFragmentWithBackStack(new CategorySelectorFragment()); switchFragmentWithBackStack(InjectorProvider.getInjector().getIndividualPickerFragment( - deepLinkCollectionId)); + this, deepLinkCollectionId)); intent.setData(null); } - mDelegate.prefetchCategories(); } @Override @@ -254,7 +267,7 @@ public class CustomizationPickerActivity extends FragmentActivity implements App return; } switchFragmentWithBackStack(InjectorProvider.getInjector().getIndividualPickerFragment( - category.getCollectionId())); + this, category.getCollectionId())); } @Override @@ -331,6 +344,15 @@ public class CustomizationPickerActivity extends FragmentActivity implements App if (mDelegate.handleActivityResult(requestCode, resultCode, data)) { if (isSUWMode(this)) { finishActivityForSUW(); + } else if (mIsUseRevampedUi) { + // We don't finish in the revamped UI to let the user have a chance to reset the + // change they made, should they want to. We do, however, remove all the fragments + // from our back stack to reveal the root fragment, revealing the main screen of the + // app. + final FragmentManager fragmentManager = getSupportFragmentManager(); + while (fragmentManager.getBackStackEntryCount() > 0) { + fragmentManager.popBackStackImmediate(); + } } else { finishActivityWithResultOk(); } diff --git a/src/com/android/wallpaper/picker/CustomizationPickerFragment.java b/src/com/android/wallpaper/picker/CustomizationPickerFragment.java index 380fe13c..cb0820d0 100644 --- a/src/com/android/wallpaper/picker/CustomizationPickerFragment.java +++ b/src/com/android/wallpaper/picker/CustomizationPickerFragment.java @@ -33,34 +33,40 @@ import com.android.wallpaper.R; import com.android.wallpaper.model.CustomizationSectionController; import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController; import com.android.wallpaper.model.PermissionRequester; -import com.android.wallpaper.model.WallpaperColorsViewModel; import com.android.wallpaper.model.WallpaperPreviewNavigator; -import com.android.wallpaper.model.WorkspaceViewModel; import com.android.wallpaper.module.CustomizationSections; import com.android.wallpaper.module.FragmentFactory; import com.android.wallpaper.module.Injector; import com.android.wallpaper.module.InjectorProvider; import com.android.wallpaper.picker.customization.ui.binder.CustomizationPickerBinder; import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel; +import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperQuickSwitchViewModel; import com.android.wallpaper.util.ActivityUtils; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import kotlinx.coroutines.DisposableHandle; + /** The Fragment UI for customization sections. */ public class CustomizationPickerFragment extends AppbarFragment implements CustomizationSectionNavigationController { private static final String TAG = "CustomizationPickerFragment"; private static final String SCROLL_POSITION_Y = "SCROLL_POSITION_Y"; - private static final String KEY_IS_USE_REVAMPED_UI = "is_use_revamped_ui"; + protected static final String KEY_IS_USE_REVAMPED_UI = "is_use_revamped_ui"; + private static final String KEY_START_FROM_LOCK_SCREEN = "start_from_lock_screen"; + private DisposableHandle mBinding; /** Returns a new instance of {@link CustomizationPickerFragment}. */ - public static CustomizationPickerFragment newInstance(boolean isUseRevampedUi) { + public static CustomizationPickerFragment newInstance( + boolean isUseRevampedUi, + boolean startFromLockScreen) { final CustomizationPickerFragment fragment = new CustomizationPickerFragment(); final Bundle args = new Bundle(); args.putBoolean(KEY_IS_USE_REVAMPED_UI, isUseRevampedUi); + args.putBoolean(KEY_START_FROM_LOCK_SCREEN, startFromLockScreen); fragment.setArguments(args); return fragment; } @@ -68,9 +74,11 @@ public class CustomizationPickerFragment extends AppbarFragment implements // Note that the section views will be displayed by the list ordering. private final List<CustomizationSectionController<?>> mSectionControllers = new ArrayList<>(); private NestedScrollView mNestedScrollView; - @Nullable private Bundle mBackStackSavedInstanceState; + @Nullable + private Bundle mBackStackSavedInstanceState; private final FragmentFactory mFragmentFactory; - @Nullable private CustomizationPickerViewModel mViewModel; + @Nullable + private CustomizationPickerViewModel mViewModel; public CustomizationPickerFragment() { mFragmentFactory = InjectorProvider.getInjector().getFragmentFactory(); @@ -79,9 +87,11 @@ public class CustomizationPickerFragment extends AppbarFragment implements @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @Nullable Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.collapsing_toolbar_container_layout, - container, /* attachToRoot= */ false); - + final boolean shouldUseRevampedUi = shouldUseRevampedUi(); + final int layoutId = shouldUseRevampedUi + ? R.layout.toolbar_container_layout + : R.layout.collapsing_toolbar_container_layout; + final View view = inflater.inflate(layoutId, container, false); if (ActivityUtils.isLaunchedFromSettingsRelated(getActivity().getIntent())) { setUpToolbar(view, !ActivityEmbeddingUtils.shouldHideNavigateUpButton( getActivity(), /* isSecondLayerPage= */ true)); @@ -90,16 +100,7 @@ public class CustomizationPickerFragment extends AppbarFragment implements } final Injector injector = InjectorProvider.getInjector(); - final Bundle args = getArguments(); - final boolean isUseRevampedUi; - if (args != null && args.containsKey(KEY_IS_USE_REVAMPED_UI)) { - isUseRevampedUi = args.getBoolean(KEY_IS_USE_REVAMPED_UI); - } else { - throw new IllegalStateException( - "Must contain KEY_IS_USE_REVAMPED_UI argument, did you instantiate directly" - + " instead of using the newInstance function?"); - } - if (isUseRevampedUi) { + if (shouldUseRevampedUi) { setContentView(view, R.layout.fragment_tabbed_customization_picker); mViewModel = new ViewModelProvider( this, @@ -108,19 +109,26 @@ public class CustomizationPickerFragment extends AppbarFragment implements savedInstanceState, injector.getUndoInteractor(requireContext())) ).get(CustomizationPickerViewModel.class); + final Bundle arguments = getArguments(); + mViewModel.setInitialScreen( + arguments != null && arguments.getBoolean(KEY_START_FROM_LOCK_SCREEN)); setUpToolbarMenu(R.menu.undoable_customization_menu); final Bundle finalSavedInstanceState = savedInstanceState; - CustomizationPickerBinder.bind( + if (mBinding != null) { + mBinding.dispose(); + } + mBinding = CustomizationPickerBinder.bind( view, getToolbarId(), mViewModel, this, - isOnLockScreen -> getSectionControllers( - isOnLockScreen - ? CustomizationSections.Screen.LOCK_SCREEN - : CustomizationSections.Screen.HOME_SCREEN, - finalSavedInstanceState)); + isOnLockScreen -> filterAvailableSections( + getSectionControllers( + isOnLockScreen + ? CustomizationSections.Screen.LOCK_SCREEN + : CustomizationSections.Screen.HOME_SCREEN, + finalSavedInstanceState))); } else { setContentView(view, R.layout.fragment_customization_picker); } @@ -132,7 +140,7 @@ public class CustomizationPickerFragment extends AppbarFragment implements mNestedScrollView = view.findViewById(R.id.scroll_container); - if (!isUseRevampedUi) { + if (!shouldUseRevampedUi) { ViewGroup sectionContainer = view.findViewById(R.id.section_container); sectionContainer.setOnApplyWindowInsetsListener((v, windowInsets) -> { v.setPadding( @@ -187,12 +195,12 @@ public class CustomizationPickerFragment extends AppbarFragment implements @Override protected int getToolbarId() { - return R.id.action_bar; + return shouldUseRevampedUi() ? R.id.toolbar : R.id.action_bar; } @Override protected int getToolbarColorId() { - return android.R.color.transparent; + return shouldUseRevampedUi() ? R.color.toolbar_color : android.R.color.transparent; } @Override @@ -257,14 +265,10 @@ public class CustomizationPickerFragment extends AppbarFragment implements mSectionControllers.clear(); mSectionControllers.addAll( - getAvailableSections(getAvailableSectionControllers(savedInstanceState))); - } - - private List<CustomizationSectionController<?>> getAvailableSectionControllers( - @Nullable Bundle savedInstanceState) { - return getSectionControllers( - null, - savedInstanceState); + filterAvailableSections( + getSectionControllers( + null, + savedInstanceState))); } private List<CustomizationSectionController<?>> getSectionControllers( @@ -272,50 +276,55 @@ public class CustomizationPickerFragment extends AppbarFragment implements @Nullable Bundle savedInstanceState) { final Injector injector = InjectorProvider.getInjector(); - WallpaperColorsViewModel wcViewModel = new ViewModelProvider(getActivity()) - .get(WallpaperColorsViewModel.class); - WorkspaceViewModel workspaceViewModel = new ViewModelProvider(getActivity()) - .get(WorkspaceViewModel.class); + WallpaperQuickSwitchViewModel wallpaperQuickSwitchViewModel = new ViewModelProvider( + getActivity(), + WallpaperQuickSwitchViewModel.newFactory( + this, + savedInstanceState, + injector.getWallpaperInteractor(requireContext()))) + .get(WallpaperQuickSwitchViewModel.class); CustomizationSections sections = injector.getCustomizationSections(getActivity()); if (screen == null) { return sections.getAllSectionControllers( getActivity(), getViewLifecycleOwner(), - wcViewModel, - workspaceViewModel, + injector.getWallpaperColorsViewModel(), getPermissionRequester(), getWallpaperPreviewNavigator(), this, savedInstanceState, injector.getDisplayUtils(getActivity())); } else { - return sections.getSectionControllersForScreen( + return sections.getRevampedUISectionControllersForScreen( screen, getActivity(), getViewLifecycleOwner(), - wcViewModel, - workspaceViewModel, + injector.getWallpaperColorsViewModel(), getPermissionRequester(), getWallpaperPreviewNavigator(), this, savedInstanceState, injector.getCurrentWallpaperInfoFactory(requireContext()), - injector.getDisplayUtils(getActivity())); + injector.getDisplayUtils(getActivity()), + wallpaperQuickSwitchViewModel, + injector.getWallpaperInteractor(requireContext())); } } - protected List<CustomizationSectionController<?>> getAvailableSections( + /** Returns a filtered list containing only the available section controllers. */ + protected List<CustomizationSectionController<?>> filterAvailableSections( List<CustomizationSectionController<?>> controllers) { return controllers.stream() .filter(controller -> { - if(controller.isAvailable(getContext())) { + if (controller.isAvailable(getContext())) { return true; } else { controller.release(); Log.d(TAG, "Section is not available: " + controller); return false; - }}) + } + }) .collect(Collectors.toList()); } @@ -326,4 +335,15 @@ public class CustomizationPickerFragment extends AppbarFragment implements private WallpaperPreviewNavigator getWallpaperPreviewNavigator() { return (WallpaperPreviewNavigator) getActivity(); } + + private boolean shouldUseRevampedUi() { + final Bundle args = getArguments(); + if (args != null && args.containsKey(KEY_IS_USE_REVAMPED_UI)) { + return args.getBoolean(KEY_IS_USE_REVAMPED_UI); + } else { + throw new IllegalStateException( + "Must contain KEY_IS_USE_REVAMPED_UI argument, did you instantiate directly" + + " instead of using the newInstance function?"); + } + } } diff --git a/src/com/android/wallpaper/picker/DisplayAspectRatioLinearLayout.kt b/src/com/android/wallpaper/picker/DisplayAspectRatioLinearLayout.kt new file mode 100644 index 00000000..f970a9b9 --- /dev/null +++ b/src/com/android/wallpaper/picker/DisplayAspectRatioLinearLayout.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import androidx.core.view.children +import androidx.core.view.updateLayoutParams +import com.android.wallpaper.util.ScreenSizeCalculator + +/** + * [LinearLayout] that sizes its children using a fixed aspect ratio that is the same as that of the + * display, and can lay out multiple children horizontally with margin + */ +class DisplayAspectRatioLinearLayout( + context: Context, + attrs: AttributeSet?, +) : LinearLayout(context, attrs) { + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val screenAspectRatio = ScreenSizeCalculator.getInstance().getScreenAspectRatio(context) + val parentWidth = this.measuredWidth + val parentHeight = this.measuredHeight + val itemSpacingPx = ITEM_SPACING_DP.toPx(context.resources.displayMetrics.density) + val (childWidth, childHeight) = + if (orientation == HORIZONTAL) { + val availableWidth = + parentWidth - paddingStart - paddingEnd - (childCount - 1) * itemSpacingPx + val availableHeight = parentHeight - paddingTop - paddingBottom + var width = availableWidth / childCount + var height = (width * screenAspectRatio).toInt() + if (height > availableHeight) { + height = availableHeight + width = (height / screenAspectRatio).toInt() + } + width to height + } else { + val availableWidth = parentWidth - paddingStart - paddingEnd + val availableHeight = + parentHeight - paddingTop - paddingBottom - (childCount - 1) * itemSpacingPx + var height = availableHeight / childCount + var width = (height / screenAspectRatio).toInt() + if (width > availableWidth) { + width = availableWidth + height = (width * screenAspectRatio).toInt() + } + width to height + } + + val itemSpacingHalfPx = ITEM_SPACING_DP_HALF.toPx(context.resources.displayMetrics.density) + children.forEachIndexed { index, child -> + val addSpacingToStart = index > 0 + val addSpacingToEnd = index < (childCount - 1) + if (orientation == HORIZONTAL) { + child.updateLayoutParams<MarginLayoutParams> { + if (addSpacingToStart) this.marginStart = itemSpacingHalfPx + if (addSpacingToEnd) this.marginEnd = itemSpacingHalfPx + } + } else { + child.updateLayoutParams<MarginLayoutParams> { + if (addSpacingToStart) this.topMargin = itemSpacingHalfPx + if (addSpacingToEnd) this.bottomMargin = itemSpacingHalfPx + } + } + + child.measure( + MeasureSpec.makeMeasureSpec( + childWidth, + MeasureSpec.EXACTLY, + ), + MeasureSpec.makeMeasureSpec( + childHeight, + MeasureSpec.EXACTLY, + ), + ) + } + } + + private fun Int.toPx(density: Float): Int { + return (this * density).toInt() + } + + companion object { + private const val ITEM_SPACING_DP = 12 + private const val ITEM_SPACING_DP_HALF = ITEM_SPACING_DP / 2 + } +} diff --git a/src/com/android/wallpaper/picker/FullPreviewActivity.java b/src/com/android/wallpaper/picker/FullPreviewActivity.java index ce780644..5a22b62c 100755 --- a/src/com/android/wallpaper/picker/FullPreviewActivity.java +++ b/src/com/android/wallpaper/picker/FullPreviewActivity.java @@ -17,20 +17,21 @@ package com.android.wallpaper.picker; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.os.Bundle; import android.transition.Slide; -import android.view.View; import android.view.Window; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import com.android.wallpaper.R; -import com.android.wallpaper.model.InlinePreviewIntentFactory; +import com.android.wallpaper.config.BaseFlags; import com.android.wallpaper.model.WallpaperInfo; import com.android.wallpaper.module.InjectorProvider; import com.android.wallpaper.picker.AppbarFragment.AppbarFragmentHost; import com.android.wallpaper.util.ActivityUtils; +import com.android.wallpaper.util.DisplayUtils; /** * Activity that displays a full preview of a specific wallpaper and provides the ability to set the @@ -43,7 +44,7 @@ public class FullPreviewActivity extends BasePreviewActivity implements AppbarFr */ public static Intent newIntent(Context packageContext, WallpaperInfo wallpaperInfo) { Intent intent = new Intent(packageContext, FullPreviewActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.putExtra(EXTRA_WALLPAPER_INFO, wallpaperInfo); return intent; } @@ -74,7 +75,9 @@ public class FullPreviewActivity extends BasePreviewActivity implements AppbarFr if (fragment == null) { Intent intent = getIntent(); WallpaperInfo wallpaper = intent.getParcelableExtra(EXTRA_WALLPAPER_INFO); - boolean viewAsHome = intent.getBooleanExtra(EXTRA_VIEW_AS_HOME, true); + BaseFlags flags = InjectorProvider.getInjector().getFlags(); + boolean viewAsHome = intent.getBooleanExtra(EXTRA_VIEW_AS_HOME, !flags + .isFullscreenWallpaperPreviewEnabled(this)); boolean testingModeEnabled = intent.getBooleanExtra(EXTRA_TESTING_MODE_ENABLED, false); fragment = InjectorProvider.getInjector().getPreviewFragment( /* context= */ this, @@ -99,26 +102,15 @@ public class FullPreviewActivity extends BasePreviewActivity implements AppbarFr return !ActivityUtils.isSUWMode(getBaseContext()); } - /** - * Implementation that provides an intent to start a PreviewActivity. - */ - public static class PreviewActivityIntentFactory implements InlinePreviewIntentFactory { - @Override - public Intent newIntent(Context context, WallpaperInfo wallpaper) { - return FullPreviewActivity.newIntent(context, wallpaper); - } - } - @Override protected void onResume() { super.onResume(); + DisplayUtils displayUtils = InjectorProvider.getInjector().getDisplayUtils(this); + int orientation = displayUtils.isOnWallpaperDisplay(this) + ? ActivityInfo.SCREEN_ORIENTATION_USER : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + setRequestedOrientation(orientation); if (isInMultiWindowMode()) { onBackPressed(); } - // Hide the navigation bar - View decorView = getWindow().getDecorView(); - int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN; - decorView.setSystemUiVisibility(uiOptions); } } diff --git a/src/com/android/wallpaper/picker/ImagePreviewFragment.java b/src/com/android/wallpaper/picker/ImagePreviewFragment.java index 6d75c87b..ba477b7d 100755 --- a/src/com/android/wallpaper/picker/ImagePreviewFragment.java +++ b/src/com/android/wallpaper/picker/ImagePreviewFragment.java @@ -424,7 +424,7 @@ public class ImagePreviewFragment extends PreviewFragment { BitmapCropper bitmapCropper = mInjector.getBitmapCropper(); bitmapCropper.cropAndScaleBitmap(mWallpaperAsset, mFullResImageView.getScale(), - calculateCropRect(context), /* adjustForRtl= */ false, + calculateCropRect(context, /* cropExtraWidth= */ true), /* adjustForRtl= */ false, new BitmapCropper.Callback() { @Override public void onBitmapCropped(Bitmap croppedBitmap) { @@ -546,7 +546,7 @@ public class ImagePreviewFragment extends PreviewFragment { mFullResImageView.setScaleAndCenter(minWallpaperZoom, centerPosition); } - private Rect calculateCropRect(Context context) { + private Rect calculateCropRect(Context context, boolean cropExtraWidth) { float wallpaperZoom = mFullResImageView.getScale(); Context appContext = context.getApplicationContext(); @@ -563,12 +563,13 @@ public class ImagePreviewFragment extends PreviewFragment { Point cropSurfaceSize = WallpaperCropUtils.calculateCropSurfaceSize(res, maxCrop, minCrop, cropWidth, cropHeight); return WallpaperCropUtils.calculateCropRect(appContext, hostViewSize, - cropSurfaceSize, mRawWallpaperSize, visibleFileRect, wallpaperZoom); + cropSurfaceSize, mRawWallpaperSize, visibleFileRect, wallpaperZoom, cropExtraWidth); } @Override protected void setCurrentWallpaper(@Destination int destination) { - Rect cropRect = calculateCropRect(getContext()); + // Only crop extra wallpaper width for single display devices. + Rect cropRect = calculateCropRect(getContext(), !mDisplayUtils.hasMultiInternalDisplays()); float screenScale = WallpaperCropUtils.getScaleOfScreenResolution( mFullResImageView.getScale(), cropRect, mWallpaperScreenSize.x, mWallpaperScreenSize.y); diff --git a/src/com/android/wallpaper/picker/PreviewActivity.java b/src/com/android/wallpaper/picker/PreviewActivity.java index ba39ba4e..e7646250 100755 --- a/src/com/android/wallpaper/picker/PreviewActivity.java +++ b/src/com/android/wallpaper/picker/PreviewActivity.java @@ -25,10 +25,12 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import com.android.wallpaper.R; +import com.android.wallpaper.config.BaseFlags; import com.android.wallpaper.model.ImageWallpaperInfo; import com.android.wallpaper.model.InlinePreviewIntentFactory; import com.android.wallpaper.model.WallpaperInfo; import com.android.wallpaper.module.InjectorProvider; +import com.android.wallpaper.module.LargeScreenMultiPanesChecker; import com.android.wallpaper.picker.AppbarFragment.AppbarFragmentHost; import com.android.wallpaper.util.ActivityUtils; @@ -61,7 +63,9 @@ public class PreviewActivity extends BasePreviewActivity implements AppbarFragme if (fragment == null) { Intent intent = getIntent(); WallpaperInfo wallpaper = intent.getParcelableExtra(EXTRA_WALLPAPER_INFO); - boolean viewAsHome = intent.getBooleanExtra(EXTRA_VIEW_AS_HOME, true); + BaseFlags flags = InjectorProvider.getInjector().getFlags(); + boolean viewAsHome = intent.getBooleanExtra(EXTRA_VIEW_AS_HOME, !flags + .isFullscreenWallpaperPreviewEnabled(this)); boolean testingModeEnabled = intent.getBooleanExtra(EXTRA_TESTING_MODE_ENABLED, false); fragment = InjectorProvider.getInjector().getPreviewFragment( /* context */ this, @@ -115,6 +119,14 @@ public class PreviewActivity extends BasePreviewActivity implements AppbarFragme public static class PreviewActivityIntentFactory implements InlinePreviewIntentFactory { @Override public Intent newIntent(Context context, WallpaperInfo wallpaper) { + LargeScreenMultiPanesChecker multiPanesChecker = new LargeScreenMultiPanesChecker(); + // Launch a full preview activity for devices supporting multipanel mode + if (multiPanesChecker.isMultiPanesEnabled(context) + && InjectorProvider.getInjector().getFlags() + .isFullscreenWallpaperPreviewEnabled(context)) { + return FullPreviewActivity.newIntent(context, wallpaper); + } + return PreviewActivity.newIntent(context, wallpaper); } } diff --git a/src/com/android/wallpaper/picker/PreviewFragment.java b/src/com/android/wallpaper/picker/PreviewFragment.java index e9e78c8a..6671b208 100755 --- a/src/com/android/wallpaper/picker/PreviewFragment.java +++ b/src/com/android/wallpaper/picker/PreviewFragment.java @@ -203,7 +203,7 @@ public abstract class PreviewFragment extends AppbarFragment implements mFullScreenAnimation.getStatusBarHeight(), previewHeader.getPaddingRight(), previewHeader.getPaddingBottom()); - return windowInsets.consumeSystemWindowInsets(); + return windowInsets.CONSUMED; } ); diff --git a/src/com/android/wallpaper/picker/StandalonePreviewActivity.java b/src/com/android/wallpaper/picker/StandalonePreviewActivity.java index f4938059..3b65306c 100755 --- a/src/com/android/wallpaper/picker/StandalonePreviewActivity.java +++ b/src/com/android/wallpaper/picker/StandalonePreviewActivity.java @@ -19,12 +19,13 @@ import static com.android.wallpaper.util.ActivityUtils.startActivityForResultSaf import android.Manifest.permission; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.util.Log; -import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; @@ -62,7 +63,8 @@ public class StandalonePreviewActivity extends BasePreviewActivity implements Ap enableFullScreen(); - mUserEventLogger = InjectorProvider.getInjector().getUserEventLogger(getApplicationContext()); + mUserEventLogger = InjectorProvider.getInjector().getUserEventLogger( + getApplicationContext()); mUserEventLogger.logStandalonePreviewLaunched(); Intent cropAndSetWallpaperIntent = getIntent(); @@ -80,10 +82,11 @@ public class StandalonePreviewActivity extends BasePreviewActivity implements Ap mUserEventLogger.logStandalonePreviewImageUriHasReadPermission( isReadPermissionGrantedForImageUri); - // Request storage permission if necessary (i.e., on Android M and later if storage permission - // has not already been granted) and delay loading the PreviewFragment until the permission is - // granted. - if (!isReadPermissionGrantedForImageUri && !isReadExternalStoragePermissionGrantedForApp()) { + // Request storage permission if necessary (i.e., on Android M and later if storage + // permission has not already been granted) and delay loading the PreviewFragment until the + // permission is granted. + if (!isReadPermissionGrantedForImageUri + && !isReadExternalStoragePermissionGrantedForApp()) { requestPermissions( new String[]{permission.READ_MEDIA_IMAGES}, READ_EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE); @@ -103,8 +106,20 @@ public class StandalonePreviewActivity extends BasePreviewActivity implements Ap } @Override + protected void onResume() { + super.onResume(); + Resources res = getResources(); + boolean isDeviceFoldableOrTablet = res.getBoolean(R.bool.is_large_screen); + + if (!isDeviceFoldableOrTablet) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + } + + @SuppressWarnings("MissingSuperCall") // TODO: Fix me + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { + @NonNull int[] grantResults) { // Load the preview fragment if the storage permission was granted. if (requestCode == READ_EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) { boolean isGranted = permissions.length > 0 @@ -137,14 +152,6 @@ public class StandalonePreviewActivity extends BasePreviewActivity implements Ap return getIntent().getBooleanExtra(KEY_UP_ARROW, false); } - @Override - protected void enableFullScreen() { - super.enableFullScreen(); - getWindow().setFlags( - WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, - WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); - } - /** * Launches multi-pane when it is enabled, for non-Settings' trampoline launch case will * retrieve EXTRA_STREAM's image URI and assign back its intent by calling setData(). @@ -155,22 +162,10 @@ public class StandalonePreviewActivity extends BasePreviewActivity implements Ap Intent intent = getIntent(); if (!ActivityUtils.isLaunchedFromSettingsTrampoline(intent) && !ActivityUtils.isLaunchedFromSettingsRelated(intent)) { - Uri uri = intent.getData(); - if (uri != null) { - // Grant URI permission for next launching activity. - grantUriPermission(getPackageName(), uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (!InjectorProvider.getInjector().getFlags().isFullscreenWallpaperPreviewEnabled( + this)) { + launchMultiPanes(checker); } - - Intent previewLaunch = checker.getMultiPanesIntent(intent); - previewLaunch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - // Put image URI and back arrow condition to separate extras. - .putExtra(Intent.EXTRA_STREAM, intent.getData()) - .putExtra(KEY_UP_ARROW, true); - - startActivityForResultSafely(/* activity= */ this, previewLaunch, /* requestCode= */ - 0); - finish(); } else { Uri uri = intent.hasExtra(Intent.EXTRA_STREAM) ? intent.getParcelableExtra( Intent.EXTRA_STREAM) : null; @@ -181,6 +176,26 @@ public class StandalonePreviewActivity extends BasePreviewActivity implements Ap } } + private void launchMultiPanes(MultiPanesChecker checker) { + Intent intent = getIntent(); + Uri uri = intent.getData(); + if (uri != null) { + // Grant URI permission for next launching activity. + grantUriPermission(getPackageName(), uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + Intent previewLaunch = checker.getMultiPanesIntent(intent); + previewLaunch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // Put image URI and back arrow condition to separate extras. + .putExtra(Intent.EXTRA_STREAM, intent.getData()) + .putExtra(KEY_UP_ARROW, true); + + startActivityForResultSafely(/* activity= */ this, previewLaunch, /* requestCode= */ + 0); + finish(); + } + /** * Creates a new instance of {@link PreviewFragment} and loads the fragment into this activity's * fragment container so that it's shown to the user. diff --git a/src/com/android/wallpaper/picker/TouchForwardingLayout.java b/src/com/android/wallpaper/picker/TouchForwardingLayout.java index 2cb692de..ced59689 100644 --- a/src/com/android/wallpaper/picker/TouchForwardingLayout.java +++ b/src/com/android/wallpaper/picker/TouchForwardingLayout.java @@ -17,6 +17,7 @@ package com.android.wallpaper.picker; import android.content.Context; import android.util.AttributeSet; +import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; @@ -26,13 +27,22 @@ public class TouchForwardingLayout extends FrameLayout { private View mView; private boolean mForwardingEnabled; + private GestureDetector mGestureDetector; public TouchForwardingLayout(Context context, AttributeSet attrs) { super(context, attrs); + mGestureDetector = new GestureDetector(context, + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + return performClick(); + } + }); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { + mGestureDetector.onTouchEvent(ev); if (mView != null && mForwardingEnabled) { mView.dispatchTouchEvent(ev); } diff --git a/src/com/android/wallpaper/picker/ViewOnlyPreviewActivity.java b/src/com/android/wallpaper/picker/ViewOnlyPreviewActivity.java index 4ff3c2ea..7377103f 100755 --- a/src/com/android/wallpaper/picker/ViewOnlyPreviewActivity.java +++ b/src/com/android/wallpaper/picker/ViewOnlyPreviewActivity.java @@ -26,6 +26,7 @@ import com.android.wallpaper.R; import com.android.wallpaper.model.InlinePreviewIntentFactory; import com.android.wallpaper.model.WallpaperInfo; import com.android.wallpaper.module.InjectorProvider; +import com.android.wallpaper.module.LargeScreenMultiPanesChecker; import com.android.wallpaper.picker.AppbarFragment.AppbarFragmentHost; import com.android.wallpaper.util.ActivityUtils; @@ -94,6 +95,14 @@ public class ViewOnlyPreviewActivity extends BasePreviewActivity implements Appb @Override public Intent newIntent(Context context, WallpaperInfo wallpaper) { + LargeScreenMultiPanesChecker multiPanesChecker = new LargeScreenMultiPanesChecker(); + // Launch a full preview activity for devices supporting multipanel mode + if (multiPanesChecker.isMultiPanesEnabled(context) + && InjectorProvider.getInjector().getFlags() + .isFullscreenWallpaperPreviewEnabled(context)) { + return FullPreviewActivity.newIntent(context, wallpaper, mIsViewAsHome); + } + if (mIsHomeAndLockPreviews) { return ViewOnlyPreviewActivity.newIntent(context, wallpaper, mIsViewAsHome); } diff --git a/src/com/android/wallpaper/picker/WallpaperOnlyFragment.java b/src/com/android/wallpaper/picker/WallpaperOnlyFragment.java index 7673df2f..6d99bed9 100644 --- a/src/com/android/wallpaper/picker/WallpaperOnlyFragment.java +++ b/src/com/android/wallpaper/picker/WallpaperOnlyFragment.java @@ -15,6 +15,8 @@ */ package com.android.wallpaper.picker; +import android.os.Bundle; + import com.android.wallpaper.R; import com.android.wallpaper.model.CustomizationSectionController; import com.android.wallpaper.model.WallpaperSectionController; @@ -25,17 +27,26 @@ import java.util.stream.Collectors; /** The Fragment UI for wallpaper only section. */ public class WallpaperOnlyFragment extends CustomizationPickerFragment { + /** Returns a new instance of {@link WallpaperOnlyFragment}. */ + public static WallpaperOnlyFragment newInstance(boolean isUseRevampedUi) { + final WallpaperOnlyFragment fragment = new WallpaperOnlyFragment(); + final Bundle args = new Bundle(); + args.putBoolean(KEY_IS_USE_REVAMPED_UI, isUseRevampedUi); + fragment.setArguments(args); + return fragment; + } + @Override public CharSequence getDefaultTitle() { return getString(R.string.wallpaper_app_name); } @Override - protected List<CustomizationSectionController<?>> getAvailableSections( + protected List<CustomizationSectionController<?>> filterAvailableSections( List<CustomizationSectionController<?>> controllers) { List<CustomizationSectionController<?>> wallpaperOnlySections = controllers.stream() .filter(controller -> controller instanceof WallpaperSectionController) .collect(Collectors.toList()); - return super.getAvailableSections(wallpaperOnlySections); + return super.filterAvailableSections(wallpaperOnlySections); } } diff --git a/src/com/android/wallpaper/picker/WallpaperPickerDelegate.java b/src/com/android/wallpaper/picker/WallpaperPickerDelegate.java index 5b60c064..3c788ef4 100644 --- a/src/com/android/wallpaper/picker/WallpaperPickerDelegate.java +++ b/src/com/android/wallpaper/picker/WallpaperPickerDelegate.java @@ -467,7 +467,6 @@ public class WallpaperPickerDelegate implements MyPhotosStarter { PREVIEW_WALLPAPER_REQUEST_CODE); return false; case PREVIEW_LIVE_WALLPAPER_REQUEST_CODE: - mWallpaperPersister.onLiveWallpaperSet(); populateCategories(/* forceRefresh= */ true); return true; case VIEW_ONLY_PREVIEW_WALLPAPER_REQUEST_CODE: @@ -475,7 +474,6 @@ public class WallpaperPickerDelegate implements MyPhotosStarter { case PREVIEW_WALLPAPER_REQUEST_CODE: // User previewed and selected a wallpaper, so finish this activity. if (data != null && data.getBooleanExtra(IS_LIVE_WALLPAPER, false)) { - mWallpaperPersister.onLiveWallpaperSet(); populateCategories(/* forceRefresh= */ true); } return true; diff --git a/src/com/android/wallpaper/picker/WorkspaceSurfaceHolderCallback.java b/src/com/android/wallpaper/picker/WorkspaceSurfaceHolderCallback.java index bb4c0d36..9c26cc3d 100644 --- a/src/com/android/wallpaper/picker/WorkspaceSurfaceHolderCallback.java +++ b/src/com/android/wallpaper/picker/WorkspaceSurfaceHolderCallback.java @@ -46,12 +46,15 @@ public class WorkspaceSurfaceHolderCallback implements SurfaceHolder.Callback { private static final String TAG = "WsSurfaceHolderCallback"; private static final String KEY_WALLPAPER_COLORS = "wallpaper_colors"; + public static final int MESSAGE_ID_UPDATE_PREVIEW = 1337; + public static final String KEY_HIDE_BOTTOM_ROW = "hide_bottom_row"; private final SurfaceView mWorkspaceSurface; private final PreviewUtils mPreviewUtils; private final boolean mShouldUseWallpaperColors; private final AtomicBoolean mRequestPending = new AtomicBoolean(false); private WallpaperColors mWallpaperColors; + private boolean mHideBottomRow; private boolean mIsWallpaperColorsReady; private Surface mLastSurface; private Message mCallback; @@ -126,27 +129,45 @@ public class WorkspaceSurfaceHolderCallback implements SurfaceHolder.Callback { } mWallpaperColors = colors; mIsWallpaperColorsReady = true; - maybeRenderPreview(); + } + + /** + * Set the current flag if we should hide the workspace bottom row. + */ + public void setHideBottomRow(boolean hideBottomRow) { + mHideBottomRow = hideBottomRow; + } + + /** + * Hides the components in the bottom row. + * + * @param hide True to hide and false to show. + */ + public void hideBottomRow(boolean hide) { + Bundle data = new Bundle(); + data.putBoolean(KEY_HIDE_BOTTOM_ROW, hide); + send(MESSAGE_ID_UPDATE_PREVIEW, data); } public void setListener(WorkspaceRenderListener listener) { mListener = listener; } - private void maybeRenderPreview() { + /** + * Render the preview with the current selected {@link #mWallpaperColors} and + * {@link #mHideBottomRow}. + */ + public void maybeRenderPreview() { if ((mShouldUseWallpaperColors && !mIsWallpaperColorsReady) || mLastSurface == null) { return; } - mRequestPending.set(true); requestPreview(mWorkspaceSurface, (result) -> { mRequestPending.set(false); if (result != null && mLastSurface != null) { mWorkspaceSurface.setChildSurfacePackage( SurfaceViewUtils.getSurfacePackage(result)); - mCallback = SurfaceViewUtils.getCallback(result); - if (mNeedsToCleanUp) { cleanUp(); } else if (mListener != null) { @@ -216,6 +237,7 @@ public class WorkspaceSurfaceHolderCallback implements SurfaceHolder.Callback { Bundle request = SurfaceViewUtils.createSurfaceViewRequest(workspaceSurface, mExtras); if (mWallpaperColors != null) { request.putParcelable(KEY_WALLPAPER_COLORS, mWallpaperColors); + request.putBoolean(KEY_HIDE_BOTTOM_ROW, mHideBottomRow); } mPreviewUtils.renderPreview(request, callback); } diff --git a/src/com/android/wallpaper/picker/common/button/ui/viewbinder/ButtonViewBinder.kt b/src/com/android/wallpaper/picker/common/button/ui/viewbinder/ButtonViewBinder.kt new file mode 100644 index 00000000..6e63835b --- /dev/null +++ b/src/com/android/wallpaper/picker/common/button/ui/viewbinder/ButtonViewBinder.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.common.button.ui.viewbinder + +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.LayoutRes +import com.android.wallpaper.R +import com.android.wallpaper.picker.common.button.ui.viewmodel.ButtonViewModel +import com.android.wallpaper.picker.common.text.ui.viewbinder.TextViewBinder + +object ButtonViewBinder { + /** Returns a newly-created [View] that's already bound to the given [ButtonViewModel]. */ + fun create( + parent: ViewGroup, + viewModel: ButtonViewModel, + @LayoutRes buttonLayoutResourceId: Int = R.layout.dialog_button, + ): View { + val view = + LayoutInflater.from( + ContextThemeWrapper( + parent.context, + viewModel.style.styleResourceId, + ) + ) + .inflate( + buttonLayoutResourceId, + parent, + false, + ) + val text: TextView = view.requireViewById(R.id.text) + view.setOnClickListener { viewModel.onClicked?.invoke() } + + TextViewBinder.bind( + view = text, + viewModel = viewModel.text, + ) + + return view + } +} diff --git a/src/com/android/wallpaper/model/WorkspaceViewModel.kt b/src/com/android/wallpaper/picker/common/button/ui/viewmodel/ButtonStyle.kt index 627406ee..a83a8ab4 100644 --- a/src/com/android/wallpaper/model/WorkspaceViewModel.kt +++ b/src/com/android/wallpaper/picker/common/button/ui/viewmodel/ButtonStyle.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,18 +12,23 @@ * 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.wallpaper.model -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel +package com.android.wallpaper.picker.common.button.ui.viewmodel -/** ViewModel class to keep track of workspace updates. */ -class WorkspaceViewModel : ViewModel() { +import androidx.annotation.StyleRes +import com.android.wallpaper.R - /** - * Triggers workspace updates through flipping the value from {@code false} to {@code true}, or - * from {@code true} to {@code false}. - */ - val updateWorkspace: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() } +sealed class ButtonStyle( + @StyleRes open val styleResourceId: Int, +) { + object Primary : + ButtonStyle( + styleResourceId = R.style.DialogButton_Primary, + ) + object Secondary : + ButtonStyle( + styleResourceId = R.style.DialogButton_Secondary, + ) } diff --git a/src/com/android/wallpaper/picker/common/button/ui/viewmodel/ButtonViewModel.kt b/src/com/android/wallpaper/picker/common/button/ui/viewmodel/ButtonViewModel.kt new file mode 100644 index 00000000..62f3e81d --- /dev/null +++ b/src/com/android/wallpaper/picker/common/button/ui/viewmodel/ButtonViewModel.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.common.button.ui.viewmodel + +import com.android.wallpaper.picker.common.text.ui.viewmodel.Text + +data class ButtonViewModel( + val text: Text, + val style: ButtonStyle, + val onClicked: (() -> Unit)? = null, +) diff --git a/src/com/android/wallpaper/picker/common/dialog/ui/viewbinder/DialogViewBinder.kt b/src/com/android/wallpaper/picker/common/dialog/ui/viewbinder/DialogViewBinder.kt new file mode 100644 index 00000000..7130f21a --- /dev/null +++ b/src/com/android/wallpaper/picker/common/dialog/ui/viewbinder/DialogViewBinder.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.common.dialog.ui.viewbinder + +import android.app.Dialog +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.android.wallpaper.R +import com.android.wallpaper.picker.common.button.ui.viewbinder.ButtonViewBinder +import com.android.wallpaper.picker.common.dialog.ui.viewmodel.DialogViewModel +import com.android.wallpaper.picker.common.icon.ui.viewbinder.IconViewBinder +import com.android.wallpaper.picker.common.text.ui.viewbinder.TextViewBinder + +object DialogViewBinder { + /** Returns a shown dialog that's bound to the given [DialogViewModel]. */ + fun show( + context: Context, + viewModel: DialogViewModel, + onDismissed: (() -> Unit)? = null, + @LayoutRes dialogLayoutResourceId: Int = R.layout.dialog_view, + @LayoutRes buttonLayoutResourceId: Int = R.layout.dialog_button, + ): Dialog { + val view = LayoutInflater.from(context).inflate(dialogLayoutResourceId, null) + val icon: ImageView = view.requireViewById(R.id.icon) + val title: TextView = view.requireViewById(R.id.title) + val message: TextView = view.requireViewById(R.id.message) + val buttonContainer: ViewGroup = view.requireViewById(R.id.button_container) + + viewModel.icon?.let { + IconViewBinder.bind( + view = icon, + viewModel = it, + ) + icon.isVisible = true + } + ?: run { icon.isVisible = false } + + viewModel.title?.let { + TextViewBinder.bind( + view = title, + viewModel = it, + ) + title.isVisible = true + } + ?: run { title.isVisible = false } + + viewModel.message?.let { + TextViewBinder.bind( + view = message, + viewModel = it, + ) + message.isVisible = true + } + ?: run { message.isVisible = false } + + val dialog = + AlertDialog.Builder(context, R.style.LightDialogTheme) + .setView(view) + .apply { + if (viewModel.onDismissed != null || onDismissed != null) { + setOnDismissListener { + onDismissed?.invoke() + viewModel.onDismissed?.invoke() + } + } + } + .create() + + buttonContainer.removeAllViews() + viewModel.buttons.forEach { buttonViewModel -> + buttonContainer.addView( + ButtonViewBinder.create( + parent = buttonContainer, + viewModel = + buttonViewModel.copy( + onClicked = { + buttonViewModel.onClicked?.invoke() + dialog.dismiss() + }, + ), + buttonLayoutResourceId = buttonLayoutResourceId, + ) + ) + } + + dialog.show() + return dialog + } +} diff --git a/src/com/android/wallpaper/picker/common/dialog/ui/viewmodel/DialogViewModel.kt b/src/com/android/wallpaper/picker/common/dialog/ui/viewmodel/DialogViewModel.kt new file mode 100644 index 00000000..a34bf33d --- /dev/null +++ b/src/com/android/wallpaper/picker/common/dialog/ui/viewmodel/DialogViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.common.dialog.ui.viewmodel + +import com.android.wallpaper.picker.common.button.ui.viewmodel.ButtonViewModel +import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon +import com.android.wallpaper.picker.common.text.ui.viewmodel.Text + +data class DialogViewModel( + val icon: Icon? = null, + val title: Text? = null, + val message: Text? = null, + val buttons: List<ButtonViewModel> = emptyList(), + val onDismissed: (() -> Unit)? = null, +) diff --git a/src/com/android/wallpaper/picker/common/icon/ui/viewbinder/ContentDescriptionViewBinder.kt b/src/com/android/wallpaper/picker/common/icon/ui/viewbinder/ContentDescriptionViewBinder.kt new file mode 100644 index 00000000..e31852b5 --- /dev/null +++ b/src/com/android/wallpaper/picker/common/icon/ui/viewbinder/ContentDescriptionViewBinder.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.common.icon.ui.viewbinder + +import android.view.View +import com.android.wallpaper.picker.common.text.ui.viewmodel.Text + +object ContentDescriptionViewBinder { + fun bind( + view: View, + viewModel: Text, + ) { + view.contentDescription = + when (viewModel) { + is Text.Resource -> view.context.getString(viewModel.res) + is Text.Loaded -> viewModel.text + } + } +} diff --git a/src/com/android/wallpaper/picker/common/icon/ui/viewbinder/IconViewBinder.kt b/src/com/android/wallpaper/picker/common/icon/ui/viewbinder/IconViewBinder.kt new file mode 100644 index 00000000..79ec5682 --- /dev/null +++ b/src/com/android/wallpaper/picker/common/icon/ui/viewbinder/IconViewBinder.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.common.icon.ui.viewbinder + +import android.widget.ImageView +import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon +import com.android.wallpaper.picker.common.text.ui.viewmodel.Text + +object IconViewBinder { + fun bind( + view: ImageView, + viewModel: Icon, + ) { + when (viewModel) { + is Icon.Resource -> view.setImageResource(viewModel.res) + is Icon.Loaded -> view.setImageDrawable(viewModel.drawable) + } + + view.contentDescription = + when (viewModel.contentDescription) { + is Text.Resource -> + view.context.getString((viewModel.contentDescription as Text.Resource).res) + is Text.Loaded -> (viewModel.contentDescription as Text.Loaded).text + null -> null + } + } +} diff --git a/src/com/android/wallpaper/picker/common/icon/ui/viewmodel/Icon.kt b/src/com/android/wallpaper/picker/common/icon/ui/viewmodel/Icon.kt new file mode 100644 index 00000000..aa42e46a --- /dev/null +++ b/src/com/android/wallpaper/picker/common/icon/ui/viewmodel/Icon.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.common.icon.ui.viewmodel + +import android.graphics.drawable.Drawable +import androidx.annotation.DrawableRes +import com.android.wallpaper.picker.common.text.ui.viewmodel.Text + +sealed class Icon( + open val contentDescription: Text?, +) { + data class Resource( + @DrawableRes val res: Int, + override val contentDescription: Text?, + ) : + Icon( + contentDescription = contentDescription, + ) + + data class Loaded( + val drawable: Drawable, + override val contentDescription: Text?, + ) : + Icon( + contentDescription = contentDescription, + ) +} diff --git a/src/com/android/wallpaper/picker/common/text/ui/viewbinder/TextViewBinder.kt b/src/com/android/wallpaper/picker/common/text/ui/viewbinder/TextViewBinder.kt new file mode 100644 index 00000000..2b58bbef --- /dev/null +++ b/src/com/android/wallpaper/picker/common/text/ui/viewbinder/TextViewBinder.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.common.text.ui.viewbinder + +import android.widget.TextView +import com.android.wallpaper.picker.common.text.ui.viewmodel.Text + +object TextViewBinder { + fun bind( + view: TextView, + viewModel: Text, + ) { + when (viewModel) { + is Text.Resource -> view.setText(viewModel.res) + is Text.Loaded -> view.text = viewModel.text + } + } +} diff --git a/src/com/android/wallpaper/picker/common/text/ui/viewmodel/Text.kt b/src/com/android/wallpaper/picker/common/text/ui/viewmodel/Text.kt new file mode 100644 index 00000000..fc423e3e --- /dev/null +++ b/src/com/android/wallpaper/picker/common/text/ui/viewmodel/Text.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.common.text.ui.viewmodel + +import android.content.Context +import androidx.annotation.StringRes + +sealed class Text { + data class Resource( + @StringRes val res: Int, + ) : Text() + + data class Loaded( + val text: String, + ) : Text() + + fun asString(context: Context): String { + return when (this) { + is Resource -> context.getString(res) + is Loaded -> text + } + } + + companion object { + /** + * Returns `true` if the given [Text] instances evaluate to the values; `false` otherwise. + */ + fun evaluationEquals( + context: Context, + first: Text?, + second: Text?, + ): Boolean { + return first?.asString(context) == second?.asString(context) + } + } +} diff --git a/src/com/android/wallpaper/picker/customization/data/content/WallpaperClient.kt b/src/com/android/wallpaper/picker/customization/data/content/WallpaperClient.kt new file mode 100644 index 00000000..e849426b --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/data/content/WallpaperClient.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.data.content + +import android.graphics.Bitmap +import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination +import com.android.wallpaper.picker.customization.shared.model.WallpaperModel +import kotlinx.coroutines.flow.Flow + +/** Defines interface for classes that can interact with the Wallpaper API. */ +interface WallpaperClient { + + /** Lists the most recent wallpapers. The first one is the most recent (current) wallpaper. */ + fun recentWallpapers( + destination: WallpaperDestination, + limit: Int, + ): Flow<List<WallpaperModel>> + + /** Returns the selected wallpaper. */ + suspend fun getCurrentWallpaper( + destination: WallpaperDestination, + ): WallpaperModel + + /** + * Asynchronously sets the wallpaper to the one with the given ID. + * + * @param destination The screen to set the wallpaper on. + * @param wallpaperId The ID of the wallpaper to set. + * @param onDone A callback to invoke when setting is done. + */ + suspend fun setWallpaper( + destination: WallpaperDestination, + wallpaperId: String, + onDone: () -> Unit + ) + + /** Returns a thumbnail for the wallpaper with the given ID. */ + suspend fun loadThumbnail(wallpaperId: String): Bitmap? +} diff --git a/src/com/android/wallpaper/picker/customization/data/content/WallpaperClientImpl.kt b/src/com/android/wallpaper/picker/customization/data/content/WallpaperClientImpl.kt new file mode 100644 index 00000000..e85cdfd8 --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/data/content/WallpaperClientImpl.kt @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.data.content + +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.database.ContentObserver +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Log +import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination +import com.android.wallpaper.picker.customization.shared.model.WallpaperModel +import java.io.IOException +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch + +class WallpaperClientImpl( + private val context: Context, +) : WallpaperClient { + + override fun recentWallpapers( + destination: WallpaperDestination, + limit: Int, + ): Flow<List<WallpaperModel>> { + return callbackFlow { + suspend fun queryAndSend(limit: Int) { + send(queryRecentWallpapers(destination = destination, limit = limit)) + } + + val contentObserver = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + launch { queryAndSend(limit = limit) } + } + } + + context.contentResolver.registerContentObserver( + LIST_RECENTS_URI, + /* notifyForDescendants= */ true, + contentObserver, + ) + queryAndSend(limit = limit) + + awaitClose { context.contentResolver.unregisterContentObserver(contentObserver) } + } + } + + override suspend fun getCurrentWallpaper( + destination: WallpaperDestination, + ): WallpaperModel { + return queryRecentWallpapers(destination = destination, limit = 1).first() + } + + override suspend fun setWallpaper( + destination: WallpaperDestination, + wallpaperId: String, + onDone: () -> Unit + ) { + val updateValues = ContentValues() + updateValues.put(KEY_ID, wallpaperId) + updateValues.put(KEY_SCREEN, destination.asString()) + val updatedRowCount = context.contentResolver.update(SET_WALLPAPER_URI, updateValues, null) + if (updatedRowCount == 0) { + Log.e(TAG, "Error setting wallpaper: $wallpaperId") + } + onDone.invoke() + } + + private suspend fun queryRecentWallpapers( + destination: WallpaperDestination, + limit: Int, + ): List<WallpaperModel> { + context.contentResolver + .query( + LIST_RECENTS_URI.buildUpon().appendPath(destination.asString()).build(), + arrayOf( + KEY_ID, + KEY_PLACEHOLDER_COLOR, + ), + null, + null, + ) + .use { cursor -> + if (cursor == null || cursor.count == 0) { + return emptyList() + } + + return buildList { + val idColumnIndex = cursor.getColumnIndex(KEY_ID) + val placeholderColorColumnIndex = cursor.getColumnIndex(KEY_PLACEHOLDER_COLOR) + while (cursor.moveToNext() && size < limit) { + val wallpaperId = cursor.getString(idColumnIndex) + val placeholderColor = cursor.getInt(placeholderColorColumnIndex) + add( + WallpaperModel( + wallpaperId = wallpaperId, + placeholderColor = placeholderColor, + ) + ) + } + } + } + } + + override suspend fun loadThumbnail( + wallpaperId: String, + ): Bitmap? { + try { + // We're already using this in a suspend function, so we're okay. + @Suppress("BlockingMethodInNonBlockingContext") + context.contentResolver + .openFile( + GET_THUMBNAIL_BASE_URI.buildUpon().appendPath(wallpaperId).build(), + "r", + null, + ) + .use { file -> + if (file == null) { + Log.e(TAG, "Error getting wallpaper preview: $wallpaperId") + } else { + return BitmapFactory.decodeFileDescriptor(file.fileDescriptor) + } + } + } catch (e: IOException) { + Log.e(TAG, "Error getting wallpaper preview: $wallpaperId", e) + } + + return null + } + + private fun WallpaperDestination.asString(): String { + return when (this) { + WallpaperDestination.BOTH -> SCREEN_ALL + WallpaperDestination.HOME -> SCREEN_HOME + WallpaperDestination.LOCK -> SCREEN_LOCK + } + } + + companion object { + private const val TAG = "WallpaperClientImpl" + private const val AUTHORITY = "com.google.android.apps.wallpaper.recents" + + /** Path for making a content provider request to set the wallpaper. */ + private const val PATH_SET_WALLPAPER = "set_recent_wallpaper" + /** Path for making a content provider request to query for the recent wallpapers. */ + private const val PATH_LIST_RECENTS = "list_recent" + /** Path for making a content provider request to query for the thumbnail of a wallpaper. */ + private const val PATH_GET_THUMBNAIL = "thumb" + + private val BASE_URI = + Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build() + /** [Uri] for making a content provider request to set the wallpaper. */ + private val SET_WALLPAPER_URI = BASE_URI.buildUpon().appendPath(PATH_SET_WALLPAPER).build() + /** [Uri] for making a content provider request to query for the recent wallpapers. */ + private val LIST_RECENTS_URI = BASE_URI.buildUpon().appendPath(PATH_LIST_RECENTS).build() + /** + * [Uri] for making a content provider request to query for the thumbnail of a wallpaper. + */ + private val GET_THUMBNAIL_BASE_URI = + BASE_URI.buildUpon().appendPath(PATH_GET_THUMBNAIL).build() + + /** Key for a parameter used to pass the wallpaper ID to/from the content provider. */ + private const val KEY_ID = "id" + /** Key for a parameter used to pass the screen to/from the content provider. */ + private const val KEY_SCREEN = "screen" + private const val SCREEN_ALL = "all_screens" + private const val SCREEN_HOME = "home_screen" + private const val SCREEN_LOCK = "lock_screen" + /** + * Key for a parameter used to get the placeholder color for a wallpaper from the content + * provider. + */ + private const val KEY_PLACEHOLDER_COLOR = "placeholder_color" + } +} diff --git a/src/com/android/wallpaper/picker/customization/data/repository/WallpaperRepository.kt b/src/com/android/wallpaper/picker/customization/data/repository/WallpaperRepository.kt new file mode 100644 index 00000000..6234fa53 --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/data/repository/WallpaperRepository.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.data.repository + +import android.graphics.Bitmap +import com.android.wallpaper.picker.customization.data.content.WallpaperClient +import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination +import com.android.wallpaper.picker.customization.shared.model.WallpaperModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +/** Encapsulates access to wallpaper-related data. */ +class WallpaperRepository( + private val scope: CoroutineScope, + private val client: WallpaperClient, + private val backgroundDispatcher: CoroutineDispatcher, +) { + /** The ID of the currently-selected wallpaper. */ + fun selectedWallpaperId( + destination: WallpaperDestination, + ): StateFlow<String> { + return client + .recentWallpapers(destination = destination, limit = 1) + .map { previews -> previews.first().wallpaperId } + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(), + initialValue = + runBlocking { + client.getCurrentWallpaper(destination = destination).wallpaperId + }, + ) + } + + private val _selectingWallpaperId = + MutableStateFlow<Map<WallpaperDestination, String?>>(emptyMap()) + /** + * The ID of the wallpaper that is in the process of becoming the selected wallpaper or `null` + * if no such transaction is currently taking place. + */ + val selectingWallpaperId: StateFlow<Map<WallpaperDestination, String?>> = + _selectingWallpaperId.asStateFlow() + + /** Lists the most recent wallpapers. The first one is the most recent (current) wallpaper. */ + fun recentWallpapers( + destination: WallpaperDestination, + limit: Int, + ): Flow<List<WallpaperModel>> { + return client + .recentWallpapers(destination = destination, limit = limit) + .flowOn(backgroundDispatcher) + } + + /** Returns a thumbnail for the wallpaper with the given ID. */ + suspend fun loadThumbnail(wallpaperId: String): Bitmap? { + return withContext(backgroundDispatcher) { client.loadThumbnail(wallpaperId) } + } + + /** Sets the wallpaper to the one with the given ID. */ + suspend fun setWallpaper( + destination: WallpaperDestination, + wallpaperId: String, + ) { + _selectingWallpaperId.value = + _selectingWallpaperId.value.toMutableMap().apply { this[destination] = wallpaperId } + withContext(backgroundDispatcher) { + client.setWallpaper( + destination = destination, + wallpaperId = wallpaperId, + ) { + _selectingWallpaperId.value = + _selectingWallpaperId.value.toMutableMap().apply { this[destination] = null } + } + } + } +} diff --git a/src/com/android/wallpaper/picker/customization/domain/interactor/WallpaperInteractor.kt b/src/com/android/wallpaper/picker/customization/domain/interactor/WallpaperInteractor.kt new file mode 100644 index 00000000..d9e2ef64 --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/domain/interactor/WallpaperInteractor.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.domain.interactor + +import android.graphics.Bitmap +import com.android.wallpaper.module.CustomizationSections +import com.android.wallpaper.picker.customization.data.repository.WallpaperRepository +import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination +import com.android.wallpaper.picker.customization.shared.model.WallpaperModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map + +/** Handles business logic for wallpaper-related use-cases. */ +class WallpaperInteractor( + private val repository: WallpaperRepository, + /** Returns whether wallpaper picker should handle reload */ + val shouldHandleReload: () -> Boolean = { true }, +) { + /** Returns a flow that is updated whenever the wallpaper has been updated */ + fun wallpaperUpdateEvents(screen: CustomizationSections.Screen): Flow<WallpaperModel> { + return when (screen) { + CustomizationSections.Screen.LOCK_SCREEN -> + previews(WallpaperDestination.LOCK, 1).map { recentWallpapers -> + recentWallpapers[0] + } + CustomizationSections.Screen.HOME_SCREEN -> + previews(WallpaperDestination.HOME, 1).map { recentWallpapers -> + recentWallpapers[0] + } + } + } + + /** Returns the ID of the currently-selected wallpaper. */ + fun selectedWallpaperId( + destination: WallpaperDestination, + ): StateFlow<String> { + return repository.selectedWallpaperId(destination = destination) + } + + /** + * Returns the ID of the wallpaper that is in the process of becoming the selected wallpaper or + * `null` if no such transaction is currently taking place. + */ + fun selectingWallpaperId( + destination: WallpaperDestination, + ): Flow<String?> { + return repository.selectingWallpaperId.map { it[destination] } + } + + /** + * Lists the [maxResults] most recent wallpapers. + * + * The first one is the most recent (current) wallpaper. + */ + fun previews( + destination: WallpaperDestination, + maxResults: Int, + ): Flow<List<WallpaperModel>> { + return repository + .recentWallpapers( + destination = destination, + limit = maxResults, + ) + .map { previews -> + if (previews.size > maxResults) { + previews.subList(0, maxResults) + } else { + previews + } + } + } + + /** Sets the wallpaper to the one with the given ID. */ + suspend fun setWallpaper( + destination: WallpaperDestination, + wallpaperId: String, + ) { + repository.setWallpaper( + destination = destination, + wallpaperId = wallpaperId, + ) + } + + /** Returns a thumbnail for the wallpaper with the given ID. */ + suspend fun loadThumbnail(wallpaperId: String): Bitmap? { + return repository.loadThumbnail( + wallpaperId = wallpaperId, + ) + } +} diff --git a/src/com/android/wallpaper/picker/customization/domain/interactor/WallpaperSnapshotRestorer.kt b/src/com/android/wallpaper/picker/customization/domain/interactor/WallpaperSnapshotRestorer.kt new file mode 100644 index 00000000..7c23cfed --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/domain/interactor/WallpaperSnapshotRestorer.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.domain.interactor + +import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination +import com.android.wallpaper.picker.undo.domain.interactor.SnapshotRestorer +import com.android.wallpaper.picker.undo.domain.interactor.SnapshotStore +import com.android.wallpaper.picker.undo.shared.model.RestorableSnapshot +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.launch + +/** Stores and restores undo snapshots for wallpaper state. */ +class WallpaperSnapshotRestorer( + private val scope: CoroutineScope, + private val interactor: WallpaperInteractor, +) : SnapshotRestorer { + + private var store: SnapshotStore = SnapshotStore.NOOP + + override suspend fun setUpSnapshotRestorer( + store: SnapshotStore, + ): RestorableSnapshot { + this.store = store + startObserving() + return snapshot() + } + + override suspend fun restoreToSnapshot( + snapshot: RestorableSnapshot, + ) { + val homeWallpaperId = snapshot.args[SELECTED_HOME_SCREEN_WALLPAPER_ID] + if (!homeWallpaperId.isNullOrEmpty()) { + interactor.setWallpaper( + destination = WallpaperDestination.HOME, + wallpaperId = homeWallpaperId + ) + } + + val lockWallpaperId = snapshot.args[SELECTED_LOCK_SCREEN_WALLPAPER_ID] + if (!lockWallpaperId.isNullOrEmpty()) { + interactor.setWallpaper( + destination = WallpaperDestination.LOCK, + wallpaperId = lockWallpaperId + ) + } + } + + private fun startObserving() { + scope.launch { + combine( + interactor.selectedWallpaperId(destination = WallpaperDestination.HOME), + interactor.selectedWallpaperId(destination = WallpaperDestination.LOCK), + ::Pair, + ) + .drop(1) // We skip the first value because it's the same as the initial. + .collect { (homeWallpaperId, lockWallpaperId) -> + store.store( + snapshot( + homeWallpaperId, + lockWallpaperId, + ) + ) + } + } + } + + private fun snapshot( + homeWallpaperId: String = querySelectedWallpaperId(destination = WallpaperDestination.HOME), + lockWallpaperId: String = querySelectedWallpaperId(destination = WallpaperDestination.LOCK), + ): RestorableSnapshot { + return RestorableSnapshot( + args = + buildMap { + put( + SELECTED_HOME_SCREEN_WALLPAPER_ID, + homeWallpaperId, + ) + put( + SELECTED_LOCK_SCREEN_WALLPAPER_ID, + lockWallpaperId, + ) + } + ) + } + + private fun querySelectedWallpaperId(destination: WallpaperDestination): String { + return interactor.selectedWallpaperId(destination = destination).value + } + + companion object { + private const val SELECTED_HOME_SCREEN_WALLPAPER_ID = "selected_home_screen_wallpaper_id" + private const val SELECTED_LOCK_SCREEN_WALLPAPER_ID = "selected_lock_screen_wallpaper_id" + } +} diff --git a/src/com/android/wallpaper/picker/customization/shared/model/WallpaperDestination.kt b/src/com/android/wallpaper/picker/customization/shared/model/WallpaperDestination.kt new file mode 100644 index 00000000..89d5af4e --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/shared/model/WallpaperDestination.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.shared.model + +/** Enumerates all known wallpaper destinations. */ +enum class WallpaperDestination { + /** Both [HOME] and [LOCK] destinations. */ + BOTH, + /** The home screen wallpaper. */ + HOME, + /** The lock screen wallpaper. */ + LOCK, +} diff --git a/src/com/android/wallpaper/picker/undo/ui/viewmodel/UndoDialogViewModel.kt b/src/com/android/wallpaper/picker/customization/shared/model/WallpaperModel.kt index b7f754b5..a0d8b38d 100644 --- a/src/com/android/wallpaper/picker/undo/ui/viewmodel/UndoDialogViewModel.kt +++ b/src/com/android/wallpaper/picker/customization/shared/model/WallpaperModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,10 @@ * */ -package com.android.wallpaper.picker.undo.ui.viewmodel +package com.android.wallpaper.picker.customization.shared.model -/** Models the UI state for an undo confirmation dialog. */ -data class UndoDialogViewModel( - val onConfirmed: () -> Unit, - val onDismissed: () -> Unit, +/** Models a single wallpaper preview. */ +data class WallpaperModel( + val wallpaperId: String, + val placeholderColor: Int, ) diff --git a/src/com/android/wallpaper/picker/customization/ui/binder/CustomizationPickerBinder.kt b/src/com/android/wallpaper/picker/customization/ui/binder/CustomizationPickerBinder.kt index 53c8375b..be036bb4 100644 --- a/src/com/android/wallpaper/picker/customization/ui/binder/CustomizationPickerBinder.kt +++ b/src/com/android/wallpaper/picker/customization/ui/binder/CustomizationPickerBinder.kt @@ -30,17 +30,30 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.wallpaper.R import com.android.wallpaper.model.CustomizationSectionController -import com.android.wallpaper.model.WallpaperSectionController import com.android.wallpaper.picker.SectionView -import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewSectionController import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel import com.android.wallpaper.picker.undo.ui.binder.RevertToolbarButtonBinder +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.launch typealias SectionController = CustomizationSectionController<*> /** Binds view to view-model for the customization picker. */ object CustomizationPickerBinder { + + /** + * Binds the given view and view-model, keeping the UI up-to-date and listening to user input. + * + * @param view The root of the UI to keep up-to-date and observe for user input. + * @param toolbarViewId The view ID of the toolbar view. + * @param viewModel The view-model to observe UI state from and report user input to. + * @param lifecycleOwner An owner of the lifecycle, so we can stop doing work when the lifecycle + * cleans up. + * @param sectionControllerProvider A function that can provide the list of [SectionController] + * instances to show, based on the given passed-in value of "isOnLockScreen". + * @return A [DisposableHandle] to use to dispose of the binding before another binding is about + * to be created by a subsequent call to this function. + */ @JvmStatic fun bind( view: View, @@ -48,7 +61,7 @@ object CustomizationPickerBinder { viewModel: CustomizationPickerViewModel, lifecycleOwner: LifecycleOwner, sectionControllerProvider: (isOnLockScreen: Boolean) -> List<SectionController>, - ) { + ): DisposableHandle { RevertToolbarButtonBinder.bind( view = view.requireViewById(toolbarViewId), viewModel = viewModel.undo, @@ -77,68 +90,82 @@ object CustomizationPickerBinder { topMargin = 0 } - lifecycleOwner.lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.isOnLockScreen.collect { isOnLockScreen -> - // These are the available section controllers we should use now. - val newSectionControllers = - sectionControllerProvider.invoke(isOnLockScreen).filter { - it.isAvailable(view.context) - } + val job = + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.isOnLockScreen.collect { isOnLockScreen -> + // These are the available section controllers we should use now. + val newSectionControllers = + sectionControllerProvider.invoke(isOnLockScreen).filter { + it.isAvailable(view.context) + } - check( - newSectionControllers[0] is WallpaperSectionController || - newSectionControllers[0] is ScreenPreviewSectionController - ) { - "The first section must always be the preview or the assumption below" + - " must be updated." - } + check( + newSectionControllers[0].shouldRetainInstanceWhenSwitchingTabs() + ) { + "We are not recreating the first section when the users switching" + + " between the home screen and lock screen tab. The first" + + " section should always retain." + } - val firstTime = sectionContainer.childCount == 0 - if (!firstTime) { - // Remove all views, except the very first one, which we assume is for - // the wallpaper preview section. - sectionContainer.removeViews(1, sectionContainer.childCount - 1) + val firstTime = sectionContainer.childCount == 0 + if (!firstTime) { + // Remove all views, except the very first one, which we assume is + // for + // the wallpaper preview section. + sectionContainer.removeViews(1, sectionContainer.childCount - 1) - // The old controllers for the removed views should be released, except - // for the very first one, which is for the wallpaper preview section; - // that one we keep but just tell it that we switched screens. - sectionContainer.children - .mapNotNull { it.tag as? SectionController } - .forEachIndexed { index, oldController -> - if (index == 0) { - // We assume that index 0 is the wallpaper preview section. - // We keep it because it's an expensive section (as it needs - // to maintain a wallpaper connection that seems to be - // making assumptions about its SurfaceView always remaining - // attached to the window). - oldController.onScreenSwitched(isOnLockScreen) - } else { - // All other old controllers will be thrown out so let's - // release them. - oldController.release() + // The old controllers for the removed views should be released, + // except + // for the very first one, which is for the wallpaper preview + // section; + // that one we keep but just tell it that we switched screens. + sectionContainer.children + .mapNotNull { it.tag as? SectionController } + .forEachIndexed { index, oldController -> + if (index == 0) { + // We assume that index 0 is the wallpaper preview + // section. + // We keep it because it's an expensive section (as it + // needs + // to maintain a wallpaper connection that seems to be + // making assumptions about its SurfaceView always + // remaining + // attached to the window). + oldController.onScreenSwitched(isOnLockScreen) + } else { + // All other old controllers will be thrown out so let's + // release them. + oldController.release() + } } - } - } + } - // Let's add the new controllers and views. - newSectionControllers.forEachIndexed { index, controller -> - if (firstTime || index > 0) { - val addedView = controller.createView(view.context, isOnLockScreen) - addedView.tag = controller - sectionContainer.addView(addedView) + // Let's add the new controllers and views. + newSectionControllers.forEachIndexed { index, controller -> + if (firstTime || index > 0) { + val addedView = + controller.createView( + view.context, + CustomizationSectionController.ViewCreationParams( + isOnLockScreen = isOnLockScreen, + ) + ) + addedView?.tag = controller + sectionContainer.addView(addedView) + } } } } } - } - // This happens when the lifecycle is stopped. - sectionContainer.children - .mapNotNull { it.tag as? CustomizationSectionController<out SectionView> } - .forEach { controller -> controller.release() } - sectionContainer.removeAllViews() - } + // This happens when the lifecycle is stopped. + sectionContainer.children + .mapNotNull { it.tag as? CustomizationSectionController<out SectionView> } + .forEach { controller -> controller.release() } + sectionContainer.removeAllViews() + } + return DisposableHandle { job.cancel() } } } diff --git a/src/com/android/wallpaper/picker/customization/ui/binder/ScreenPreviewBinder.kt b/src/com/android/wallpaper/picker/customization/ui/binder/ScreenPreviewBinder.kt index 9df609eb..a703b9a0 100644 --- a/src/com/android/wallpaper/picker/customization/ui/binder/ScreenPreviewBinder.kt +++ b/src/com/android/wallpaper/picker/customization/ui/binder/ScreenPreviewBinder.kt @@ -23,18 +23,22 @@ import android.content.Intent import android.os.Bundle import android.service.wallpaper.WallpaperService import android.view.SurfaceView +import android.view.View +import android.view.ViewGroup import androidx.cardview.widget.CardView import androidx.core.view.isVisible +import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.android.wallpaper.R import com.android.wallpaper.asset.Asset import com.android.wallpaper.asset.BitmapCachingAsset import com.android.wallpaper.asset.CurrentWallpaperAssetVN import com.android.wallpaper.model.LiveWallpaperInfo import com.android.wallpaper.model.WallpaperInfo +import com.android.wallpaper.module.CustomizationSections import com.android.wallpaper.picker.WorkspaceSurfaceHolderCallback import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel import com.android.wallpaper.util.ResourceUtils @@ -57,8 +61,16 @@ object ScreenPreviewBinder { id: Int, args: Bundle = Bundle.EMPTY, ) + fun destroy() } + /** + * Binds the view to the given [viewModel]. + * + * Note that if [dimWallpaper] is `true`, the wallpaper will be dimmed (to help highlight + * something that is changing on top of the wallpaper, for example, the lock screen shortcuts or + * the clock). + */ @JvmStatic fun bind( activity: Activity, @@ -66,9 +78,19 @@ object ScreenPreviewBinder { viewModel: ScreenPreviewViewModel, lifecycleOwner: LifecycleOwner, offsetToStart: Boolean, + dimWallpaper: Boolean = false, + // TODO (b/270193793): add below fields to all usages of this class & remove default values + screen: CustomizationSections.Screen = CustomizationSections.Screen.LOCK_SCREEN, + onPreviewDirty: () -> Unit = {}, ): Binding { val workspaceSurface: SurfaceView = previewView.requireViewById(R.id.workspace_surface) val wallpaperSurface: SurfaceView = previewView.requireViewById(R.id.wallpaper_surface) + wallpaperSurface.setZOrderOnTop(false) + + if (dimWallpaper) { + previewView.requireViewById<View>(R.id.wallpaper_dimming_scrim).isVisible = true + workspaceSurface.setZOrderOnTop(true) + } previewView.radius = previewView.resources.getDimension(R.dimen.wallpaper_picker_entry_card_corner_radius) @@ -78,10 +100,23 @@ object ScreenPreviewBinder { var wallpaperConnection: WallpaperConnection? = null var wallpaperInfo: WallpaperInfo? = null - lifecycleOwner.lifecycle.addObserver( - LifecycleEventObserver { _, event -> - when (event) { - Lifecycle.Event.ON_CREATE -> { + val job = + lifecycleOwner.lifecycleScope.launch { + launch { + val lifecycleObserver = + object : DefaultLifecycleObserver { + override fun onStop(owner: LifecycleOwner) { + super.onStop(owner) + wallpaperConnection?.disconnect() + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + wallpaperConnection?.setVisibility(false) + } + } + + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { previewSurfaceCallback = WorkspaceSurfaceHolderCallback( workspaceSurface, @@ -89,7 +124,9 @@ object ScreenPreviewBinder { viewModel.getInitialExtras(), ) workspaceSurface.holder.addCallback(previewSurfaceCallback) - workspaceSurface.setZOrderMediaOverlay(true) + if (!dimWallpaper) { + workspaceSurface.setZOrderMediaOverlay(true) + } wallpaperSurfaceCallback = WallpaperSurfaceCallback( @@ -114,15 +151,42 @@ object ScreenPreviewBinder { ) } wallpaperSurface.holder.addCallback(wallpaperSurfaceCallback) - wallpaperSurface.setZOrderMediaOverlay(true) + if (!dimWallpaper) { + wallpaperSurface.setZOrderMediaOverlay(true) + } + + lifecycleOwner.lifecycle.addObserver(lifecycleObserver) } - Lifecycle.Event.ON_DESTROY -> { - workspaceSurface.holder.removeCallback(previewSurfaceCallback) - previewSurfaceCallback?.cleanUp() - wallpaperSurface.holder.removeCallback(wallpaperSurfaceCallback) - wallpaperSurfaceCallback?.cleanUp() + + // Here when destroyed. + lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) + workspaceSurface.holder.removeCallback(previewSurfaceCallback) + previewSurfaceCallback?.cleanUp() + wallpaperSurface.holder.removeCallback(wallpaperSurfaceCallback) + wallpaperSurfaceCallback?.cleanUp() + } + + launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + var initialWallpaperUpdate = true + viewModel.wallpaperUpdateEvents(screen)?.collect { + // Do not update screen preview on initial update,since the initial + // update results from starting or resuming the activity. + // + // In addition, update screen preview only if system color is a preset + // color. Otherwise, setting wallpaper will cause a change in wallpaper + // color and trigger a reset from system ui + if (initialWallpaperUpdate) { + initialWallpaperUpdate = false + } else if (viewModel.shouldHandleReload()) { + onPreviewDirty() + } + } } - Lifecycle.Event.ON_RESUME -> { + } + + launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { lifecycleOwner.lifecycleScope.launch { wallpaperInfo = viewModel.getWallpaperInfo() (wallpaperInfo as? LiveWallpaperInfo)?.let { liveWallpaperInfo -> @@ -162,16 +226,8 @@ object ScreenPreviewBinder { ) } } - Lifecycle.Event.ON_PAUSE -> { - wallpaperConnection?.setVisibility(false) - } - Lifecycle.Event.ON_STOP -> { - wallpaperConnection?.disconnect() - } - else -> Unit } } - ) return object : Binding { override fun show() { @@ -189,6 +245,24 @@ object ScreenPreviewBinder { override fun sendMessage(id: Int, args: Bundle) { previewSurfaceCallback?.send(id, args) } + + override fun destroy() { + job.cancel() + // We want to remove the SurfaceView from its parent and add it back. This causes + // the hierarchy to treat the SurfaceView as "dirty" which will cause it to render + // itself anew the next time the bind function is invoked. + removeAndReadd(workspaceSurface) + } + } + } + + private fun removeAndReadd(view: View) { + (view.parent as? ViewGroup)?.let { parent -> + val indexInParent = parent.indexOfChild(view) + if (indexInParent >= 0) { + parent.removeView(view) + parent.addView(view, indexInParent) + } } } diff --git a/src/com/android/wallpaper/picker/customization/ui/binder/WallpaperQuickSwitchOptionBinder.kt b/src/com/android/wallpaper/picker/customization/ui/binder/WallpaperQuickSwitchOptionBinder.kt new file mode 100644 index 00000000..11f2ede3 --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/ui/binder/WallpaperQuickSwitchOptionBinder.kt @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.ui.binder + +import android.animation.ValueAnimator +import android.view.View +import android.widget.ImageView +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.wallpaper.R +import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperQuickSwitchOptionViewModel +import kotlinx.coroutines.launch + +/** + * Binds between the view and view-model for a single wallpaper quick switch option. + * + * The options are presented to the user in some sort of collection and clicking on one of the + * options selects that wallpaper. + */ +object WallpaperQuickSwitchOptionBinder { + + /** Binds the given view to the given view-model. */ + fun bind( + view: View, + viewModel: WallpaperQuickSwitchOptionViewModel, + lifecycleOwner: LifecycleOwner, + smallOptionWidthPx: Int, + largeOptionWidthPx: Int, + ) { + val selectionBorder: View = view.requireViewById(R.id.selection_border) + val selectionIcon: View = view.requireViewById(R.id.selection_icon) + val progressIndicator: View = view.requireViewById(R.id.progress_indicator) + val thumbnailView: ImageView = view.requireViewById(R.id.thumbnail) + val placeholder: ImageView = view.requireViewById(R.id.placeholder) + + placeholder.setBackgroundColor(viewModel.placeholderColor) + + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.onSelected.collect { onSelectedOrNull -> + view.setOnClickListener( + if (onSelectedOrNull != null) { + { onSelectedOrNull.invoke() } + } else { + null + } + ) + } + } + + launch { + // We want to skip animating the first width update. + var isFirstValue = true + viewModel.isLarge.collect { isLarge -> + updateWidth( + view = view, + targetWidthPx = if (isLarge) largeOptionWidthPx else smallOptionWidthPx, + animate = !isFirstValue, + ) + isFirstValue = false + } + } + + launch { + viewModel.isSelectionBorderVisible.collect { + selectionBorder.animatedVisibility(isVisible = it) + } + } + + launch { + viewModel.isSelectionIconVisible.collect { + selectionIcon.animatedVisibility(isVisible = it) + } + } + + launch { + viewModel.isProgressIndicatorVisible.collect { + progressIndicator.animatedVisibility(isVisible = it) + } + } + + launch { + val thumbnail = viewModel.thumbnail() + if (thumbnailView.tag != thumbnail) { + thumbnailView.tag = thumbnail + if (thumbnail != null) { + thumbnailView.setImageBitmap(thumbnail) + thumbnailView.fadeIn() + } else { + thumbnailView.fadeOut() + } + } + } + } + } + } + + /** + * Updates the view width. + * + * @param view The [View] to update. + * @param targetWidthPx The width we want the view to have. + * @param animate Whether the update should be animated. + */ + private fun updateWidth( + view: View, + targetWidthPx: Int, + animate: Boolean, + ) { + fun setWidth(widthPx: Int) { + view.updateLayoutParams { width = widthPx } + } + + if (!animate) { + setWidth(widthPx = targetWidthPx) + return + } + + ValueAnimator.ofInt( + view.width, + targetWidthPx, + ) + .apply { + addUpdateListener { setWidth(it.animatedValue as Int) } + start() + } + } + + private fun View.animatedVisibility( + isVisible: Boolean, + ) { + if (isVisible) { + fadeIn() + } else { + fadeOut() + } + } + + private fun View.fadeIn() { + if (isVisible) { + return + } + + alpha = 0f + isVisible = true + animate().alpha(1f).start() + } + + private fun View.fadeOut() { + if (!isVisible) { + return + } + + animate().alpha(0f).withEndAction { isVisible = false }.start() + } +} diff --git a/src/com/android/wallpaper/picker/customization/ui/binder/WallpaperQuickSwitchSectionBinder.kt b/src/com/android/wallpaper/picker/customization/ui/binder/WallpaperQuickSwitchSectionBinder.kt new file mode 100644 index 00000000..95f233c8 --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/ui/binder/WallpaperQuickSwitchSectionBinder.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.ui.binder + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.DimenRes +import androidx.core.view.doOnLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.wallpaper.R +import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperQuickSwitchViewModel +import kotlinx.coroutines.launch + +/** Binds between the view and view-model for the wallpaper quick switch section. */ +object WallpaperQuickSwitchSectionBinder { + fun bind( + view: View, + viewModel: WallpaperQuickSwitchViewModel, + lifecycleOwner: LifecycleOwner, + onNavigateToFullWallpaperSelector: () -> Unit, + ) { + view.requireViewById<View>(R.id.more_wallpapers).setOnClickListener { + onNavigateToFullWallpaperSelector() + } + + val optionContainer: ViewGroup = view.requireViewById(R.id.options) + // We have to wait for the container to be laid out before we can bind it because we need + // its size to calculate the sizes of the option items. + optionContainer.doOnLayout { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + bindOptions( + parent = optionContainer, + viewModel = viewModel, + lifecycleOwner = lifecycleOwner, + ) + } + } + } + } + } + + /** Binds the option items to the given parent. */ + private suspend fun bindOptions( + parent: ViewGroup, + viewModel: WallpaperQuickSwitchViewModel, + lifecycleOwner: LifecycleOwner, + ) { + viewModel.options.collect { options -> + // Remove all views from a previous update. + parent.removeAllViews() + + // Calculate the sizes that views should have. + val (largeOptionWidth, smallOptionWidth) = calculateSizes(parent, options.size) + + // Create, add, and bind a view for each option. + options.forEach { option -> + val optionView = + createOptionView( + parent = parent, + ) + parent.addView(optionView) + WallpaperQuickSwitchOptionBinder.bind( + view = optionView, + viewModel = option, + lifecycleOwner = lifecycleOwner, + smallOptionWidthPx = smallOptionWidth, + largeOptionWidthPx = largeOptionWidth, + ) + } + } + } + + /** + * Returns a pair where the first value is the width that we should use for the large/selected + * option and the second value is the width for the small/non-selected options. + */ + private fun calculateSizes( + parent: View, + optionCount: Int, + ): Pair<Int, Int> { + // The large/selected option is a square. Its size should be equal to the height of its + // container (with padding removed). + val largeOptionWidth = parent.height - parent.paddingTop - parent.paddingBottom + // We'll use the total (non-padded) width of the container to figure out the widths of the + // small/non-selected options. + val optionContainerWidthWithoutPadding = + parent.width - parent.paddingStart - parent.paddingEnd + // First, we will need the total of the widths of all the spacings between the options. + val spacingWidth = parent.dimensionResource(R.dimen.spacing_8dp) + val totalMarginWidths = (optionCount - 1) * spacingWidth + + val remainingSpaceForSmallOptions = + optionContainerWidthWithoutPadding - largeOptionWidth - totalMarginWidths + // One option is always large, the rest are small. + val numberOfSmallOptions = optionCount - 1 + val smallOptionWidth = + if (numberOfSmallOptions != 0) { + (remainingSpaceForSmallOptions / numberOfSmallOptions).coerceAtMost( + parent.dimensionResource(R.dimen.wallpaper_quick_switch_max_option_width) + ) + } else { + 0 + } + + return Pair(largeOptionWidth, smallOptionWidth) + } + + /** Returns a new [View] for an option, without attaching it to the view-tree. */ + private fun createOptionView( + parent: ViewGroup, + ): View { + return LayoutInflater.from(parent.context) + .inflate( + R.layout.wallpaper_quick_switch_option, + parent, + false, + ) + } + + /** Compose-inspired cnvenience alias for getting a dimension in pixels. */ + private fun View.dimensionResource( + @DimenRes res: Int, + ): Int { + return context.resources.getDimensionPixelSize(res) + } +} diff --git a/src/com/android/wallpaper/picker/customization/ui/section/ConnectedSectionController.kt b/src/com/android/wallpaper/picker/customization/ui/section/ConnectedSectionController.kt new file mode 100644 index 00000000..aae5edfe --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/ui/section/ConnectedSectionController.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.ui.section + +import android.annotation.SuppressLint +import android.content.Context +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import com.android.wallpaper.R +import com.android.wallpaper.model.CustomizationSectionController +import com.android.wallpaper.picker.SectionView +import java.util.* + +/** + * A section controller that renders two sections that are connected. + * + * In portrait mode, they are rendered vertically; in landscape mode, side-by-side. + */ +class ConnectedSectionController( + /** First section. */ + private val firstSectionController: CustomizationSectionController<out SectionView>, + /** Second section. */ + private val secondSectionController: CustomizationSectionController<out SectionView>, + /** Whether to flip the order of the child sections when laid out horizontally. */ + private val reverseOrderWhenHorizontal: Boolean = false, +) : CustomizationSectionController<ResponsiveLayoutSectionView> { + override fun isAvailable(context: Context): Boolean { + return firstSectionController.isAvailable(context) || + secondSectionController.isAvailable(context) + } + + @SuppressLint("InflateParams") // It's okay that we're inflating without a parent view. + override fun createView(context: Context): ResponsiveLayoutSectionView { + val view = + LayoutInflater.from(context) + .inflate( + R.layout.responsive_section, + null, + ) as ResponsiveLayoutSectionView + + val isHorizontal = view.orientation == LinearLayout.HORIZONTAL + val flipViewOrder = reverseOrderWhenHorizontal && isHorizontal + + add( + parentView = view, + childController = + if (flipViewOrder) { + secondSectionController + } else { + firstSectionController + }, + isHorizontal = isHorizontal, + isFirst = true, + ) + add( + parentView = view, + childController = + if (flipViewOrder) { + firstSectionController + } else { + secondSectionController + }, + isHorizontal = isHorizontal, + isFirst = false, + ) + + return view + } + + private fun add( + parentView: LinearLayout, + childController: CustomizationSectionController<out SectionView>, + isHorizontal: Boolean, + isFirst: Boolean, + ) { + val childView = + childController.createView( + context = parentView.context, + params = + CustomizationSectionController.ViewCreationParams( + isConnectedHorizontallyToOtherSections = isHorizontal, + ), + ) + + if (isHorizontal) { + // We want each child to stretch to fill an equal amount as the other children. + childView.layoutParams = + LinearLayout.LayoutParams( + /* width= */ 0, + /* height= */ LinearLayout.LayoutParams.WRAP_CONTENT, + /* weight= */ 1f, + ) + + val isLeftToRight = + TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == + View.LAYOUT_DIRECTION_LTR + childView.setBackgroundResource( + if (isLeftToRight) { + // In left-to-right layouts, the first item is on the left. + if (isFirst) { + R.drawable.leftmost_connected_section_background + } else { + R.drawable.rightmost_connected_section_background + } + } else { + // In right-to-left layouts, the first item is on the right. + if (isFirst) { + R.drawable.rightmost_connected_section_background + } else { + R.drawable.leftmost_connected_section_background + } + } + ) + } + + parentView.addView(childView) + } +} diff --git a/src/com/android/wallpaper/picker/customization/ui/section/ResponsiveLayoutSectionView.kt b/src/com/android/wallpaper/picker/customization/ui/section/ResponsiveLayoutSectionView.kt new file mode 100644 index 00000000..52469468 --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/ui/section/ResponsiveLayoutSectionView.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.ui.section + +import android.content.Context +import android.util.AttributeSet +import com.android.wallpaper.picker.SectionView + +class ResponsiveLayoutSectionView( + context: Context, + attrs: AttributeSet?, +) : + SectionView( + context, + attrs, + ) diff --git a/src/com/android/wallpaper/picker/customization/ui/section/ScreenPreviewSectionController.kt b/src/com/android/wallpaper/picker/customization/ui/section/ScreenPreviewSectionController.kt index 6cb7d068..821c5bd8 100644 --- a/src/com/android/wallpaper/picker/customization/ui/section/ScreenPreviewSectionController.kt +++ b/src/com/android/wallpaper/picker/customization/ui/section/ScreenPreviewSectionController.kt @@ -19,19 +19,23 @@ package com.android.wallpaper.picker.customization.ui.section import android.annotation.SuppressLint import android.app.Activity -import android.app.WallpaperColors import android.content.Context +import android.os.Bundle import android.view.LayoutInflater +import android.view.View import androidx.cardview.widget.CardView +import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope +import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants import com.android.wallpaper.R import com.android.wallpaper.model.CustomizationSectionController import com.android.wallpaper.model.WallpaperColorsViewModel import com.android.wallpaper.model.WallpaperInfo import com.android.wallpaper.module.CurrentWallpaperInfoFactory import com.android.wallpaper.module.CustomizationSections +import com.android.wallpaper.picker.CategorySelectorFragment +import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor import com.android.wallpaper.picker.customization.ui.binder.ScreenPreviewBinder import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel import com.android.wallpaper.util.DisplayUtils @@ -44,19 +48,28 @@ import kotlinx.coroutines.withContext /** Controls the screen preview section. */ @OptIn(ExperimentalCoroutinesApi::class) -class ScreenPreviewSectionController( +open class ScreenPreviewSectionController( private val activity: Activity, private val lifecycleOwner: LifecycleOwner, private val initialScreen: CustomizationSections.Screen, private val wallpaperInfoFactory: CurrentWallpaperInfoFactory, private val colorViewModel: WallpaperColorsViewModel, private val displayUtils: DisplayUtils, + private val navigator: CustomizationSectionController.CustomizationSectionNavigationController, + private val wallpaperInteractor: WallpaperInteractor, ) : CustomizationSectionController<ScreenPreviewView> { private lateinit var lockScreenBinding: ScreenPreviewBinder.Binding private lateinit var homeScreenBinding: ScreenPreviewBinder.Binding - override fun isAvailable(context: Context?): Boolean { + /** Override to hide the lock screen clock preview. */ + open val hideLockScreenClockPreview = false + + override fun shouldRetainInstanceWhenSwitchingTabs(): Boolean { + return true + } + + override fun isAvailable(context: Context): Boolean { // Assumption is that, if this section controller is included, we are using the revamped UI // so it should always be shown. return true @@ -70,6 +83,9 @@ class ScreenPreviewSectionController( R.layout.screen_preview_section, /* parent= */ null, ) as ScreenPreviewView + val onClickListener = + View.OnClickListener { navigator.navigateTo(CategorySelectorFragment()) } + view.setOnClickListener(onClickListener) val lockScreenView: CardView = view.requireViewById(R.id.lock_preview) val homeScreenView: CardView = view.requireViewById(R.id.home_preview) @@ -95,7 +111,7 @@ class ScreenPreviewSectionController( loadInitialColors( context = context, wallpaper = wallpaper, - liveData = colorViewModel.lockWallpaperColors, + screen = CustomizationSections.Screen.LOCK_SCREEN, ) continuation.resume(wallpaper, null) }, @@ -104,11 +120,29 @@ class ScreenPreviewSectionController( } }, onWallpaperColorChanged = { colors -> - colorViewModel.lockWallpaperColors.value = colors + colorViewModel.setLockWallpaperColors(colors) + }, + initialExtrasProvider = { + Bundle().apply { + // Hide the clock from the system UI rendered preview so we can + // place the carousel on top of it. + putBoolean( + ClockPreviewConstants.KEY_HIDE_CLOCK, + hideLockScreenClockPreview, + ) + } }, + wallpaperInteractor = wallpaperInteractor, ), lifecycleOwner = lifecycleOwner, - offsetToStart = displayUtils.isOnWallpaperDisplay(activity), + offsetToStart = displayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(activity), + screen = CustomizationSections.Screen.LOCK_SCREEN, + onPreviewDirty = { + // only the visible binding should recreate the activity so it's not done twice + if (lockScreenView.isVisible) { + activity.recreate() + } + }, ) homeScreenBinding = ScreenPreviewBinder.bind( @@ -132,7 +166,7 @@ class ScreenPreviewSectionController( loadInitialColors( context = context, wallpaper = wallpaper, - liveData = colorViewModel.homeWallpaperColors, + screen = CustomizationSections.Screen.HOME_SCREEN ) continuation.resume(wallpaper, null) }, @@ -141,11 +175,19 @@ class ScreenPreviewSectionController( } }, onWallpaperColorChanged = { colors -> - colorViewModel.lockWallpaperColors.value = colors + colorViewModel.setHomeWallpaperColors(colors) }, + wallpaperInteractor = wallpaperInteractor, ), lifecycleOwner = lifecycleOwner, - offsetToStart = displayUtils.isOnWallpaperDisplay(activity), + offsetToStart = displayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(activity), + screen = CustomizationSections.Screen.HOME_SCREEN, + onPreviewDirty = { + // only the visible binding should recreate the activity so it's not done twice + if (homeScreenView.isVisible) { + activity.recreate() + } + }, ) onScreenSwitched(isOnLockScreen = initialScreen == CustomizationSections.Screen.LOCK_SCREEN) @@ -166,11 +208,19 @@ class ScreenPreviewSectionController( private fun loadInitialColors( context: Context, wallpaper: WallpaperInfo?, - liveData: MutableLiveData<WallpaperColors>, + screen: CustomizationSections.Screen, ) { lifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { val colors = wallpaper?.computeColorInfo(context)?.get()?.wallpaperColors - withContext(Dispatchers.Main) { liveData.value = colors } + withContext(Dispatchers.Main) { + if (colors != null) { + if (screen == CustomizationSections.Screen.LOCK_SCREEN) { + colorViewModel.setLockWallpaperColors(colors) + } else { + colorViewModel.setHomeWallpaperColors(colors) + } + } + } } } } diff --git a/src/com/android/wallpaper/picker/customization/ui/section/ScreenPreviewView.kt b/src/com/android/wallpaper/picker/customization/ui/section/ScreenPreviewView.kt index ee55de41..c9e8022b 100644 --- a/src/com/android/wallpaper/picker/customization/ui/section/ScreenPreviewView.kt +++ b/src/com/android/wallpaper/picker/customization/ui/section/ScreenPreviewView.kt @@ -19,7 +19,11 @@ package com.android.wallpaper.picker.customization.ui.section import android.content.Context import android.util.AttributeSet +import android.view.MotionEvent +import android.view.ViewConfiguration import com.android.wallpaper.picker.SectionView +import kotlin.math.pow +import kotlin.math.sqrt class ScreenPreviewView( context: Context, @@ -28,4 +32,57 @@ class ScreenPreviewView( SectionView( context, attrs, - ) + ) { + + private var downX = 0f + private var downY = 0f + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + downX = event.x + downY = event.y + } + + // We want to intercept clicks so the Carousel MotionLayout child doesn't prevent users from + // clicking on the screen preview. + if (isClick(event, downX, downY)) { + return performClick() + } + + return super.onInterceptTouchEvent(event) + } + + companion object { + private fun isClick(event: MotionEvent, downX: Float, downY: Float): Boolean { + return when { + // It's not a click if the event is not an UP action (though it may become one + // later, when/if an UP is received). + event.actionMasked != MotionEvent.ACTION_UP -> false + // It's not a click if too much time has passed between the down and the current + // event. + gestureElapsedTime(event) > ViewConfiguration.getTapTimeout() -> false + // It's not a click if the touch traveled too far. + distanceMoved(event, downX, downY) > ViewConfiguration.getTouchSlop() -> false + // Otherwise, this is a click! + else -> true + } + } + + /** + * Returns the distance that the pointer traveled in the touch gesture the given event is + * part of. + */ + private fun distanceMoved(event: MotionEvent, downX: Float, downY: Float): Float { + val deltaX = event.x - downX + val deltaY = event.y - downY + return sqrt(deltaX.pow(2) + deltaY.pow(2)) + } + + /** + * Returns the elapsed time since the touch gesture the given event is part of has begun. + */ + private fun gestureElapsedTime(event: MotionEvent): Long { + return event.eventTime - event.downTime + } + } +} diff --git a/src/com/android/wallpaper/picker/customization/ui/section/WallpaperQuickSwitchSectionController.kt b/src/com/android/wallpaper/picker/customization/ui/section/WallpaperQuickSwitchSectionController.kt new file mode 100644 index 00000000..d54e9c0e --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/ui/section/WallpaperQuickSwitchSectionController.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.ui.section + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import androidx.lifecycle.LifecycleOwner +import com.android.wallpaper.R +import com.android.wallpaper.model.CustomizationSectionController +import com.android.wallpaper.module.CustomizationSections +import com.android.wallpaper.picker.CategorySelectorFragment +import com.android.wallpaper.picker.customization.ui.binder.WallpaperQuickSwitchSectionBinder +import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperQuickSwitchViewModel + +/** Controls a section that lets the user switch wallpapers quickly. */ +class WallpaperQuickSwitchSectionController( + private val screen: CustomizationSections.Screen, + private val viewModel: WallpaperQuickSwitchViewModel, + private val lifecycleOwner: LifecycleOwner, + private val navigator: CustomizationSectionController.CustomizationSectionNavigationController, +) : CustomizationSectionController<WallpaperQuickSwitchView> { + + override fun isAvailable(context: Context): Boolean { + return true + } + + @SuppressLint("InflateParams") // We don't care that the parent is null. + override fun createView(context: Context): WallpaperQuickSwitchView { + val view = + LayoutInflater.from(context) + .inflate( + R.layout.wallpaper_quick_switch_section, + /* parent= */ null, + ) as WallpaperQuickSwitchView + viewModel.setOnLockScreen( + isLockScreenSelected = screen == CustomizationSections.Screen.LOCK_SCREEN, + ) + WallpaperQuickSwitchSectionBinder.bind( + view = view, + viewModel = viewModel, + lifecycleOwner = lifecycleOwner, + onNavigateToFullWallpaperSelector = { + navigator.navigateTo(CategorySelectorFragment()) + }, + ) + return view + } + + override fun onScreenSwitched(isOnLockScreen: Boolean) { + viewModel.setOnLockScreen(isLockScreenSelected = isOnLockScreen) + } +} diff --git a/src/com/android/wallpaper/picker/customization/ui/section/WallpaperQuickSwitchView.kt b/src/com/android/wallpaper/picker/customization/ui/section/WallpaperQuickSwitchView.kt new file mode 100644 index 00000000..5a23f09e --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/ui/section/WallpaperQuickSwitchView.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.ui.section + +import android.content.Context +import android.util.AttributeSet +import com.android.wallpaper.picker.SectionView + +class WallpaperQuickSwitchView( + context: Context, + attrs: AttributeSet?, +) : + SectionView( + context, + attrs, + ) diff --git a/src/com/android/wallpaper/picker/customization/ui/viewmodel/CustomizationPickerViewModel.kt b/src/com/android/wallpaper/picker/customization/ui/viewmodel/CustomizationPickerViewModel.kt index 7fd622cb..14c0715d 100644 --- a/src/com/android/wallpaper/picker/customization/ui/viewmodel/CustomizationPickerViewModel.kt +++ b/src/com/android/wallpaper/picker/customization/ui/viewmodel/CustomizationPickerViewModel.kt @@ -81,7 +81,22 @@ constructor( } init { - _isOnLockScreen.value = savedStateHandle[KEY_SAVED_STATE_IS_ON_LOCK_SCREEN] ?: true + savedStateHandle.get<Boolean>(KEY_SAVED_STATE_IS_ON_LOCK_SCREEN)?.let { + _isOnLockScreen.value = it + } + } + + /** + * Sets the initial screen we should be on, unless there's already a selected screen from a + * previous saved state, in which case we ignore the passed-in one. + */ + fun setInitialScreen(onLockScreen: Boolean) { + _isOnLockScreen.value = + savedStateHandle[KEY_SAVED_STATE_IS_ON_LOCK_SCREEN] + ?: run { + savedStateHandle[KEY_SAVED_STATE_IS_ON_LOCK_SCREEN] = onLockScreen + onLockScreen + } } companion object { diff --git a/src/com/android/wallpaper/picker/customization/ui/viewmodel/ScreenPreviewViewModel.kt b/src/com/android/wallpaper/picker/customization/ui/viewmodel/ScreenPreviewViewModel.kt index 79e18a38..1ceaa80b 100644 --- a/src/com/android/wallpaper/picker/customization/ui/viewmodel/ScreenPreviewViewModel.kt +++ b/src/com/android/wallpaper/picker/customization/ui/viewmodel/ScreenPreviewViewModel.kt @@ -20,7 +20,11 @@ package com.android.wallpaper.picker.customization.ui.viewmodel import android.app.WallpaperColors import android.os.Bundle import com.android.wallpaper.model.WallpaperInfo +import com.android.wallpaper.module.CustomizationSections +import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor +import com.android.wallpaper.picker.customization.shared.model.WallpaperModel import com.android.wallpaper.util.PreviewUtils +import kotlinx.coroutines.flow.Flow /** Models the UI state for a preview of the home screen or lock screen. */ class ScreenPreviewViewModel( @@ -28,7 +32,19 @@ class ScreenPreviewViewModel( private val initialExtrasProvider: () -> Bundle? = { null }, private val wallpaperInfoProvider: suspend () -> WallpaperInfo?, private val onWallpaperColorChanged: (WallpaperColors?) -> Unit = {}, + // TODO (b/270193793): add below field to all usages, remove default value & make non-nullable + private val wallpaperInteractor: WallpaperInteractor? = null, ) { + /** Returns whether wallpaper picker should handle reload */ + fun shouldHandleReload(): Boolean { + return wallpaperInteractor?.let { it.shouldHandleReload() } ?: true + } + + /** Returns a flow that is updated whenever the wallpaper has been updated */ + fun wallpaperUpdateEvents(screen: CustomizationSections.Screen): Flow<WallpaperModel>? { + return wallpaperInteractor?.wallpaperUpdateEvents(screen) + } + fun getInitialExtras(): Bundle? { return initialExtrasProvider.invoke() } diff --git a/src/com/android/wallpaper/picker/customization/ui/viewmodel/WallpaperQuickSwitchOptionViewModel.kt b/src/com/android/wallpaper/picker/customization/ui/viewmodel/WallpaperQuickSwitchOptionViewModel.kt new file mode 100644 index 00000000..c8cb2fde --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/ui/viewmodel/WallpaperQuickSwitchOptionViewModel.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.ui.viewmodel + +import android.graphics.Bitmap +import kotlinx.coroutines.flow.Flow + +/** Models the UI state for an option in the wallpaper quick switcher. */ +data class WallpaperQuickSwitchOptionViewModel( + /** The ID of the wallpaper this option is associated with. */ + val wallpaperId: String, + /** A placeholder color to show in the option while we load the preview thumbnail. */ + val placeholderColor: Int, + /** A function to invoke to get the preview thumbnail for the option. */ + val thumbnail: suspend () -> Bitmap?, + /** + * Whether the option should be rendered as large. If `false`, the option should be rendered + * smaller. + */ + val isLarge: Flow<Boolean>, + /** Whether the progress indicator should be visible. */ + val isProgressIndicatorVisible: Flow<Boolean>, + /** Whether the selection border should be visible. */ + val isSelectionBorderVisible: Flow<Boolean>, + /** Whether the selection icon should be visible. */ + val isSelectionIconVisible: Flow<Boolean>, + /** + * A function to invoke when the option is clicked by the user. If `null`, the option is not + * clickable. + */ + val onSelected: Flow<(() -> Unit)?>, +) diff --git a/src/com/android/wallpaper/picker/customization/ui/viewmodel/WallpaperQuickSwitchViewModel.kt b/src/com/android/wallpaper/picker/customization/ui/viewmodel/WallpaperQuickSwitchViewModel.kt new file mode 100644 index 00000000..105dcff0 --- /dev/null +++ b/src/com/android/wallpaper/picker/customization/ui/viewmodel/WallpaperQuickSwitchViewModel.kt @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.customization.ui.viewmodel + +import android.os.Bundle +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.AbstractSavedStateViewModelFactory +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.savedstate.SavedStateRegistryOwner +import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor +import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch + +/** Models UI state for views that can render wallpaper quick switching. */ +@OptIn(ExperimentalCoroutinesApi::class) +class WallpaperQuickSwitchViewModel +@VisibleForTesting +constructor( + private val interactor: WallpaperInteractor, + maxOptions: Int, +) : ViewModel() { + private val isLockScreenSelected = MutableStateFlow(false) + + private val selectedWallpaperId: Flow<String> = + isLockScreenSelected + .flatMapLatest { isOnLockScreen -> + interactor.selectedWallpaperId( + destination = + if (isOnLockScreen) { + WallpaperDestination.LOCK + } else { + WallpaperDestination.HOME + }, + ) + } + .shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) + private val selectingWallpaperId: Flow<String?> = + isLockScreenSelected + .flatMapLatest { isOnLockScreen -> + interactor.selectingWallpaperId( + destination = + if (isOnLockScreen) { + WallpaperDestination.LOCK + } else { + WallpaperDestination.HOME + }, + ) + } + .shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) + + val options: Flow<List<WallpaperQuickSwitchOptionViewModel>> = + isLockScreenSelected + .flatMapLatest { isOnLockScreen -> + interactor + .previews( + destination = + if (isOnLockScreen) { + WallpaperDestination.LOCK + } else { + WallpaperDestination.HOME + }, + maxResults = maxOptions, + ) + .distinctUntilChangedBy { previews -> + // Produce a key that's the same if the same set of wallpapers is available, + // even if in a different order. This is so that the view can keep from + // moving the wallpaper options around when the sort order changes as the + // user selects different wallpapers. + previews.map { preview -> preview.wallpaperId }.sorted().joinToString(",") + } + .map { previews -> + // True if any option is becoming selected following user click. + val isSomethingBecomingSelectedFlow: Flow<Boolean> = + selectingWallpaperId.distinctUntilChanged().map { it != null } + + previews.map { preview -> + // True if this option is currently selected. + val isSelectedFlow: Flow<Boolean> = + selectedWallpaperId.distinctUntilChanged().map { + it == preview.wallpaperId + } + // True if this option is becoming the selected one following user + // click. + val isBecomingSelectedFlow: Flow<Boolean> = + selectingWallpaperId.distinctUntilChanged().map { + it == preview.wallpaperId + } + + WallpaperQuickSwitchOptionViewModel( + wallpaperId = preview.wallpaperId, + placeholderColor = preview.placeholderColor, + thumbnail = { + interactor.loadThumbnail( + wallpaperId = preview.wallpaperId, + ) + }, + isLarge = + combine( + isSelectedFlow, + isBecomingSelectedFlow, + isSomethingBecomingSelectedFlow, + ) { isSelected, isBecomingSelected, isSomethingBecomingSelected + -> + // The large option is the one that's currently selected or + // the one that is becoming the selected one following user + // click. + (isSelected && !isSomethingBecomingSelected) || + isBecomingSelected + }, + // We show the progress indicator if the option is in the process of + // becoming the selected one following user click. + isProgressIndicatorVisible = isBecomingSelectedFlow, + isSelectionBorderVisible = + combine( + isSelectedFlow, + isBecomingSelectedFlow, + isSomethingBecomingSelectedFlow, + ) { isSelected, isBeingSelected, isSomethingBecomingSelected -> + // The selection border is shown for the option that is the + // one that's currently selected or the one that is becoming + // the selected one following user click. + (isSelected && !isSomethingBecomingSelected) || + isBeingSelected + }, + isSelectionIconVisible = + combine( + isSelectedFlow, + isSomethingBecomingSelectedFlow, + ) { isSelected, isSomethingBecomingSelected -> + // The selection icon is shown for the option that is + // currently selected but only if nothing else is becoming + // selected. If anything is being selected following user + // click, the selection icon is not shown on any option. + isSelected && !isSomethingBecomingSelected + }, + onSelected = + combine( + isSelectedFlow, + isBecomingSelectedFlow, + isSomethingBecomingSelectedFlow, + ) { isSelected, isBeingSelected, isSomethingBecomingSelected + -> + // An option is selectable if it is not itself becoming + // selected following user click or if nothing else is + // becoming selected but this option is not the selected + // one. + (isSomethingBecomingSelected && !isBeingSelected) || + (!isSomethingBecomingSelected && !isSelected) + } + .distinctUntilChanged() + .map { isSelectable -> + if (isSelectable) { + { + // A selectable option can become selected. + viewModelScope.launch { + interactor.setWallpaper( + destination = + if (isOnLockScreen) { + WallpaperDestination.LOCK + } else { + WallpaperDestination.HOME + }, + wallpaperId = preview.wallpaperId, + ) + } + } + } else { + // A non-selectable option cannot become selected. + null + } + } + ) + } + } + } + .shareIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + replay = 1, + ) + + fun setOnLockScreen(isLockScreenSelected: Boolean) { + this.isLockScreenSelected.value = isLockScreenSelected + } + + companion object { + @JvmStatic + fun newFactory( + owner: SavedStateRegistryOwner, + defaultArgs: Bundle? = null, + interactor: WallpaperInteractor, + ): AbstractSavedStateViewModelFactory = + object : AbstractSavedStateViewModelFactory(owner, defaultArgs) { + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create( + key: String, + modelClass: Class<T>, + handle: SavedStateHandle, + ): T { + return WallpaperQuickSwitchViewModel( + interactor = interactor, + maxOptions = MAX_OPTIONS, + ) + as T + } + } + + /** The maximum number of options to show, including the currently-selected one. */ + private const val MAX_OPTIONS = 5 + } +} diff --git a/src/com/android/wallpaper/picker/individual/IndividualHolder.java b/src/com/android/wallpaper/picker/individual/IndividualHolder.java index c780db1e..feff2b8d 100755 --- a/src/com/android/wallpaper/picker/individual/IndividualHolder.java +++ b/src/com/android/wallpaper/picker/individual/IndividualHolder.java @@ -70,7 +70,9 @@ abstract class IndividualHolder extends ViewHolder { mTitleView.setVisibility(View.VISIBLE); mTileLayout.setContentDescription(title); } else if (firstAttribution != null) { - mTileLayout.setContentDescription(firstAttribution); + String contentDescription = wallpaper.getContentDescription(mActivity); + mTileLayout.setContentDescription( + contentDescription != null ? contentDescription : firstAttribution); } Drawable overlayIcon = wallpaper.getOverlayIcon(mActivity); diff --git a/src/com/android/wallpaper/picker/individual/IndividualPickerFragment2.kt b/src/com/android/wallpaper/picker/individual/IndividualPickerFragment2.kt index 4a21dbc2..3186eb5f 100644 --- a/src/com/android/wallpaper/picker/individual/IndividualPickerFragment2.kt +++ b/src/com/android/wallpaper/picker/individual/IndividualPickerFragment2.kt @@ -43,12 +43,14 @@ import androidx.annotation.DrawableRes import androidx.cardview.widget.CardView import androidx.core.widget.ContentLoadingProgressBar import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.wallpaper.R import com.android.wallpaper.model.Category import com.android.wallpaper.model.CategoryProvider import com.android.wallpaper.model.CategoryReceiver +import com.android.wallpaper.model.LiveWallpaperInfo import com.android.wallpaper.model.WallpaperCategory import com.android.wallpaper.model.WallpaperInfo import com.android.wallpaper.model.WallpaperRotationInitializer @@ -71,6 +73,8 @@ import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDele import com.bumptech.glide.Glide import com.bumptech.glide.MemoryCategory import java.util.Date +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch /** Displays the Main UI for picking an individual wallpaper image. */ class IndividualPickerFragment2 : @@ -109,18 +113,20 @@ class IndividualPickerFragment2 : private lateinit var imageGrid: RecyclerView private var adapter: IndividualAdapter? = null - private lateinit var category: WallpaperCategory + private var category: WallpaperCategory? = null private var wallpaperRotationInitializer: WallpaperRotationInitializer? = null private lateinit var items: MutableList<PickerItem> private var packageStatusNotifier: PackageStatusNotifier? = null - private var isWallpapersReceived = false - private var appStatusListener: PackageStatusNotifier.Listener? = null + private var appStatusListener: PackageStatusNotifier.Listener? = null private var progressDialog: ProgressDialog? = null + private var testingMode = false private var loading: ContentLoadingProgressBar? = null + private var shouldReloadWallpapers = false private lateinit var categoryProvider: CategoryProvider + private var appliedWallpaperIds: Set<String> = setOf() /** * Staged error dialog fragments that were unable to be shown when the activity didn't allow @@ -177,14 +183,14 @@ class IndividualPickerFragment2 : return } category = fetchedCategory as WallpaperCategory - onCategoryLoaded() + category?.let { onCategoryLoaded(it) } } }, false ) } - fun onCategoryLoaded() { + fun onCategoryLoaded(category: Category) { val fragmentHost = getIndividualPickerFragmentHost() if (fragmentHost.isHostToolbarShown) { fragmentHost.setToolbarTitle(category.title) @@ -195,22 +201,12 @@ class IndividualPickerFragment2 : if (mToolbar != null && isRotationEnabled()) { setUpToolbarMenu(R.menu.individual_picker_menu) } - fetchWallpapers(false) + var shouldForceReload = false if (category.supportsThirdParty()) { - appStatusListener = - PackageStatusNotifier.Listener { pkgName: String?, status: Int -> - if ( - status != PackageStatusNotifier.PackageStatus.REMOVED || - category.containsThirdParty(pkgName) - ) { - fetchWallpapers(true) - } - } - packageStatusNotifier?.addListener( - appStatusListener, - WallpaperService.SERVICE_INTERFACE - ) + shouldForceReload = true } + fetchWallpapers(shouldForceReload) + registerPackageListener(category) } private fun fetchWallpapers(forceReload: Boolean) { @@ -218,13 +214,13 @@ class IndividualPickerFragment2 : isWallpapersReceived = false updateLoading() val context = requireContext() - category.fetchWallpapers( + category?.fetchWallpapers( context.applicationContext, { fetchedWallpapers -> isWallpapersReceived = true updateLoading() val byGroup = fetchedWallpapers.groupBy { it.getGroupName(context) } - val appliedWallpaperIds = getAppliedWallpaperIds() + appliedWallpaperIds = getAppliedWallpaperIds() byGroup.forEach { (groupName, wallpapers) -> if (!TextUtils.isEmpty(groupName)) { items.add( @@ -235,12 +231,16 @@ class IndividualPickerFragment2 : } ) } + val currentWallpaper = WallpaperManager.getInstance(context).wallpaperInfo items.addAll( wallpapers.map { - PickerItem.WallpaperItem( - it, - appliedWallpaperIds.contains(it.wallpaperId) - ) + val isApplied = + if (it is LiveWallpaperInfo) { + it.isApplied(currentWallpaper) + } else { + appliedWallpaperIds.contains(it.wallpaperId) + } + PickerItem.WallpaperItem(it, isApplied) } ) } @@ -260,6 +260,24 @@ class IndividualPickerFragment2 : ) } + private fun registerPackageListener(category: Category) { + if (category.supportsThirdParty()) { + appStatusListener = + PackageStatusNotifier.Listener { pkgName: String?, status: Int -> + if ( + status != PackageStatusNotifier.PackageStatus.REMOVED || + category.containsThirdParty(pkgName) + ) { + fetchWallpapers(true) + } + } + packageStatusNotifier?.addListener( + appStatusListener, + WallpaperService.SERVICE_INTERFACE + ) + } + } + private fun updateLoading() { if (isWallpapersReceived) { loading?.hide() @@ -293,7 +311,7 @@ class IndividualPickerFragment2 : if (isRotationEnabled()) { setUpToolbarMenu(R.menu.individual_picker_menu) } - setTitle(category.title) + setTitle(category?.title) } imageGrid = view.findViewById<View>(R.id.wallpaper_grid) as RecyclerView loading = view.findViewById(R.id.loading_indicator) @@ -328,7 +346,7 @@ class IndividualPickerFragment2 : return } // Skip if category hasn't loaded yet - if (!this::category.isInitialized) { + if (category == null) { return } if (context == null) { @@ -365,7 +383,7 @@ class IndividualPickerFragment2 : } else { SizeCalculator.getIndividualTileSize(activity!!) } - setUpImageGrid(tileSizePx) + setUpImageGrid(tileSizePx, checkNotNull(category)) imageGrid.setAccessibilityDelegateCompat( WallpaperPickerRecyclerViewAccessibilityDelegate( imageGrid, @@ -408,7 +426,7 @@ class IndividualPickerFragment2 : * Create the adapter and assign it to mImageGrid. Both mImageGrid and mCategory are guaranteed * to not be null when this method is called. */ - private fun setUpImageGrid(tileSizePx: Point) { + private fun setUpImageGrid(tileSizePx: Point, category: Category) { adapter = IndividualAdapter( items, @@ -435,6 +453,14 @@ class IndividualPickerFragment2 : imageGrid.layoutManager = gridLayoutManager } + private suspend fun fetchWallpapersIfNeeded() { + coroutineScope { + if (isWallpapersReceived && (shouldReloadWallpapers || isAppliedWallpaperChanged())) { + fetchWallpapers(true) + } + } + } + override fun onResume() { super.onResume() val preferences = InjectorProvider.getInjector().getPreferences(requireActivity()) @@ -452,10 +478,16 @@ class IndividualPickerFragment2 : parentFragmentManager, TAG_START_ROTATION_ERROR_DIALOG ) + lifecycleScope.launch { fetchWallpapersIfNeeded() } } stagedStartRotationErrorDialogFragment = null } + override fun onPause() { + shouldReloadWallpapers = category?.supportsWallpaperSetUpdates() ?: false + super.onPause() + } + override fun onDestroyView() { super.onDestroyView() getIndividualPickerFragmentHost().removeToolbarMenu() @@ -493,7 +525,7 @@ class IndividualPickerFragment2 : override fun startRotation(@NetworkPreference networkPreference: Int) { if (!isRotationEnabled()) { - Log.e(TAG, "Rotation is not enabled for this category " + category.title) + Log.e(TAG, "Rotation is not enabled for this category " + category?.title) return } @@ -628,6 +660,17 @@ class IndividualPickerFragment2 : return appliedWallpaperIds } + // TODO(b/277180178): Extract the check to another class for unit testing + private fun isAppliedWallpaperChanged(): Boolean { + // Reload wallpapers if the current wallpapers have changed + getAppliedWallpaperIds().let { + if (appliedWallpaperIds.isEmpty() || appliedWallpaperIds != it) { + return true + } + } + return false + } + sealed class PickerItem(val title: CharSequence = "") { class WallpaperItem(val wallpaperInfo: WallpaperInfo, val isApplied: Boolean) : PickerItem() diff --git a/src/com/android/wallpaper/picker/option/ui/adapter/OptionItemAdapter.kt b/src/com/android/wallpaper/picker/option/ui/adapter/OptionItemAdapter.kt new file mode 100644 index 00000000..150dc42c --- /dev/null +++ b/src/com/android/wallpaper/picker/option/ui/adapter/OptionItemAdapter.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.option.ui.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.android.wallpaper.R +import com.android.wallpaper.picker.option.ui.binder.OptionItemBinder +import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** Adapts between option items and their views. */ +class OptionItemAdapter<T>( + @LayoutRes private val layoutResourceId: Int, + private val lifecycleOwner: LifecycleOwner, + private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO, + private val foregroundTintSpec: OptionItemBinder.TintSpec? = null, + private val bindIcon: (View, T) -> Unit, +) : RecyclerView.Adapter<OptionItemAdapter.ViewHolder>() { + + private val items = mutableListOf<OptionItemViewModel<T>>() + + fun setItems(items: List<OptionItemViewModel<T>>) { + lifecycleOwner.lifecycleScope.launch { + val oldItems = this@OptionItemAdapter.items + val newItems = items + val diffResult = + withContext(backgroundDispatcher) { + DiffUtil.calculateDiff( + object : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return oldItems.size + } + + override fun getNewListSize(): Int { + return newItems.size + } + + override fun areItemsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + return oldItem.key.value == newItem.key.value + } + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + return oldItem == newItem + } + }, + /* detectMoves= */ false, + ) + } + + oldItems.clear() + oldItems.addAll(items) + diffResult.dispatchUpdatesTo(this@OptionItemAdapter) + } + } + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var disposableHandle: DisposableHandle? = null + } + + override fun getItemCount(): Int { + return items.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(parent.context) + .inflate( + layoutResourceId, + parent, + false, + ) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.disposableHandle?.dispose() + val item = items[position] + item.payload?.let { + bindIcon(holder.itemView.requireViewById(R.id.foreground), item.payload) + } + holder.disposableHandle = + OptionItemBinder.bind( + view = holder.itemView, + viewModel = item, + lifecycleOwner = lifecycleOwner, + foregroundTintSpec = foregroundTintSpec, + ) + } +} diff --git a/src/com/android/wallpaper/picker/option/ui/binder/OptionItemBinder.kt b/src/com/android/wallpaper/picker/option/ui/binder/OptionItemBinder.kt new file mode 100644 index 00000000..802fc62c --- /dev/null +++ b/src/com/android/wallpaper/picker/option/ui/binder/OptionItemBinder.kt @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.wallpaper.picker.option.ui.binder + +import android.view.View +import android.view.ViewPropertyAnimator +import android.view.animation.LinearInterpolator +import android.view.animation.PathInterpolator +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.wallpaper.R +import com.android.wallpaper.picker.common.icon.ui.viewbinder.ContentDescriptionViewBinder +import com.android.wallpaper.picker.common.text.ui.viewbinder.TextViewBinder +import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.launch + +object OptionItemBinder { + /** + * Binds the given [View] to the given [OptionItemViewModel]. + * + * The child views of [view] must be named and arranged in the following manner, from top of the + * z-axis to the bottom: + * - [R.id.foreground] is the foreground drawable ([ImageView]). + * - [R.id.background] is the view in the background ([View]). + * - [R.id.selection_border] is a view rendering a border. It must have the same exact size as + * [R.id.background] ([View]) and must be placed below it on the z-axis (you read that right). + * + * The animation logic in this binder takes care of scaling up the border at the right time to + * help it peek out around the background. In order to allow for this, you may need to disable + * the clipping of child views across the view-tree using: + * ``` + * android:clipChildren="false" + * ``` + * + * Optionally, there may be an [R.id.text] [TextView] to show the text from the view-model. If + * one is not supplied, the text will be used as the content description of the icon. + * + * @param view The view; it must contain the child views described above. + * @param viewModel The view-model. + * @param lifecycleOwner The [LifecycleOwner]. + * @param animationSpec The specification for the animation. + * @param foregroundTintSpec The specification of how to tint the foreground icons. + * @return A [DisposableHandle] that must be invoked when the view is recycled. + */ + fun bind( + view: View, + viewModel: OptionItemViewModel<*>, + lifecycleOwner: LifecycleOwner, + animationSpec: AnimationSpec = AnimationSpec(), + foregroundTintSpec: TintSpec? = null, + ): DisposableHandle { + val borderView: View = view.requireViewById(R.id.selection_border) + val backgroundView: View = view.requireViewById(R.id.background) + val foregroundView: View = view.requireViewById(R.id.foreground) + val textView: TextView? = view.findViewById(R.id.text) + + if (textView != null) { + TextViewBinder.bind( + view = textView, + viewModel = viewModel.text, + ) + } else { + // Use the text as the content description of the foreground if we don't have a TextView + // dedicated to for the text. + ContentDescriptionViewBinder.bind( + view = foregroundView, + viewModel = viewModel.text, + ) + } + view.alpha = + if (viewModel.isEnabled) { + animationSpec.enabledAlpha + } else { + animationSpec.disabledAlpha + } + view.onLongClickListener = + if (viewModel.onLongClicked != null) { + View.OnLongClickListener { + viewModel.onLongClicked.invoke() + true + } + } else { + null + } + + val job = + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + // We only want to animate if the view-model is updating in response to a + // selection or deselection of the same exact option. For that, we save the + // last + // value of isSelected. + var lastSelected: Boolean? = null + + viewModel.key + .flatMapLatest { + // If the key changed, then it means that this binding is no longer + // rendering the UI for the same option as before, we nullify the + // last + // selected value to "forget" that we've ever seen a value for + // isSelected, + // effectively starting anew so the first update doesn't animate. + lastSelected = null + viewModel.isSelected + } + .collect { isSelected -> + if (foregroundTintSpec != null && foregroundView is ImageView) { + if (isSelected) { + foregroundView.setColorFilter( + foregroundTintSpec.selectedColor + ) + } else { + foregroundView.setColorFilter( + foregroundTintSpec.unselectedColor + ) + } + } + + animatedSelection( + animationSpec = animationSpec, + borderView = borderView, + contentView = backgroundView, + isSelected = isSelected, + animate = lastSelected != null && lastSelected != isSelected, + ) + lastSelected = isSelected + } + } + + launch { + viewModel.onClicked.collect { onClicked -> + view.setOnClickListener( + if (onClicked != null) { + View.OnClickListener { onClicked.invoke() } + } else { + null + } + ) + } + } + } + } + + return DisposableHandle { job.cancel() } + } + + /** + * Uses a "bouncy" animation to animate the selecting or un-selecting of a view with a + * background and a border. + * + * Note that it is expected that the [borderView] is below the [contentView] on the z axis so + * the latter obscures the former at rest. + * + * @param borderView A view for the selection border that should be shown when the view is + * + * ``` + * selected. + * @param contentView + * ``` + * + * The view containing the opaque part of the view. + * + * @param isSelected Whether the view is selected or not. + * @param animationSpec The specification for the animation. + * @param animate Whether to animate; if `false`, will jump directly to the final state without + * + * ``` + * animating. + * ``` + */ + private fun animatedSelection( + borderView: View, + contentView: View, + isSelected: Boolean, + animationSpec: AnimationSpec, + animate: Boolean = true, + ) { + if (isSelected) { + if (!animate) { + borderView.alpha = 1f + borderView.scale(1f) + contentView.scale(0.86f) + return + } + + // Border scale. + borderView + .animate() + .scale(1.099f) + .setDuration(animationSpec.durationMs / 2) + .setInterpolator(PathInterpolator(0.29f, 0f, 0.67f, 1f)) + .withStartAction { + borderView.scaleX = 0.98f + borderView.scaleY = 0.98f + borderView.alpha = 1f + } + .withEndAction { + borderView + .animate() + .scale(1f) + .setDuration(animationSpec.durationMs / 2) + .setInterpolator(PathInterpolator(0.33f, 0f, 0.15f, 1f)) + .start() + } + .start() + + // Background scale. + contentView + .animate() + .scale(0.9321f) + .setDuration(animationSpec.durationMs / 2) + .setInterpolator(PathInterpolator(0.29f, 0f, 0.67f, 1f)) + .withEndAction { + contentView + .animate() + .scale(0.86f) + .setDuration(animationSpec.durationMs / 2) + .setInterpolator(PathInterpolator(0.33f, 0f, 0.15f, 1f)) + .start() + } + .start() + } else { + if (!animate) { + borderView.alpha = 0f + contentView.scale(1f) + return + } + + // Border opacity. + borderView + .animate() + .alpha(0f) + .setDuration(animationSpec.durationMs / 2) + .setInterpolator(LinearInterpolator()) + .start() + + // Border scale. + borderView + .animate() + .scale(1f) + .setDuration(animationSpec.durationMs) + .setInterpolator(PathInterpolator(0.2f, 0f, 0f, 1f)) + .start() + + // Background scale. + contentView + .animate() + .scale(1f) + .setDuration(animationSpec.durationMs) + .setInterpolator(PathInterpolator(0.2f, 0f, 0f, 1f)) + .start() + } + } + + data class AnimationSpec( + /** Opacity of the option when it's enabled. */ + val enabledAlpha: Float = 1f, + /** Opacity of the option when it's disabled. */ + val disabledAlpha: Float = 0.3f, + /** Duration of the animation, in milliseconds. */ + val durationMs: Long = 333L, + ) + + data class TintSpec( + @ColorInt val selectedColor: Int, + @ColorInt val unselectedColor: Int, + ) + + private fun View.scale(scale: Float) { + scaleX = scale + scaleY = scale + } + + private fun ViewPropertyAnimator.scale(scale: Float): ViewPropertyAnimator { + return scaleX(scale).scaleY(scale) + } +} diff --git a/src/com/android/wallpaper/picker/option/ui/viewmodel/OptionItemViewModel.kt b/src/com/android/wallpaper/picker/option/ui/viewmodel/OptionItemViewModel.kt new file mode 100644 index 00000000..61068867 --- /dev/null +++ b/src/com/android/wallpaper/picker/option/ui/viewmodel/OptionItemViewModel.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.option.ui.viewmodel + +import com.android.wallpaper.picker.common.text.ui.viewmodel.Text +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** Models UI state for an item in a list of selectable options. */ +data class OptionItemViewModel<Payload>( + /** + * A stable key that uniquely identifies this option amongst all other options in the same list + * of options. + */ + val key: StateFlow<String>, + + /** + * The view model representing additional details needed for binding the icon of an option item + */ + val payload: Payload? = null, + + /** + * A text to show to the user (or attach as content description on the icon, if there's no + * dedicated view for it). + */ + val text: Text, + + /** Whether this option is selected. */ + val isSelected: StateFlow<Boolean>, + + /** Whether this option is enabled. */ + val isEnabled: Boolean = true, + + /** Notifies that the option has been clicked by the user. */ + val onClicked: Flow<(() -> Unit)?>, + + /** Notifies that the option has been long-clicked by the user. */ + val onLongClicked: (() -> Unit)? = null, +) { + override fun equals(other: Any?): Boolean { + val otherItem = other as? OptionItemViewModel<*> ?: return false + // skipping comparison of onClicked because it is correlated with + // changes on isSelected + return this.payload == otherItem.payload && + this.text == otherItem.text && + this.isSelected.value == otherItem.isSelected.value && + this.isEnabled == otherItem.isEnabled && + this.onLongClicked == otherItem.onLongClicked + } +} diff --git a/src/com/android/wallpaper/picker/undo/domain/interactor/SnapshotRestorer.kt b/src/com/android/wallpaper/picker/undo/domain/interactor/SnapshotRestorer.kt index 30478341..27ae6095 100644 --- a/src/com/android/wallpaper/picker/undo/domain/interactor/SnapshotRestorer.kt +++ b/src/com/android/wallpaper/picker/undo/domain/interactor/SnapshotRestorer.kt @@ -25,12 +25,12 @@ interface SnapshotRestorer { /** * Sets up the restorer. * - * @param updater An updater the can be used when a new snapshot should be stored; invoke this - * in response to state changes that you wish could be restored when the user asks to reset the - * changes. + * @param store An object the can be used when a new snapshot should be stored; use this in + * response to state changes that you wish could be restored when the user asks to reset the + * changes. * @return A snapshot of the initial state as it was at the moment that this method was invoked. */ - suspend fun setUpSnapshotRestorer(updater: (RestorableSnapshot) -> Unit): RestorableSnapshot + suspend fun setUpSnapshotRestorer(store: SnapshotStore): RestorableSnapshot /** Restores the state to what is described in the given snapshot. */ suspend fun restoreToSnapshot(snapshot: RestorableSnapshot) diff --git a/src/com/android/wallpaper/picker/undo/domain/interactor/SnapshotStore.kt b/src/com/android/wallpaper/picker/undo/domain/interactor/SnapshotStore.kt new file mode 100644 index 00000000..e2906a84 --- /dev/null +++ b/src/com/android/wallpaper/picker/undo/domain/interactor/SnapshotStore.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.picker.undo.domain.interactor + +import com.android.wallpaper.picker.undo.shared.model.RestorableSnapshot + +interface SnapshotStore { + fun retrieve(): RestorableSnapshot + fun store(snapshot: RestorableSnapshot) + + /** A "no op" implementation of [SnapshotStore] that's safe to call. */ + object NOOP : SnapshotStore { + override fun retrieve(): RestorableSnapshot { + return RestorableSnapshot(emptyMap()) + } + + override fun store(snapshot: RestorableSnapshot) = Unit + } +} diff --git a/src/com/android/wallpaper/picker/undo/domain/interactor/UndoInteractor.kt b/src/com/android/wallpaper/picker/undo/domain/interactor/UndoInteractor.kt index 3aa80ae1..53119fd2 100644 --- a/src/com/android/wallpaper/picker/undo/domain/interactor/UndoInteractor.kt +++ b/src/com/android/wallpaper/picker/undo/domain/interactor/UndoInteractor.kt @@ -18,6 +18,7 @@ package com.android.wallpaper.picker.undo.domain.interactor import com.android.wallpaper.picker.undo.data.repository.UndoRepository +import com.android.wallpaper.picker.undo.shared.model.RestorableSnapshot import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @@ -27,14 +28,17 @@ import kotlinx.coroutines.launch * * ## Usage * 1. Instantiate, injecting the supported [SnapshotRestorer] into it, one for each feature that + * * ``` * should support undo functionality. * ``` * 2. Call [startSession] which will bootstrap all passed-in [SnapshotRestorer] instances and + * * ``` * hydrate our model with the latest snapshots from each one. * ``` * 3. Observe [isUndoable] to know whether the UI for triggering an "undo" action should be made + * * ``` * visible to the user. * ``` @@ -51,18 +55,29 @@ class UndoInteractor( /** Bootstraps the undo system, querying each undo-supporting area for the initial snapshot. */ fun startSession() { - // TODO(b/262924056): take in a saved instance state and reuse it instead. repository.clearAllDirty() restorerByOwnerId.forEach { (ownerId, restorer) -> scope.launch { val initialSnapshot = - restorer.setUpSnapshotRestorer { subsequentSnapshot -> - val initialSnapshot = repository.getSnapshot(ownerId) - repository.putDirty( - ownerId = ownerId, - isDirty = initialSnapshot != subsequentSnapshot - ) - } + restorer.setUpSnapshotRestorer( + object : SnapshotStore { + override fun retrieve(): RestorableSnapshot { + return repository.getSnapshot(ownerId) + ?: error( + "No snapshot for this owner ID! Did you call this before" + + " storing a snapshot?" + ) + } + + override fun store(snapshot: RestorableSnapshot) { + val initialSnapshot = repository.getSnapshot(ownerId) + repository.putDirty( + ownerId = ownerId, + isDirty = initialSnapshot != snapshot + ) + } + } + ) repository.putSnapshot( ownerId = ownerId, diff --git a/src/com/android/wallpaper/picker/undo/shared/model/RestorableSnapshot.kt b/src/com/android/wallpaper/picker/undo/shared/model/RestorableSnapshot.kt index aac0e22d..4161a145 100644 --- a/src/com/android/wallpaper/picker/undo/shared/model/RestorableSnapshot.kt +++ b/src/com/android/wallpaper/picker/undo/shared/model/RestorableSnapshot.kt @@ -20,4 +20,33 @@ package com.android.wallpaper.picker.undo.shared.model /** Models a snapshot of the state of an undo-supporting feature at a given time. */ data class RestorableSnapshot( val args: Map<String, String>, -) +) { + /** + * Returns a copy of the [RestorableSnapshot] but with the [block] applied to its arguments. + * + * Sample usage: + * ``` + * val previousSnapshot: RestorableSnapshot = ... + * val nextSnapshot = previousSnapshot { args -> + * args.put("one", "true") + * args.remove("two") + * } + * + * // Now, nextSnapshot is exactly like previousSnapshot but with its args having "one" mapped + * // to "true" and without "two", since it was removed. + * ``` + * + * @param block A function that receives the original [args] from the current + * [RestorableSnapshot] and can edit them for inclusion into the returned + * [RestorableSnapshot]. + */ + fun copy( + block: (MutableMap<String, String>) -> Unit, + ): RestorableSnapshot { + val mutableArgs = args.toMutableMap() + block(mutableArgs) + return RestorableSnapshot( + args = mutableArgs.toMap(), + ) + } +} diff --git a/src/com/android/wallpaper/picker/undo/ui/binder/RevertToolbarButtonBinder.kt b/src/com/android/wallpaper/picker/undo/ui/binder/RevertToolbarButtonBinder.kt index a1eb0185..a2e85ee6 100644 --- a/src/com/android/wallpaper/picker/undo/ui/binder/RevertToolbarButtonBinder.kt +++ b/src/com/android/wallpaper/picker/undo/ui/binder/RevertToolbarButtonBinder.kt @@ -17,7 +17,6 @@ package com.android.wallpaper.picker.undo.ui.binder -import android.app.AlertDialog import android.app.Dialog import android.widget.Toolbar import androidx.lifecycle.Lifecycle @@ -25,7 +24,8 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.wallpaper.R -import com.android.wallpaper.picker.undo.ui.viewmodel.UndoDialogViewModel +import com.android.wallpaper.picker.common.dialog.ui.viewbinder.DialogViewBinder +import com.android.wallpaper.picker.common.dialog.ui.viewmodel.DialogViewModel import com.android.wallpaper.picker.undo.ui.viewmodel.UndoViewModel import kotlinx.coroutines.launch @@ -44,18 +44,12 @@ object RevertToolbarButtonBinder { var dialog: Dialog? = null - fun showDialog(viewModel: UndoDialogViewModel) { + fun showDialog(viewModel: DialogViewModel) { dialog = - AlertDialog.Builder(view.context, R.style.LightDialogTheme) - .setTitle(R.string.reset_confirmation_dialog_title) - .setMessage(R.string.reset_confirmation_dialog_message) - .setPositiveButton(R.string.reset) { _, _ -> viewModel.onConfirmed() } - .setNegativeButton(R.string.cancel, null) - .setOnDismissListener { - dialog = null - viewModel.onDismissed() - } - .create() + DialogViewBinder.show( + context = view.context, + viewModel = viewModel, + ) dialog?.show() } diff --git a/src/com/android/wallpaper/picker/undo/ui/viewmodel/UndoViewModel.kt b/src/com/android/wallpaper/picker/undo/ui/viewmodel/UndoViewModel.kt index 5e0701bd..37b3e832 100644 --- a/src/com/android/wallpaper/picker/undo/ui/viewmodel/UndoViewModel.kt +++ b/src/com/android/wallpaper/picker/undo/ui/viewmodel/UndoViewModel.kt @@ -17,6 +17,12 @@ package com.android.wallpaper.picker.undo.ui.viewmodel +import com.android.wallpaper.R +import com.android.wallpaper.picker.common.button.ui.viewmodel.ButtonStyle +import com.android.wallpaper.picker.common.button.ui.viewmodel.ButtonViewModel +import com.android.wallpaper.picker.common.dialog.ui.viewmodel.DialogViewModel +import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon +import com.android.wallpaper.picker.common.text.ui.viewmodel.Text import com.android.wallpaper.picker.undo.domain.interactor.UndoInteractor import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -28,21 +34,39 @@ class UndoViewModel( ) { /** Whether the "revert" button should be visible. */ val isRevertButtonVisible: Flow<Boolean> = interactor.isUndoable - private val _dialog = MutableStateFlow<UndoDialogViewModel?>(null) + private val _dialog = MutableStateFlow<DialogViewModel?>(null) /** * A view-model of the undo confirmation dialog that should be shown, or `null` when no dialog * should be shown. */ - val dialog: Flow<UndoDialogViewModel?> = _dialog.asStateFlow() + val dialog: Flow<DialogViewModel?> = _dialog.asStateFlow() /** Notifies that the "revert" button has been clicked by the user. */ fun onRevertButtonClicked() { _dialog.value = - UndoDialogViewModel( - onConfirmed = { - interactor.revertAll() - _dialog.value = null - }, + DialogViewModel( + icon = + Icon.Resource( + res = R.drawable.ic_device_reset, + contentDescription = null, + ), + title = Text.Resource(R.string.reset_confirmation_dialog_title), + message = Text.Resource(R.string.reset_confirmation_dialog_message), + buttons = + listOf( + ButtonViewModel( + text = Text.Resource(R.string.cancel), + style = ButtonStyle.Secondary, + ), + ButtonViewModel( + text = Text.Resource(R.string.reset), + style = ButtonStyle.Primary, + onClicked = { + interactor.revertAll() + _dialog.value = null + }, + ), + ), onDismissed = { _dialog.value = null }, ) } diff --git a/src/com/android/wallpaper/settings/data/repository/SecureSettingsRepository.kt b/src/com/android/wallpaper/settings/data/repository/SecureSettingsRepository.kt new file mode 100644 index 00000000..db56b678 --- /dev/null +++ b/src/com/android/wallpaper/settings/data/repository/SecureSettingsRepository.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.wallpaper.settings.data.repository + +import android.content.ContentResolver +import android.database.ContentObserver +import android.provider.Settings +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +/** Defines interface for classes that can provide access to data from [Settings.Secure]. */ +interface SecureSettingsRepository { + + /** Returns a [Flow] tracking the value of a setting as an [Int]. */ + fun intSetting( + name: String, + defaultValue: Int = 0, + ): Flow<Int> + + /** Updates the value of the setting with the given name. */ + suspend fun set( + name: String, + value: Int, + ) + + suspend fun get( + name: String, + defaultValue: Int = 0, + ): Int +} + +class SecureSettingsRepositoryImpl( + private val contentResolver: ContentResolver, + private val backgroundDispatcher: CoroutineDispatcher, +) : SecureSettingsRepository { + + override fun intSetting( + name: String, + defaultValue: Int, + ): Flow<Int> { + return callbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + contentResolver.registerContentObserver( + Settings.Secure.getUriFor(name), + /* notifyForDescendants= */ false, + observer, + ) + send(Unit) + + awaitClose { contentResolver.unregisterContentObserver(observer) } + } + .map { Settings.Secure.getInt(contentResolver, name, defaultValue) } + // The above work is done on the background thread (which is important for accessing + // settings through the content resolver). + .flowOn(backgroundDispatcher) + } + + override suspend fun set(name: String, value: Int) { + withContext(backgroundDispatcher) { + Settings.Secure.putInt( + contentResolver, + name, + value, + ) + } + } + + override suspend fun get(name: String, defaultValue: Int): Int { + return withContext(backgroundDispatcher) { + Settings.Secure.getInt( + contentResolver, + name, + defaultValue, + ) + } + } +} diff --git a/src/com/android/wallpaper/util/ActivityUtils.java b/src/com/android/wallpaper/util/ActivityUtils.java index 7456edd7..c6ffc61e 100755 --- a/src/com/android/wallpaper/util/ActivityUtils.java +++ b/src/com/android/wallpaper/util/ActivityUtils.java @@ -122,4 +122,13 @@ public final class ActivityUtils { return "wallpaper_only".equals( intent.getStringExtra("com.android.launcher3.WALLPAPER_FLAVOR")); } + + /** + * Returns {@code true} if the activity was launched from the home screen (launcher); + * {@code false} otherwise. + */ + public static boolean isLaunchedFromLauncher(Intent intent) { + return LaunchSourceUtils.LAUNCH_SOURCE_LAUNCHER.equals( + intent.getStringExtra(WALLPAPER_LAUNCH_SOURCE)); + } }
\ No newline at end of file diff --git a/src/com/android/wallpaper/util/DisplayUtils.kt b/src/com/android/wallpaper/util/DisplayUtils.kt index ce6980ed..cd364dbe 100644 --- a/src/com/android/wallpaper/util/DisplayUtils.kt +++ b/src/com/android/wallpaper/util/DisplayUtils.kt @@ -21,49 +21,92 @@ import android.graphics.Point import android.hardware.display.DisplayManager import android.util.Log import android.view.Display +import android.view.Surface.ROTATION_270 +import android.view.Surface.ROTATION_90 /** * Utility class to provide methods to find and obtain information about displays via {@link * DisplayManager} */ -class DisplayUtils(context: Context) { +class DisplayUtils(private val context: Context) { companion object { private const val TAG = "DisplayUtils" + private val ROTATION_HORIZONTAL_HINGE = setOf(ROTATION_90, ROTATION_270) } - private val internalDisplays: List<Display> + private val displayManager: DisplayManager by lazy { + context.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + } - init { - val appContext = context.applicationContext - val dm = appContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager - val allDisplays: Array<out Display> = - dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) - if (allDisplays.isEmpty()) { - Log.e(TAG, "No displays found on context $appContext") - throw RuntimeException("No displays found!") - } - internalDisplays = allDisplays.filter { it.type == Display.TYPE_INTERNAL } + fun hasMultiInternalDisplays(): Boolean { + return getInternalDisplays().size > 1 } - /** Returns the {@link Display} to be used to calculate wallpaper size and cropping. */ + /** + * Returns the internal {@link Display} with tthe largest area to be used to calculate wallpaper + * size and cropping. + */ fun getWallpaperDisplay(): Display { - return internalDisplays.maxWithOrNull { a, b -> getRealSize(a) - getRealSize(b) } + val internalDisplays = getInternalDisplays() + return internalDisplays.maxWithOrNull { a, b -> getRealArea(a) - getRealArea(b) } ?: internalDisplays[0] } /** + * Checks if the device only has one display or unfolded screen in horizontal hinge orientation. + */ + fun isSingleDisplayOrUnfoldedHorizontalHinge(activity: Activity): Boolean { + return !hasMultiInternalDisplays() || isUnfoldedHorizontalHinge(activity) + } + + /** + * Checks if the device is a foldable and it's unfolded and in horizontal hinge orientation + * (portrait). + */ + fun isUnfoldedHorizontalHinge(activity: Activity): Boolean { + return activity.display.rotation in ROTATION_HORIZONTAL_HINGE && + isOnWallpaperDisplay(activity) && + hasMultiInternalDisplays() + } + + fun getMaxDisplaysDimension(): Point { + val dimen = Point() + getInternalDisplays().let { displays -> + dimen.x = displays.maxOf { getRealSize(it).x } + dimen.y = displays.maxOf { getRealSize(it).y } + } + return dimen + } + + /** * Returns `true` if the current display is the wallpaper display on a multi-display device. * * On a multi-display device the wallpaper display is the largest display while on a single * display device the only display is both the wallpaper display and the current display. */ fun isOnWallpaperDisplay(activity: Activity): Boolean { - return activity.display.displayId == getWallpaperDisplay().displayId + return activity.display.uniqueId == getWallpaperDisplay().uniqueId } - private fun getRealSize(display: Display): Int { + private fun getRealArea(display: Display): Int { val p = Point() display.getRealSize(p) return p.x * p.y } + + private fun getRealSize(display: Display): Point { + val p = Point() + display.getRealSize(p) + return p + } + + private fun getInternalDisplays(): List<Display> { + val allDisplays: Array<out Display> = + displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) + if (allDisplays.isEmpty()) { + Log.e(TAG, "No displays found on context ${context.applicationContext}") + throw RuntimeException("No displays found!") + } + return allDisplays.filter { it.type == Display.TYPE_INTERNAL } + } } diff --git a/src/com/android/wallpaper/util/VideoWallpaperUtils.java b/src/com/android/wallpaper/util/VideoWallpaperUtils.java new file mode 100644 index 00000000..2093fe83 --- /dev/null +++ b/src/com/android/wallpaper/util/VideoWallpaperUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wallpaper.util; + +import com.android.wallpaper.model.LiveWallpaperInfo; +import com.android.wallpaper.model.WallpaperInfo; + +/** + * Workarounds for dealing with issues displaying video wallpaper previews until better solutions + * are found. + * + * See b/268066031. + */ +public class VideoWallpaperUtils { + + /** + * Transition time for fade-in animation. + */ + public static final int TRANSITION_MILLIS = 250; + + /** + * Returns true if the is a video wallpaper that requires the fade-in workaround. + */ + public static boolean needsFadeIn(WallpaperInfo info) { + return info instanceof LiveWallpaperInfo; + } +} diff --git a/src/com/android/wallpaper/util/WallpaperCropUtils.java b/src/com/android/wallpaper/util/WallpaperCropUtils.java index 380a6b61..4aed3c2d 100755 --- a/src/com/android/wallpaper/util/WallpaperCropUtils.java +++ b/src/com/android/wallpaper/util/WallpaperCropUtils.java @@ -190,7 +190,8 @@ public final class WallpaperCropUtils { * @return a Rect representing the area of the wallpaper to crop. */ public static Rect calculateCropRect(Context context, float wallpaperZoom, Point wallpaperSize, - Point defaultCropSurfaceSize, Point targetHostSize, int scrollX, int scrollY) { + Point defaultCropSurfaceSize, Point targetHostSize, int scrollX, int scrollY, + boolean cropExtraWidth) { // Calculate Rect of wallpaper in physical pixel terms (i.e., scaled to current zoom). int scaledWallpaperWidth = Math.round(wallpaperSize.x * wallpaperZoom); int scaledWallpaperHeight = Math.round(wallpaperSize.y * wallpaperZoom); @@ -205,12 +206,14 @@ public final class WallpaperCropUtils { int extraWidth = defaultCropSurfaceSize.x - targetHostSize.x; int extraHeightTopAndBottom = (int) ((defaultCropSurfaceSize.y - targetHostSize.y) / 2f); - // Try to increase size of screenRect to include extra width depending on the layout - // direction. - if (isRtl(context)) { - cropRect.left = Math.max(cropRect.left - extraWidth, rect.left); - } else { - cropRect.right = Math.min(cropRect.right + extraWidth, rect.right); + if (cropExtraWidth) { + // Try to increase size of screenRect to include extra width depending on the layout + // direction. + if (isRtl(context)) { + cropRect.left = Math.max(cropRect.left - extraWidth, rect.left); + } else { + cropRect.right = Math.min(cropRect.right + extraWidth, rect.right); + } } // Try to increase the size of the cropRect to to include extra height. @@ -301,14 +304,32 @@ public final class WallpaperCropUtils { * @param rawWallpaperSize the size of the raw wallpaper as a Point (x,y). * @param visibleRawWallpaperRect the area of the raw wallpaper which is expected to see. * @param wallpaperZoom the factor which is used to scale the raw wallpaper. + * @param cropExtraWidth true to crop extra wallpaper width for panel sliding. */ public static Rect calculateCropRect(Context context, Point hostViewSize, Point cropSize, - Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom) { + Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom, + boolean cropExtraWidth) { int scrollX = (int) (visibleRawWallpaperRect.left * wallpaperZoom); int scrollY = (int) (visibleRawWallpaperRect.top * wallpaperZoom); return calculateCropRect(context, wallpaperZoom, rawWallpaperSize, cropSize, hostViewSize, - scrollX, scrollY); + scrollX, scrollY, cropExtraWidth); + } + + /** + * Calculates {@link Rect} of the wallpaper which we want to crop to in physical pixel terms + * (i.e., scaled to current zoom). + * + * @param hostViewSize the size of the view hosting the wallpaper as a Point (x,y). + * @param cropSize the default size of the crop as a Point (x,y). + * @param rawWallpaperSize the size of the raw wallpaper as a Point (x,y). + * @param visibleRawWallpaperRect the area of the raw wallpaper which is expected to see. + * @param wallpaperZoom the factor which is used to scale the raw wallpaper. + */ + public static Rect calculateCropRect(Context context, Point hostViewSize, Point cropSize, + Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom) { + return calculateCropRect(context, hostViewSize, cropSize, rawWallpaperSize, + visibleRawWallpaperRect, wallpaperZoom, /* cropExtraWidth= */ true); } /** diff --git a/src/com/android/wallpaper/widget/DuoTabs.java b/src/com/android/wallpaper/widget/DuoTabs.java index a8f30240..b0fb0b14 100644 --- a/src/com/android/wallpaper/widget/DuoTabs.java +++ b/src/com/android/wallpaper/widget/DuoTabs.java @@ -18,8 +18,8 @@ package com.android.wallpaper.widget; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; -import android.widget.Button; import android.widget.FrameLayout; +import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.NonNull; @@ -54,10 +54,10 @@ public final class DuoTabs extends FrameLayout { void onTabSelected(@Tab int tab); } - OnTabSelectedListener mOnTabSelectedListener; - Button mPrimaryTab; - Button mSecondaryTab; - @Tab int mCurrentOverlayTab; + private OnTabSelectedListener mOnTabSelectedListener; + private final TextView mPrimaryTab; + private final TextView mSecondaryTab; + @Tab private int mCurrentOverlayTab; /** * Constructor diff --git a/src/com/android/wallpaper/widget/FloatingSheet.kt b/src/com/android/wallpaper/widget/FloatingSheet.kt index 7c8bbd76..26f068a4 100644 --- a/src/com/android/wallpaper/widget/FloatingSheet.kt +++ b/src/com/android/wallpaper/widget/FloatingSheet.kt @@ -154,8 +154,8 @@ class FloatingSheet(context: Context, attrs: AttributeSet?) : FrameLayout(contex * Adds Floating Sheet Callback to connected [BottomSheetBehavior]. * * @param callback the callback for floating sheet state changes, has to be in the type of - * [BottomSheetBehavior.BottomSheetCallback] since the floating sheet behavior is currently - * based on [BottomSheetBehavior] + * [BottomSheetBehavior.BottomSheetCallback] since the floating sheet behavior is currently + * based on [BottomSheetBehavior] */ fun addFloatingSheetCallback(callback: BottomSheetCallback?) { floatingSheetBehavior.addBottomSheetCallback(callback!!) diff --git a/src/com/android/wallpaper/widget/WallpaperControlButtonGroup.java b/src/com/android/wallpaper/widget/WallpaperControlButtonGroup.java index 00a681ae..c5c8f7ea 100644 --- a/src/com/android/wallpaper/widget/WallpaperControlButtonGroup.java +++ b/src/com/android/wallpaper/widget/WallpaperControlButtonGroup.java @@ -35,22 +35,26 @@ import com.android.wallpaper.R; public final class WallpaperControlButtonGroup extends FrameLayout { public static final int DELETE = 0; - public static final int CUSTOMIZE = 1; - public static final int EFFECTS = 2; - public static final int INFORMATION = 3; + public static final int EDIT = 1; + public static final int CUSTOMIZE = 2; + public static final int EFFECTS = 3; + public static final int INFORMATION = 4; + public static final int SHARE = 5; /** * Overlay tab */ - @IntDef({DELETE, CUSTOMIZE, EFFECTS, INFORMATION}) + @IntDef({DELETE, EDIT, CUSTOMIZE, EFFECTS, SHARE, INFORMATION}) public @interface WallpaperControlType { } - final int[] mFloatingSheetControlButtonTypes = { CUSTOMIZE, EFFECTS, INFORMATION }; + final int[] mFloatingSheetControlButtonTypes = { CUSTOMIZE, EFFECTS, SHARE, INFORMATION }; ToggleButton mDeleteButton; + ToggleButton mEditButton; ToggleButton mCustomizeButton; ToggleButton mEffectsButton; + ToggleButton mShareButton; ToggleButton mInformationButton; /** @@ -60,8 +64,10 @@ public final class WallpaperControlButtonGroup extends FrameLayout { super(context, attrs); LayoutInflater.from(context).inflate(R.layout.wallpaper_control_button_group, this, true); mDeleteButton = findViewById(R.id.delete_button); + mEditButton = findViewById(R.id.edit_button); mCustomizeButton = findViewById(R.id.customize_button); mEffectsButton = findViewById(R.id.effects_button); + mShareButton = findViewById(R.id.share_button); mInformationButton = findViewById(R.id.information_button); } @@ -81,10 +87,14 @@ public final class WallpaperControlButtonGroup extends FrameLayout { switch (type) { case DELETE: return mDeleteButton; + case EDIT: + return mEditButton; case CUSTOMIZE: return mCustomizeButton; case EFFECTS: return mEffectsButton; + case SHARE: + return mShareButton; case INFORMATION: return mInformationButton; default: @@ -115,15 +125,21 @@ public final class WallpaperControlButtonGroup extends FrameLayout { return; } mDeleteButton.setForeground(null); + mEditButton.setForeground(null); mCustomizeButton.setForeground(null); mEffectsButton.setForeground(null); + mShareButton.setForeground(null); mInformationButton.setForeground(null); mDeleteButton.setForeground(AppCompatResources.getDrawable(context, R.drawable.wallpaper_control_button_delete)); + mEditButton.setForeground( + AppCompatResources.getDrawable(context, R.drawable.wallpaper_control_button_edit)); mCustomizeButton.setForeground(AppCompatResources.getDrawable(context, R.drawable.wallpaper_control_button_customize)); mEffectsButton.setForeground(AppCompatResources.getDrawable(context, R.drawable.wallpaper_control_button_effect)); + mShareButton.setForeground(AppCompatResources.getDrawable(context, + R.drawable.wallpaper_control_button_share)); mInformationButton.setForeground( AppCompatResources.getDrawable(context, R.drawable.wallpaper_control_button_info)); } diff --git a/src/com/android/wallpaper/widget/floatingsheetcontent/WallpaperInfoContent.kt b/src/com/android/wallpaper/widget/floatingsheetcontent/WallpaperInfoContent.kt index 2bc75a7a..611df546 100644 --- a/src/com/android/wallpaper/widget/floatingsheetcontent/WallpaperInfoContent.kt +++ b/src/com/android/wallpaper/widget/floatingsheetcontent/WallpaperInfoContent.kt @@ -75,6 +75,8 @@ class WallpaperInfoContent(private var context: Context, private val wallpaper: wallpaper!!, actionLabel, WallpaperInfoHelper.shouldShowExploreButton(context, exploreIntent) - ) { onExploreClicked() } + ) { + onExploreClicked() + } } } |