[2/n] Move new picker logic to AOSP
- Wallpaper section
Bug: 190354625
Test: Build and run the app
Change-Id: I32d1eb29fff4a24529a5b6036f0aa30755c4f65b
diff --git a/res/drawable/ic_nav_wallpaper.xml b/res/drawable/ic_nav_wallpaper.xml
new file mode 100644
index 0000000..c935666
--- /dev/null
+++ b/res/drawable/ic_nav_wallpaper.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Represents the wallpaper icon (a "landscape" shape in a box) -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M9,12.71l2.14,2.58l3,-3.87L18,16.57H6L9,12.71zM5,5h6V3H5C3.9,3 3,3.9 3,5v6h2V5zM19,19h-6v2h6c1.1,0 2,-0.9 2,-2v-6h-2V19zM5,19v-6H3v6c0,1.1 0.9,2 2,2h6v-2H5zM19,5v6h2V5c0,-1.1 -0.9,-2 -2,-2h-6v2H19zM16,9c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1c-0.55,0 -1,0.45 -1,1S15.45,9 16,9z"/>
+</vector>
diff --git a/res/drawable/wallpaper_section_background.xml b/res/drawable/wallpaper_section_background.xml
new file mode 100644
index 0000000..208f543
--- /dev/null
+++ b/res/drawable/wallpaper_section_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <corners android:radius="28dp" />
+ <solid android:color="?androidprv:attr/colorSurface" />
+</shape>
diff --git a/res/layout/wallpaper_section_view.xml b/res/layout/wallpaper_section_view.xml
new file mode 100644
index 0000000..e963b3a
--- /dev/null
+++ b/res/layout/wallpaper_section_view.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<com.android.wallpaper.picker.WallpaperSectionView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/section_bottom_padding"
+ android:paddingHorizontal="@dimen/section_horizontal_padding"
+ android:orientation="vertical">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/wallpaper_section_background"
+ android:contentDescription="@string/wallpaper_preview_card_content_description">
+
+ <!-- Width percent = 146(preview width in spec) / 364(parent width in spec) = 0.4 -->
+ <!-- The height will be determined programmatically -->
+ <include
+ android:id="@+id/lock_preview"
+ layout="@layout/wallpaper_preview_card"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="@dimen/wallpaper_preview_margin_top"
+ app:layout_constraintEnd_toStartOf="@id/home_preview"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_percent="0.4" />
+
+ <include
+ android:id="@+id/home_preview"
+ layout="@layout/wallpaper_preview_card"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="@dimen/wallpaper_preview_margin_top"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/lock_preview"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_percent="0.4" />
+
+ <include
+ android:id="@+id/permission_needed"
+ layout="@layout/permission_needed_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/home_preview" />
+
+ <Button
+ android:id="@+id/wallpaper_picker_entry"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="@dimen/wallpaper_picker_entry_margin_vertical"
+ android:background="@drawable/btn_transparent_background"
+ android:textColor="?android:attr/colorAccent"
+ android:drawablePadding="@dimen/wallpaper_picker_entry_drawable_padding"
+ android:drawableStart="@drawable/ic_nav_wallpaper"
+ android:drawableTint="?android:attr/colorAccent"
+ android:text="@string/wallpaper_picker_entry_title"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/permission_needed" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</com.android.wallpaper.picker.WallpaperSectionView>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 2d63edc..34c327d 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -193,4 +193,16 @@
<!-- The size of lock screen preview 2 on the full screen. -->
<dimen name="lock_screen_preview2_date_text_size">20dp</dimen>
<dimen name="lock_screen_preview2_time_text_size">170dp</dimen>
+
+ <!-- Common dimensions for option sections -->
+ <dimen name="section_container_vertical_margin">24dp</dimen>
+ <dimen name="section_horizontal_padding">24dp</dimen>
+ <dimen name="section_vertical_padding">24dp</dimen>
+ <dimen name="section_top_padding">16dp</dimen>
+ <dimen name="section_bottom_padding">16dp</dimen>
+
+ <!-- For the wallpaper section -->
+ <dimen name="wallpaper_preview_margin_top">24dp</dimen>
+ <dimen name="wallpaper_picker_entry_drawable_padding">8dp</dimen>
+ <dimen name="wallpaper_picker_entry_margin_vertical">18dp</dimen>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6ea6272..4f3d62e 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -351,4 +351,10 @@
<!-- Label for a button which lets user show certain parts of the fullscreen preview UI. [CHAR LIMIT=30] -->
<string name="show_ui_preview_text">Show UI Preview</string>
+ <!-- The title of wallpaper picker entry in new picker. [CHAR LIMIT=30] -->
+ <string name="wallpaper_picker_entry_title" msgid="7039652539125902659">Change wallpaper</string>
+
+ <!-- Content description of wallpaper preview card (lockscreen). [CHAR_LIMIT=40]-->
+ <string name="lockscreen_wallpaper_preview_card_content_description" msgid="8575577284424318765">Lockscreen wallpaper preview</string>
+
</resources>
diff --git a/src/com/android/wallpaper/model/HubSectionController.java b/src/com/android/wallpaper/model/HubSectionController.java
new file mode 100644
index 0000000..e271254
--- /dev/null
+++ b/src/com/android/wallpaper/model/HubSectionController.java
@@ -0,0 +1,60 @@
+/*
+ * 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 hub.
+ *
+ * @param <T> the {@link SectionView} to create for the section
+ */
+public interface HubSectionController<T extends SectionView> {
+
+ /** Interface for Customization hub section navigation. */
+ interface HubSectionNavigationController {
+ /** Navigates to the given {@code fragment}. */
+ void navigateTo(Fragment fragment);
+ }
+
+ /** Interface for Customization hub section's dark mode responding to battery saver. */
+ interface HubSectionBatterySaverListener {
+ /** Callback when battery saver's state changed with given {@code isEnabled}. */
+ void onBatterySaverStateChanged(boolean isEnabled);
+ }
+
+ /** Returns {@code true} if the Customization hub 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
+ */
+ T createView(Context context);
+
+ /** Saves the view state for configuration changes. */
+ default void onSaveInstanceState(Bundle savedInstanceState) {}
+
+ /** Releases the controller. */
+ default void release() {}
+}
diff --git a/src/com/android/wallpaper/model/PermissionRequester.java b/src/com/android/wallpaper/model/PermissionRequester.java
new file mode 100644
index 0000000..e15d588
--- /dev/null
+++ b/src/com/android/wallpaper/model/PermissionRequester.java
@@ -0,0 +1,30 @@
+/*
+ * 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 com.android.wallpaper.picker.MyPhotosStarter;
+
+/**
+ * The interface for a class that can request the permission.
+ */
+public interface PermissionRequester {
+ /**
+ * Requests the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission.
+ *
+ * @param listener the listener to be notified of permissions grant status changes
+ */
+ void requestExternalStoragePermission(MyPhotosStarter.PermissionChangedListener listener);
+}
diff --git a/src/com/android/wallpaper/model/WallpaperColorsViewModel.kt b/src/com/android/wallpaper/model/WallpaperColorsViewModel.kt
new file mode 100644
index 0000000..cecadb1
--- /dev/null
+++ b/src/com/android/wallpaper/model/WallpaperColorsViewModel.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.app.WallpaperColors
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+/**
+ * ViewModel class to keep track of WallpaperColors for the current wallpaper
+ */
+class WallpaperColorsViewModel : ViewModel() {
+
+ /**
+ * WallpaperColors for the currently set home wallpaper
+ */
+ val homeWallpaperColors: MutableLiveData<WallpaperColors> by lazy {
+ MutableLiveData<WallpaperColors>()
+ }
+
+ /**
+ * WallpaperColors for the currently set lock wallpaper
+ */
+ val lockWallpaperColors: MutableLiveData<WallpaperColors> by lazy {
+ MutableLiveData<WallpaperColors>()
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/wallpaper/model/WallpaperPreviewNavigator.java b/src/com/android/wallpaper/model/WallpaperPreviewNavigator.java
new file mode 100644
index 0000000..bf1642a
--- /dev/null
+++ b/src/com/android/wallpaper/model/WallpaperPreviewNavigator.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.android.wallpaper.model.WallpaperInfo;
+
+/**
+ * Navigates to a wallpaper preview activity.
+ */
+public interface WallpaperPreviewNavigator {
+ /**
+ * Navigates full screen wallpaper preview
+ *
+ * @param wallpaperInfo the wallpaper to preview
+ * @param isViewAsHome {@code true} if it's home wallpaper preview, {@code false} if it's
+ * lock screen wallpaper preview
+ */
+ void showViewOnlyPreview(WallpaperInfo wallpaperInfo, boolean isViewAsHome);
+}
diff --git a/src/com/android/wallpaper/model/WallpaperSectionController.java b/src/com/android/wallpaper/model/WallpaperSectionController.java
new file mode 100644
index 0000000..07f1eb4
--- /dev/null
+++ b/src/com/android/wallpaper/model/WallpaperSectionController.java
@@ -0,0 +1,533 @@
+/*
+ * 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 static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+
+import android.animation.Animator;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.WallpaperColors;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.service.wallpaper.WallpaperService;
+import android.view.LayoutInflater;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.cardview.widget.CardView;
+import androidx.core.widget.ContentLoadingProgressBar;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
+
+import com.android.wallpaper.R;
+import com.android.wallpaper.asset.Asset;
+import com.android.wallpaper.asset.BitmapCachingAsset;
+import com.android.wallpaper.module.CurrentWallpaperInfoFactory;
+import com.android.wallpaper.module.InjectorProvider;
+import com.android.wallpaper.module.UserEventLogger;
+import com.android.wallpaper.picker.CategorySelectorFragment;
+import com.android.wallpaper.picker.MyPhotosStarter;
+import com.android.wallpaper.picker.WallpaperSectionView;
+import com.android.wallpaper.picker.WorkspaceSurfaceHolderCallback;
+import com.android.wallpaper.util.ResourceUtils;
+import com.android.wallpaper.util.WallpaperConnection;
+import com.android.wallpaper.util.WallpaperSurfaceCallback;
+import com.android.wallpaper.widget.LockScreenPreviewer2;
+
+/** The class to control the wallpaper section view. */
+public class WallpaperSectionController implements HubSectionController<WallpaperSectionView>,
+ LifecycleObserver {
+
+ private static final String PERMISSION_READ_WALLPAPER_INTERNAL =
+ "android.permission.READ_WALLPAPER_INTERNAL";
+ private static final int SETTINGS_APP_INFO_REQUEST_CODE = 1;
+
+ private CardView mHomePreviewCard;
+ private ContentLoadingProgressBar mHomePreviewProgress;
+ private SurfaceView mWorkspaceSurface;
+ private WorkspaceSurfaceHolderCallback mWorkspaceSurfaceCallback;
+ private SurfaceView mHomeWallpaperSurface;
+ private WallpaperSurfaceCallback mHomeWallpaperSurfaceCallback;
+ private SurfaceView mLockWallpaperSurface;
+ private WallpaperSurfaceCallback mLockWallpaperSurfaceCallback;
+ private CardView mLockscreenPreviewCard;
+ private ViewGroup mLockPreviewContainer;
+ private ContentLoadingProgressBar mLockscreenPreviewProgress;
+ private WallpaperConnection mWallpaperConnection;
+
+ // The wallpaper information which is currently shown on the home preview.
+ private WallpaperInfo mHomePreviewWallpaperInfo;
+ // The wallpaper information which is currently shown on the lock preview.
+ private WallpaperInfo mLockPreviewWallpaperInfo;
+
+ private LockScreenPreviewer2 mLockScreenPreviewer;
+
+ private final Activity mActivity;
+ private final Context mAppContext;
+ private final LifecycleOwner mLifecycleOwner;
+ private final PermissionRequester mPermissionRequester;
+ private final WallpaperColorsViewModel mWallpaperColorsViewModel;
+ private final WorkspaceViewModel mWorkspaceViewModel;
+ private final HubSectionNavigationController mHubSectionNavigationController;
+ private final WallpaperPreviewNavigator mWallpaperPreviewNavigator;
+ private final Bundle mSavedInstanceState;
+
+ public WallpaperSectionController(Activity activity, LifecycleOwner lifecycleOwner,
+ PermissionRequester permissionRequester, WallpaperColorsViewModel colorsViewModel,
+ WorkspaceViewModel workspaceViewModel,
+ HubSectionNavigationController hubSectionNavigationController,
+ WallpaperPreviewNavigator wallpaperPreviewNavigator,
+ Bundle savedInstanceState) {
+ mActivity = activity;
+ mLifecycleOwner = lifecycleOwner;
+ mPermissionRequester = permissionRequester;
+ mAppContext = mActivity.getApplicationContext();
+ mWallpaperColorsViewModel = colorsViewModel;
+ mWorkspaceViewModel = workspaceViewModel;
+ mHubSectionNavigationController = hubSectionNavigationController;
+ mWallpaperPreviewNavigator = wallpaperPreviewNavigator;
+ mSavedInstanceState = savedInstanceState;
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ @MainThread
+ public void onResume() {
+ refreshCurrentWallpapers(/* forceRefresh= */ mSavedInstanceState == null);
+ if (mWallpaperConnection != null) {
+ mWallpaperConnection.setVisibility(true);
+ }
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ @MainThread
+ public void onPause() {
+ if (mWallpaperConnection != null) {
+ mWallpaperConnection.setVisibility(false);
+ }
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+ @MainThread
+ public void onStop() {
+ if (mWallpaperConnection != null) {
+ mWallpaperConnection.disconnect();
+ mWallpaperConnection = null;
+ }
+ }
+
+ @Override
+ public boolean isAvailable(Context context) {
+ return true;
+ }
+
+ @Override
+ public WallpaperSectionView createView(Context context) {
+ WallpaperSectionView wallpaperSectionView = (WallpaperSectionView) LayoutInflater.from(
+ context).inflate(R.layout.wallpaper_section_view, /* root= */ null);
+ mHomePreviewCard = wallpaperSectionView.findViewById(R.id.home_preview);
+ // TODO(santie) completely remove wallpaper_preview_image once we get rid of the old ui
+ mHomePreviewCard.findViewById(R.id.wallpaper_preview_image).setVisibility(View.GONE);
+ mHomePreviewCard.setContentDescription(mAppContext.getString(
+ R.string.wallpaper_preview_card_content_description));
+ mWorkspaceSurface = mHomePreviewCard.findViewById(R.id.workspace_surface);
+ mHomePreviewProgress = mHomePreviewCard.findViewById(R.id.wallpaper_preview_spinner);
+ mWorkspaceSurfaceCallback = new WorkspaceSurfaceHolderCallback(
+ mWorkspaceSurface, mAppContext);
+ mHomeWallpaperSurface = mHomePreviewCard.findViewById(R.id.wallpaper_surface);
+ mHomeWallpaperSurfaceCallback = new WallpaperSurfaceCallback(mActivity, mHomePreviewCard,
+ mHomeWallpaperSurface, () -> {
+ if (mHomePreviewWallpaperInfo != null) {
+ maybeLoadThumbnail(mHomePreviewWallpaperInfo, mHomeWallpaperSurfaceCallback);
+ }
+ });
+
+ mLockscreenPreviewCard = wallpaperSectionView.findViewById(R.id.lock_preview);
+ mLockscreenPreviewCard.setContentDescription(mAppContext.getString(
+ R.string.lockscreen_wallpaper_preview_card_content_description));
+ mLockscreenPreviewProgress = mLockscreenPreviewCard.findViewById(
+ R.id.wallpaper_preview_spinner);
+ mLockscreenPreviewCard.findViewById(R.id.wallpaper_preview_image).setVisibility(View.GONE);
+ mLockscreenPreviewCard.findViewById(R.id.workspace_surface).setVisibility(View.GONE);
+ mLockWallpaperSurface = mLockscreenPreviewCard.findViewById(R.id.wallpaper_surface);
+ mLockWallpaperSurfaceCallback = new WallpaperSurfaceCallback(mActivity,
+ mLockscreenPreviewCard, mLockWallpaperSurface, () -> {
+ if (mLockPreviewWallpaperInfo != null) {
+ maybeLoadThumbnail(mLockPreviewWallpaperInfo, mLockWallpaperSurfaceCallback);
+ }
+ });
+ mLockPreviewContainer = mLockscreenPreviewCard.findViewById(
+ R.id.lock_screen_preview_container);
+ mLockPreviewContainer.setVisibility(View.INVISIBLE);
+ mLockScreenPreviewer = new LockScreenPreviewer2(mLifecycleOwner.getLifecycle(), context,
+ mLockPreviewContainer);
+
+ setupCurrentWallpaperPreview(wallpaperSectionView);
+ final int shortDuration = mAppContext.getResources().getInteger(
+ android.R.integer.config_shortAnimTime);
+ fadeWallpaperPreview(true, shortDuration);
+ mLifecycleOwner.getLifecycle().addObserver(this);
+ updateWallpaperSurface();
+ updateWorkspaceSurface();
+
+ wallpaperSectionView.findViewById(R.id.wallpaper_picker_entry).setOnClickListener(
+ v -> mHubSectionNavigationController.navigateTo(new CategorySelectorFragment()));
+
+ mWorkspaceViewModel.getUpdateWorkspace().observe(mLifecycleOwner, update ->
+ updateWorkspacePreview(mWorkspaceSurface, mWorkspaceSurfaceCallback,
+ mWallpaperColorsViewModel.getHomeWallpaperColors().getValue())
+ );
+
+ return wallpaperSectionView;
+ }
+
+ private void updateWorkspacePreview(SurfaceView workspaceSurface,
+ WorkspaceSurfaceHolderCallback callback, @Nullable WallpaperColors colors) {
+ // Reattach SurfaceView to trigger #surfaceCreated to update preview for different option.
+ ViewGroup parent = (ViewGroup) workspaceSurface.getParent();
+ int viewIndex = parent.indexOfChild(workspaceSurface);
+ parent.removeView(workspaceSurface);
+ if (callback != null) {
+ callback.resetLastSurface();
+ callback.setWallpaperColors(colors);
+ }
+ parent.addView(workspaceSurface, viewIndex);
+ }
+
+ @Override
+ public void release() {
+ if (mLockScreenPreviewer != null) {
+ mLockScreenPreviewer.release();
+ mLockScreenPreviewer = null;
+ }
+ if (mHomeWallpaperSurfaceCallback != null) {
+ mHomeWallpaperSurfaceCallback.cleanUp();
+ }
+ if (mLockWallpaperSurfaceCallback != null) {
+ mLockWallpaperSurfaceCallback.cleanUp();
+ }
+ if (mWorkspaceSurfaceCallback != null) {
+ mWorkspaceSurfaceCallback.cleanUp();
+ }
+ mLifecycleOwner.getLifecycle().removeObserver(this);
+ }
+
+ private void setupCurrentWallpaperPreview(View rootView) {
+ if (canShowCurrentWallpaper()) {
+ showCurrentWallpaper(rootView, true);
+ } else {
+ showCurrentWallpaper(rootView, false);
+
+ Button mAllowAccessButton = rootView
+ .findViewById(R.id.permission_needed_allow_access_button);
+ mAllowAccessButton.setOnClickListener(view ->
+ mPermissionRequester.requestExternalStoragePermission(
+ new MyPhotosStarter.PermissionChangedListener() {
+
+ @Override
+ public void onPermissionsGranted() {
+ showCurrentWallpaper(rootView, true);
+ }
+
+ @Override
+ public void onPermissionsDenied(boolean dontAskAgain) {
+ if (!dontAskAgain) {
+ return;
+ }
+ showPermissionNeededDialog();
+ }
+ })
+ );
+
+ // Replace explanation text with text containing the Wallpapers app name which replaces
+ // the placeholder.
+ Resources resources = mAppContext.getResources();
+ String appName = resources.getString(R.string.app_name);
+ String explanation = resources.getString(R.string.permission_needed_explanation,
+ appName);
+ TextView explanationView = rootView.findViewById(R.id.permission_needed_explanation);
+ explanationView.setText(explanation);
+ }
+ }
+
+ private boolean canShowCurrentWallpaper() {
+ return isPermissionGranted(mAppContext, PERMISSION_READ_WALLPAPER_INTERNAL)
+ || isPermissionGranted(mAppContext, READ_EXTERNAL_STORAGE);
+ }
+
+ private boolean isPermissionGranted(Context context, String permission) {
+ return context.getPackageManager().checkPermission(permission,
+ context.getPackageName()) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private void showCurrentWallpaper(View rootView, boolean show) {
+ rootView.findViewById(R.id.home_preview)
+ .setVisibility(show ? View.VISIBLE : View.GONE);
+ rootView.findViewById(R.id.lock_preview)
+ .setVisibility(show ? View.VISIBLE : View.GONE);
+ rootView.findViewById(R.id.permission_needed)
+ .setVisibility(show ? View.GONE : View.VISIBLE);
+ }
+
+ private void showPermissionNeededDialog() {
+ String permissionNeededMessage = mAppContext.getResources().getString(
+ R.string.permission_needed_explanation_go_to_settings);
+ AlertDialog dialog = new AlertDialog.Builder(mAppContext, R.style.LightDialogTheme)
+ .setMessage(permissionNeededMessage)
+ .setPositiveButton(android.R.string.ok, /* onClickListener= */ null)
+ .setNegativeButton(
+ R.string.settings_button_label,
+ (dialogInterface, i) -> {
+ Intent appInfoIntent = new Intent();
+ appInfoIntent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ Uri uri = Uri.fromParts("package",
+ mAppContext.getPackageName(), /* fragment= */ null);
+ appInfoIntent.setData(uri);
+ mActivity.startActivityForResult(appInfoIntent,
+ SETTINGS_APP_INFO_REQUEST_CODE);
+ })
+ .create();
+ dialog.show();
+ }
+
+ /**
+ * Obtains the {@link WallpaperInfo} object(s) representing the wallpaper(s) currently set to
+ * the device from the {@link CurrentWallpaperInfoFactory}.
+ */
+ private void refreshCurrentWallpapers(boolean forceRefresh) {
+ CurrentWallpaperInfoFactory factory = InjectorProvider.getInjector()
+ .getCurrentWallpaperFactory(mAppContext);
+
+ factory.createCurrentWallpaperInfos(
+ (homeWallpaper, lockWallpaper, presentationMode) -> {
+ // A config change may have destroyed the activity since the refresh
+ // started, so check for that.
+ if (!isActivityAlive()) {
+ return;
+ }
+
+ mHomePreviewWallpaperInfo = homeWallpaper;
+ mLockPreviewWallpaperInfo =
+ lockWallpaper == null ? homeWallpaper : lockWallpaper;
+
+ updatePreview(mHomePreviewWallpaperInfo, true);
+ updatePreview(mLockPreviewWallpaperInfo, false);
+
+ WallpaperManager manager = WallpaperManager.getInstance(mAppContext);
+
+ WallpaperColors homeColors =
+ manager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
+ onHomeWallpaperColorsChanged(homeColors);
+ WallpaperColors lockColors = homeColors;
+
+ if (lockWallpaper != null) {
+ lockColors = manager.getWallpaperColors(WallpaperManager.FLAG_LOCK);
+
+ }
+ onLockWallpaperColorsChanged(lockColors);
+ }, forceRefresh);
+ }
+
+ private void updatePreview(WallpaperInfo wallpaperInfo, boolean isHomeWallpaper) {
+ if (wallpaperInfo == null) {
+ return;
+ }
+
+ if (!isActivityAlive()) {
+ return;
+ }
+
+ UserEventLogger eventLogger = InjectorProvider.getInjector().getUserEventLogger(
+ mAppContext);
+
+ WallpaperSurfaceCallback surfaceCallback = isHomeWallpaper
+ ? mHomeWallpaperSurfaceCallback : mLockWallpaperSurfaceCallback;
+ // Load thumb regardless of live wallpaper to make sure we have a placeholder while
+ // the live wallpaper initializes in that case.
+ Asset thumbAsset = maybeLoadThumbnail(wallpaperInfo, surfaceCallback);
+
+ if (isHomeWallpaper) {
+ if (mWallpaperConnection != null) {
+ mWallpaperConnection.disconnect();
+ mWallpaperConnection = null;
+ }
+ if (wallpaperInfo instanceof LiveWallpaperInfo) {
+ setUpLiveWallpaperPreview(wallpaperInfo);
+ }
+ }
+
+ View preview = isHomeWallpaper ? mHomePreviewCard : mLockscreenPreviewCard;
+ preview.setOnClickListener(view -> {
+ mWallpaperPreviewNavigator.showViewOnlyPreview(wallpaperInfo, isHomeWallpaper);
+ eventLogger.logCurrentWallpaperPreviewed();
+ });
+ }
+
+ @NonNull
+ private Asset maybeLoadThumbnail(WallpaperInfo wallpaperInfo,
+ WallpaperSurfaceCallback surfaceCallback) {
+ ImageView imageView = surfaceCallback.getHomeImageWallpaper();
+ Asset thumbAsset = new BitmapCachingAsset(mAppContext,
+ wallpaperInfo.getThumbAsset(mAppContext));
+ if (imageView != null && imageView.getDrawable() == null) {
+ thumbAsset.loadPreviewImage(mActivity, imageView,
+ ResourceUtils.getColorAttr(mActivity, android.R.attr.colorSecondary));
+ }
+ return thumbAsset;
+ }
+
+ private void onHomeWallpaperColorsChanged(WallpaperColors wallpaperColors) {
+ if (wallpaperColors != null && wallpaperColors.equals(
+ mWallpaperColorsViewModel.getHomeWallpaperColors().getValue())) {
+ return;
+ }
+ mWallpaperColorsViewModel.getHomeWallpaperColors().setValue(wallpaperColors);
+ }
+
+ private void onLockWallpaperColorsChanged(WallpaperColors wallpaperColors) {
+ if (wallpaperColors != null && wallpaperColors.equals(
+ mWallpaperColorsViewModel.getLockWallpaperColors().getValue())) {
+ return;
+ }
+ mWallpaperColorsViewModel.getLockWallpaperColors().setValue(wallpaperColors);
+ if (mLockScreenPreviewer != null) {
+ mLockScreenPreviewer.setColor(wallpaperColors);
+ }
+ }
+
+ private void setUpLiveWallpaperPreview(WallpaperInfo homeWallpaper) {
+ if (!isActivityAlive()) {
+ return;
+ }
+
+ if (WallpaperConnection.isPreviewAvailable()) {
+ final boolean isLockLive = mLockPreviewWallpaperInfo instanceof LiveWallpaperInfo;
+ mWallpaperConnection = new WallpaperConnection(
+ getWallpaperIntent(homeWallpaper.getWallpaperComponent()), mActivity,
+ new WallpaperConnection.WallpaperConnectionListener() {
+ @Override
+ public void onWallpaperColorsChanged(WallpaperColors colors,
+ int displayId) {
+ if (isLockLive && mLockScreenPreviewer != null) {
+ mLockScreenPreviewer.setColor(colors);
+ onLockWallpaperColorsChanged(colors);
+ }
+ onHomeWallpaperColorsChanged(colors);
+ }
+ },
+ mHomeWallpaperSurface, isLockLive ? mLockWallpaperSurface : null);
+
+ mWallpaperConnection.setVisibility(true);
+ mHomeWallpaperSurface.post(() -> {
+ if (mWallpaperConnection != null && !mWallpaperConnection.connect()) {
+ mWallpaperConnection = null;
+ }
+ });
+ }
+ }
+
+ private Intent getWallpaperIntent(android.app.WallpaperInfo info) {
+ return new Intent(WallpaperService.SERVICE_INTERFACE)
+ .setClassName(info.getPackageName(), info.getServiceName());
+ }
+
+ private void updateWallpaperSurface() {
+ mHomeWallpaperSurface.getHolder().addCallback(mHomeWallpaperSurfaceCallback);
+ mHomeWallpaperSurface.setZOrderMediaOverlay(true);
+ mLockWallpaperSurface.getHolder().addCallback(mLockWallpaperSurfaceCallback);
+ mLockWallpaperSurface.setZOrderMediaOverlay(true);
+ }
+
+ private void updateWorkspaceSurface() {
+ mWorkspaceSurface.setZOrderMediaOverlay(true);
+ mWorkspaceSurface.getHolder().addCallback(mWorkspaceSurfaceCallback);
+ }
+
+ private boolean isActivityAlive() {
+ return !mActivity.isDestroyed() && !mActivity.isFinishing();
+ }
+
+ private void fadeWallpaperPreview(boolean isFadeIn, int duration) {
+ setupFade(mHomePreviewCard, mHomePreviewProgress, duration, isFadeIn);
+ setupFade(mLockscreenPreviewCard, mLockscreenPreviewProgress, duration, isFadeIn);
+ }
+
+ private void setupFade(CardView cardView, ContentLoadingProgressBar progressBar, int duration,
+ boolean fadeIn) {
+ cardView.setAlpha(fadeIn ? 0.0f : 1.0f);
+ cardView.animate()
+ .alpha(fadeIn ? 1.0f : 0.0f)
+ .setDuration(duration)
+ .setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ progressBar.hide();
+ setWallpaperPreviewsVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ progressBar.hide();
+ setWallpaperPreviewsVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationStart(Animator animator) {
+ setWallpaperPreviewsVisibility(View.INVISIBLE);
+ }
+ });
+ progressBar.animate()
+ .alpha(fadeIn ? 1.0f : 0.0f)
+ .setDuration(duration * 2)
+ .setStartDelay(duration)
+ .withStartAction(progressBar::show)
+ .withEndAction(progressBar::hide);
+ }
+
+ private void setWallpaperPreviewsVisibility(int visibility) {
+ if (mHomeWallpaperSurface != null) {
+ mHomeWallpaperSurface.setVisibility(visibility);
+ }
+ if (mLockWallpaperSurface != null) {
+ mLockWallpaperSurface.setVisibility(visibility);
+ }
+ if (mWorkspaceSurface != null) {
+ mWorkspaceSurface.setVisibility(visibility);
+ }
+ if (mLockPreviewContainer != null) {
+ mLockPreviewContainer.setVisibility(visibility);
+ }
+ }
+}
diff --git a/src/com/android/wallpaper/model/WorkspaceViewModel.kt b/src/com/android/wallpaper/model/WorkspaceViewModel.kt
new file mode 100644
index 0000000..bafd6aa
--- /dev/null
+++ b/src/com/android/wallpaper/model/WorkspaceViewModel.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+/** ViewModel class to keep track of workspace updates. */
+class WorkspaceViewModel : ViewModel() {
+
+ /**
+ * 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>()
+ }
+}
diff --git a/src/com/android/wallpaper/picker/SectionView.java b/src/com/android/wallpaper/picker/SectionView.java
new file mode 100644
index 0000000..31a2014
--- /dev/null
+++ b/src/com/android/wallpaper/picker/SectionView.java
@@ -0,0 +1,54 @@
+/*
+ * 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.picker;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+/**
+ * The SectionView base for views hosting in the {@link
+ * com.android.customization.picker.hub.HubFragment}.
+ */
+public abstract class SectionView extends LinearLayout {
+
+ /** The callback for the section view updates. */
+ public interface SectionViewListener {
+ void onViewActivated(@Nullable Context context, boolean viewActivated);
+ }
+
+ protected SectionViewListener mSectionViewListener;
+ private String mTitle;
+
+ public SectionView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setTitle(String title) {
+ mTitle = title;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /** Sets the listener to the {@code SectionView} instance for reacting the view changes. */
+ public void setViewListener(SectionViewListener sectionViewListener) {
+ mSectionViewListener = sectionViewListener;
+ }
+}
diff --git a/src/com/android/wallpaper/picker/WallpaperSectionView.java b/src/com/android/wallpaper/picker/WallpaperSectionView.java
new file mode 100644
index 0000000..1d20e08
--- /dev/null
+++ b/src/com/android/wallpaper/picker/WallpaperSectionView.java
@@ -0,0 +1,73 @@
+/*
+ * 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.picker;
+
+import android.app.Activity;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+
+import androidx.cardview.widget.CardView;
+
+import com.android.wallpaper.picker.SectionView;
+import com.android.wallpaper.R;
+import com.android.wallpaper.util.ScreenSizeCalculator;
+import com.android.wallpaper.util.SizeCalculator;
+
+/**
+ * The wallpaper section view in the Customization Hub fragment.
+ */
+public final class WallpaperSectionView extends SectionView {
+
+ private CardView mHomePreviewCard;
+ private CardView mLockscreenPreviewCard;
+
+ public WallpaperSectionView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ matchDeviceShape(mHomePreviewCard);
+ matchDeviceShape(mLockscreenPreviewCard);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mHomePreviewCard = findViewById(R.id.home_preview);
+ mLockscreenPreviewCard = findViewById(R.id.lock_preview);
+
+ // Disable the shadows of these card views.
+ mHomePreviewCard.setCardElevation(0);
+ mLockscreenPreviewCard.setCardElevation(0);
+ }
+
+ private void matchDeviceShape(CardView cardView) {
+ // Match device aspect ratio
+ float screenAspectRatio =
+ ScreenSizeCalculator.getInstance().getScreenAspectRatio(getContext());
+ int cardWidth = cardView.getMeasuredWidth();
+ int cardHeight = (int) (cardWidth * screenAspectRatio);
+ ViewGroup.LayoutParams layoutParams = cardView.getLayoutParams();
+ layoutParams.height = cardHeight;
+
+ // Match device corner
+ cardView.setRadius(
+ SizeCalculator.getPreviewCornerRadius((Activity) getContext(), cardWidth));
+ }
+}