/* * 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. */ package com.android.wallpaper.picker; import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; import static com.android.wallpaper.widget.BottomActionBar.BottomAction.APPLY; import static com.android.wallpaper.widget.BottomActionBar.BottomAction.CUSTOMIZE; import static com.android.wallpaper.widget.BottomActionBar.BottomAction.DELETE; import static com.android.wallpaper.widget.BottomActionBar.BottomAction.EDIT; import static com.android.wallpaper.widget.BottomActionBar.BottomAction.INFORMATION; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.WallpaperColors; import android.app.WallpaperInfo; import android.app.WallpaperManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.service.wallpaper.IWallpaperConnection; import android.service.wallpaper.WallpaperService; import android.service.wallpaper.WallpaperSettingsActivity; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.cardview.widget.CardView; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.lifecycle.LiveData; import androidx.slice.Slice; import androidx.slice.widget.SliceLiveData; import androidx.slice.widget.SliceView; import com.android.wallpaper.R; import com.android.wallpaper.compat.BuildCompat; import com.android.wallpaper.model.SetWallpaperViewModel; import com.android.wallpaper.util.FullScreenAnimation; import com.android.wallpaper.util.ResourceUtils; import com.android.wallpaper.util.ScreenSizeCalculator; import com.android.wallpaper.util.SizeCalculator; import com.android.wallpaper.util.WallpaperConnection; import com.android.wallpaper.util.WallpaperSurfaceCallback; import com.android.wallpaper.widget.BottomActionBar; import com.android.wallpaper.widget.BottomActionBar.AccessibilityCallback; import com.android.wallpaper.widget.BottomActionBar.BottomSheetContent; import com.android.wallpaper.widget.LockScreenPreviewer; import com.android.wallpaper.widget.WallpaperColorsLoader; import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * Fragment which displays the UI for previewing an individual live wallpaper, its attribution * information and settings slices if available. */ public class LivePreviewFragment extends PreviewFragment implements WallpaperConnection.WallpaperConnectionListener { public static final String EXTRA_LIVE_WALLPAPER_INFO = "android.live_wallpaper.info"; public static final String KEY_ACTION_DELETE_LIVE_WALLPAPER = "action_delete_live_wallpaper"; private static final String TAG = "LivePreviewFragment"; /** * Instance of {@link WallpaperConnection} used to bind to the live wallpaper service to show * it in this preview fragment. * @see IWallpaperConnection */ protected WallpaperConnection mWallpaperConnection; protected CardView mHomePreviewCard; protected SurfaceView mWorkspaceSurface; protected WallpaperSurfaceCallback mWallpaperSurfaceCallback; protected WorkspaceSurfaceHolderCallback mWorkspaceSurfaceCallback; protected ViewGroup mLockPreviewContainer; protected LockScreenPreviewer mLockScreenPreviewer; private Intent mDeleteIntent; private Intent mSettingsIntent; private SliceView mSettingsSliceView; private LiveData mSettingsLiveData; private Point mScreenSize; private ViewGroup mPreviewContainer; private TouchForwardingLayout mTouchForwardingLayout; private SurfaceView mWallpaperSurface; private Future mPlaceholderColorFuture; private WallpaperColors mWallpaperColors; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); android.app.WallpaperInfo info = mWallpaper.getWallpaperComponent(); mPlaceholderColorFuture = mWallpaper.computePlaceholderColor(getContext()); String deleteAction = getDeleteAction(info); if (!TextUtils.isEmpty(deleteAction)) { mDeleteIntent = new Intent(deleteAction); mDeleteIntent.setPackage(info.getPackageName()); mDeleteIntent.putExtra(EXTRA_LIVE_WALLPAPER_INFO, info); } String settingsActivity = getSettingsActivity(info); if (settingsActivity != null) { mSettingsIntent = new Intent(); mSettingsIntent.setComponent(new ComponentName(info.getPackageName(), settingsActivity)); mSettingsIntent.putExtra(WallpaperSettingsActivity.EXTRA_PREVIEW_MODE, true); PackageManager pm = requireContext().getPackageManager(); ActivityInfo activityInfo = mSettingsIntent.resolveActivityInfo(pm, 0); if (activityInfo == null) { Log.i(TAG, "Couldn't find wallpaper settings activity: " + settingsActivity); mSettingsIntent = null; } } } @Nullable protected String getSettingsActivity(WallpaperInfo info) { return info.getSettingsActivity(); } protected Intent getWallpaperIntent(WallpaperInfo info) { return new Intent(WallpaperService.SERVICE_INTERFACE) .setClassName(info.getPackageName(), info.getServiceName()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); Activity activity = requireActivity(); mScreenSize = ScreenSizeCalculator.getInstance().getScreenSize( activity.getWindowManager().getDefaultDisplay()); mPreviewContainer = view.findViewById(R.id.live_wallpaper_preview); mTouchForwardingLayout = view.findViewById(R.id.touch_forwarding_layout); // Update preview header color which covers toolbar and status bar area. View previewHeader = view.findViewById(R.id.preview_header); previewHeader.setBackgroundColor(activity.getColor(R.color.settingslib_colorSurfaceHeader)); // Set aspect ratio on the preview card. ConstraintSet set = new ConstraintSet(); set.clone((ConstraintLayout) mPreviewContainer); String ratio = String.format(Locale.US, "%d:%d", mScreenSize.x, mScreenSize.y); set.setDimensionRatio(mTouchForwardingLayout.getId(), ratio); set.applyTo((ConstraintLayout) mPreviewContainer); mHomePreviewCard = mPreviewContainer.findViewById(R.id.wallpaper_full_preview_card); mLockPreviewContainer = mPreviewContainer.findViewById(R.id.lock_screen_preview_container); mLockScreenPreviewer = new LockScreenPreviewer(getLifecycle(), getContext(), mLockPreviewContainer); mLockScreenPreviewer.setDateViewVisibility(!mFullScreenAnimation.isFullScreen()); mFullScreenAnimation.setFullScreenStatusListener( isFullScreen -> { mLockScreenPreviewer.setDateViewVisibility(!isFullScreen); if (!isFullScreen) { mBottomActionBar.focusAccessibilityAction(EDIT); } }); mWallpaperSurface = mHomePreviewCard.findViewById(R.id.wallpaper_surface); mTouchForwardingLayout.setTargetView(mHomePreviewCard); mTouchForwardingLayout.setForwardingEnabled(true); mWorkspaceSurface = mHomePreviewCard.findViewById(R.id.workspace_surface); mWorkspaceSurfaceCallback = createWorkspaceSurfaceCallback(mWorkspaceSurface); mWallpaperSurfaceCallback = new WallpaperSurfaceCallback(getContext(), mHomePreviewCard, mWallpaperSurface, mPlaceholderColorFuture, new WallpaperSurfaceCallback.SurfaceListener() { @Override public void onSurfaceCreated() { previewLiveWallpaper(null); } }); setUpTabs(view.findViewById(R.id.separated_tabs)); view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View thisView, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { mHomePreviewCard.setRadius(SizeCalculator.getPreviewCornerRadius(activity, mHomePreviewCard.getMeasuredWidth())); view.removeOnLayoutChangeListener(this); } }); return view; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); updateWallpaperSurface(); setupCurrentWallpaperPreview(); renderWorkspaceSurface(); } private void updateWallpaperSurface() { mWallpaperSurface.getHolder().addCallback(mWallpaperSurfaceCallback); mWallpaperSurface.setZOrderMediaOverlay(true); } @Override protected void updateScreenPreview(boolean isHomeSelected) { mWorkspaceSurface.setVisibility(isHomeSelected ? View.VISIBLE : View.INVISIBLE); mLockPreviewContainer.setVisibility(isHomeSelected ? View.INVISIBLE : View.VISIBLE); mFullScreenAnimation.setIsHomeSelected(isHomeSelected); } private void setupCurrentWallpaperPreview() { mHomePreviewCard.setOnTouchListener((v, ev) -> { if (mWallpaperConnection != null && mWallpaperConnection.getEngine() != null) { float scaleRatio = (float) mTouchForwardingLayout.getWidth() / (float) mScreenSize.x; int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { mBottomActionBar.collapseBottomSheetIfExpanded(); } MotionEvent dup = MotionEvent.obtainNoHistory(ev); dup.setLocation(ev.getX() / scaleRatio, ev.getY() / scaleRatio); try { mWallpaperConnection.getEngine().dispatchPointer(dup); if (action == MotionEvent.ACTION_UP) { mWallpaperConnection.getEngine().dispatchWallpaperCommand( WallpaperManager.COMMAND_TAP, (int) ev.getX(), (int) ev.getY(), 0, null); } else if (action == MotionEvent.ACTION_POINTER_UP) { int pointerIndex = ev.getActionIndex(); mWallpaperConnection.getEngine().dispatchWallpaperCommand( WallpaperManager.COMMAND_SECONDARY_TAP, (int) ev.getX(pointerIndex), (int) ev.getY(pointerIndex), 0, null); } } catch (RemoteException e) { Log.e(TAG, "Remote exception of wallpaper connection"); } } return false; }); } @Override public void onDestroyView() { super.onDestroyView(); if (mSettingsLiveData != null && mSettingsLiveData.hasObservers() && mSettingsSliceView != null) { mSettingsLiveData.removeObserver(mSettingsSliceView); mSettingsLiveData = null; } if (mWallpaperConnection != null) { mWallpaperConnection.disconnect(); mWallpaperConnection = null; } if (mLockScreenPreviewer != null) { mLockScreenPreviewer.release(); } mWorkspaceSurfaceCallback.cleanUp(); mWorkspaceSurface.getHolder().removeCallback(mWorkspaceSurfaceCallback); mWallpaperSurfaceCallback.cleanUp(); mWallpaperSurface.getHolder().removeCallback(mWallpaperSurfaceCallback); } protected void previewLiveWallpaper(ImageView thumbnailView) { mWallpaperSurface.post(() -> { Activity activity = getActivity(); if (activity == null) { return; } if (mWallpaperSurfaceCallback.getHomeImageWallpaper() != null) { Integer placeholderColor = null; try { placeholderColor = mPlaceholderColorFuture.get(50, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { Log.i(TAG, "Couldn't obtain placeholder color", e); } mWallpaper.getThumbAsset(activity.getApplicationContext()) .loadLowResDrawable(activity, mWallpaperSurfaceCallback.getHomeImageWallpaper(), placeholderColor != null ? placeholderColor : ResourceUtils.getColorAttr(activity, android.R.attr.colorBackground), mPreviewBitmapTransformation); } setUpLiveWallpaperPreview(mWallpaper); }); } protected void setUpLiveWallpaperPreview( com.android.wallpaper.model.WallpaperInfo homeWallpaper) { Activity activity = getActivity(); if (activity == null || activity.isFinishing()) { return; } if (mWallpaperConnection != null) { mWallpaperConnection.disconnect(); } if (WallpaperConnection.isPreviewAvailable()) { mWallpaperConnection = new WallpaperConnection( getWallpaperIntent(homeWallpaper.getWallpaperComponent()), activity, /* listener= */ this, mWallpaperSurface); mWallpaperConnection.setVisibility(true); } else { WallpaperColorsLoader.getWallpaperColors( activity, homeWallpaper.getThumbAsset(activity), colors -> onWallpaperColorsChanged(colors, 0)); } if (mWallpaperConnection != null && !mWallpaperConnection.connect()) { mWallpaperConnection = null; } } private void renderWorkspaceSurface() { mWorkspaceSurface.setZOrderMediaOverlay(true); mWorkspaceSurface.getHolder().addCallback(mWorkspaceSurfaceCallback); } @Override protected void onBottomActionBarReady(BottomActionBar bottomActionBar) { super.onBottomActionBarReady(bottomActionBar); mBottomActionBar.showActionsOnly(INFORMATION, DELETE, EDIT, CUSTOMIZE, APPLY); mBottomActionBar.setActionClickListener(APPLY, unused -> onSetWallpaperClicked(null)); mBottomActionBar.bindBottomSheetContentWithAction( new WallpaperInfoContent(getContext()), INFORMATION); View separatedTabsContainer = getView().findViewById(R.id.separated_tabs_container); // Update target view's accessibility param since it will be blocked by the bottom sheet // when expanded. mBottomActionBar.setAccessibilityCallback(new AccessibilityCallback() { @Override public void onBottomSheetCollapsed() { mPreviewContainer.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); separatedTabsContainer.setImportantForAccessibility( IMPORTANT_FOR_ACCESSIBILITY_YES); } @Override public void onBottomSheetExpanded() { mPreviewContainer.setImportantForAccessibility( IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); separatedTabsContainer.setImportantForAccessibility( IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } }); final Uri uriSettingsSlice = getSettingsSliceUri(mWallpaper.getWallpaperComponent()); if (uriSettingsSlice != null) { mSettingsLiveData = SliceLiveData.fromUri(requireContext(), uriSettingsSlice); mBottomActionBar.bindBottomSheetContentWithAction( new PreviewCustomizeSettingsContent(getContext()), CUSTOMIZE); } else { if (mSettingsIntent != null) { mBottomActionBar.setActionClickListener(CUSTOMIZE, listener -> startActivity(mSettingsIntent)); } else { mBottomActionBar.hideActions(CUSTOMIZE); } } if (TextUtils.isEmpty(getDeleteAction(mWallpaper.getWallpaperComponent()))) { mBottomActionBar.hideActions(DELETE); } else { mBottomActionBar.setActionClickListener(DELETE, listener -> showDeleteConfirmDialog()); } mBottomActionBar.show(); // Action buttons are disabled when live wallpaper is not loaded. mBottomActionBar.disableActions(); // Enable buttons if loaded, or wait for it. if (isLoaded()) { mBottomActionBar.enableActions(); } } @Override public void onEngineShown() { Activity activity = getActivity(); if (activity == null) { return; } mWallpaperSurfaceCallback.getHomeImageWallpaper().animate() .setStartDelay(250) .setDuration(250) .alpha(0f) .setInterpolator(ALPHA_OUT) .start(); if (mBottomActionBar != null) { mBottomActionBar.enableActions(); } } @Override public void onWallpaperColorsChanged(WallpaperColors colors, int displayId) { mWallpaperColors = colors; mLockScreenPreviewer.setColor(colors); mFullScreenAnimation.setFullScreenTextColor( colors == null || (colors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) == 0 ? FullScreenAnimation.FullScreenTextColor.LIGHT : FullScreenAnimation.FullScreenTextColor.DARK); } @Override protected boolean isLoaded() { return mWallpaperConnection != null && mWallpaperConnection.isEngineReady(); } @SuppressLint("NewApi") //Already checking with isAtLeastQ protected Uri getSettingsSliceUri(android.app.WallpaperInfo info) { if (BuildCompat.isAtLeastQ()) { return info.getSettingsSliceUri(); } return null; } @Override protected int getLayoutResId() { return R.layout.fragment_live_preview; } @Override protected void setCurrentWallpaper(int destination) { mWallpaperSetter.setCurrentWallpaper(getActivity(), mWallpaper, null, destination, 0, null, mWallpaperColors, SetWallpaperViewModel.getCallback(mViewModelProvider)); } @Nullable protected String getDeleteAction(android.app.WallpaperInfo wallpaperInfo) { android.app.WallpaperInfo currentInfo = WallpaperManager.getInstance(requireContext()).getWallpaperInfo(); ServiceInfo serviceInfo = wallpaperInfo.getServiceInfo(); if (!isPackagePreInstalled(serviceInfo.applicationInfo)) { Log.d(TAG, "This wallpaper is not pre-installed: " + serviceInfo.name); return null; } ServiceInfo currentService = currentInfo == null ? null : currentInfo.getServiceInfo(); // A currently set Live wallpaper should not be deleted. if (currentService != null && TextUtils.equals(serviceInfo.name, currentService.name)) { return null; } final Bundle metaData = serviceInfo.metaData; if (metaData != null) { return metaData.getString(KEY_ACTION_DELETE_LIVE_WALLPAPER); } return null; } @Override public void onResume() { super.onResume(); if (mWallpaperConnection != null) { mWallpaperConnection.setVisibility(true); } } @Override public void onPause() { super.onPause(); if (mWallpaperConnection != null) { mWallpaperConnection.setVisibility(false); } } @Override public void onStop() { super.onStop(); if (mWallpaperConnection != null) { mWallpaperConnection.disconnect(); mWallpaperConnection = null; } } private void showDeleteConfirmDialog() { final AlertDialog alertDialog = new AlertDialog.Builder(getContext()) .setMessage(R.string.delete_wallpaper_confirmation) .setOnDismissListener(dialog -> mBottomActionBar.deselectAction(DELETE)) .setPositiveButton(R.string.delete_live_wallpaper, (dialog, which) -> deleteLiveWallpaper()) .setNegativeButton(android.R.string.cancel, null /* listener */) .create(); alertDialog.show(); } private void deleteLiveWallpaper() { if (mDeleteIntent != null) { requireContext().startService(mDeleteIntent); finishActivity(/* success= */ false); } } private boolean isPackagePreInstalled(ApplicationInfo info) { return info != null && (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; } private final class PreviewCustomizeSettingsContent extends BottomSheetContent { private PreviewCustomizeSettingsContent(Context context) { super(context); } @Override public int getViewId() { return R.layout.preview_customize_settings; } @Override public void onViewCreated(View previewPage) { mSettingsSliceView = previewPage.findViewById(R.id.settings_slice); mSettingsSliceView.setMode(SliceView.MODE_LARGE); mSettingsSliceView.setScrollable(false); if (mSettingsLiveData != null) { mSettingsLiveData.observeForever(mSettingsSliceView); } } @Override public void onRecreateView(View oldPreviewPage) { super.onRecreateView(oldPreviewPage); if (mSettingsLiveData != null && mSettingsLiveData.hasObservers() && mSettingsSliceView != null) { mSettingsLiveData.removeObserver(mSettingsSliceView); } } } }