/* * 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 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.ServiceConnection; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.service.wallpaper.IWallpaperConnection; import android.service.wallpaper.IWallpaperEngine; import android.service.wallpaper.IWallpaperService; import android.service.wallpaper.WallpaperService; import android.service.wallpaper.WallpaperSettingsActivity; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager.LayoutParams; import android.view.animation.AnimationUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.slice.Slice; import androidx.slice.widget.SliceLiveData; import androidx.slice.widget.SliceView; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import com.android.wallpaper.R; import com.android.wallpaper.compat.BuildCompat; import com.android.wallpaper.model.LiveWallpaperInfo; import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback; import com.google.android.material.tabs.TabLayout; import java.util.ArrayList; import java.util.List; /** * Fragment which displays the UI for previewing an individual live wallpaper, its attribution * information and settings slices if available. */ public class LivePreviewFragment extends PreviewFragment { private static final String TAG = "LivePreviewFragment"; private static final String KEY_ACTION_DELETE_LIVE_WALLPAPER = "action_delete_live_wallpaper"; private static final String EXTRA_LIVE_WALLPAPER_INFO = "android.live_wallpaper.info"; /** * Instance of {@link WallpaperConnection} used to bind to the live wallpaper service to show * it in this preview fragment. * @see IWallpaperConnection */ protected WallpaperConnection mWallpaperConnection; private Intent mWallpaperIntent; private Intent mDeleteIntent; private Intent mSettingsIntent; private List> mPages; private ViewPager mViewPager; private TabLayout mTabLayout; private SliceView mSettingsSliceView; private LiveData mSettingsLiveData; private View mLoadingScrim; private InfoPageController mInfoPageController; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); android.app.WallpaperInfo info = mWallpaper.getWallpaperComponent(); mWallpaperIntent = getWallpaperIntent(info); setUpExploreIntent(null); android.app.WallpaperInfo currentWallpaper = WallpaperManager.getInstance(requireContext()).getWallpaperInfo(); String deleteAction = getDeleteAction(info, currentWallpaper); 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) { mPages = new ArrayList<>(); View view = super.onCreateView(inflater, container, savedInstanceState); if (view == null) { return null; } Activity activity = requireActivity(); mLoadingScrim = view.findViewById(R.id.loading); setUpLoadingIndicator(); mWallpaperConnection = new WallpaperConnection(mWallpaperIntent, activity, getWallpaperConnectionListener()); container.post(() -> { if (!mWallpaperConnection.connect()) { mWallpaperConnection = null; } }); return view; } @Override public void onDestroyView() { super.onDestroyView(); if (mSettingsLiveData != null && mSettingsLiveData.hasObservers()) { mSettingsLiveData.removeObserver(mSettingsSliceView); mSettingsLiveData = null; } if (mWallpaperConnection != null) { mWallpaperConnection.disconnect(); } mWallpaperConnection = null; super.onDestroy(); } @Override protected void setUpBottomSheetView(ViewGroup bottomSheet) { initInfoPage(); initSettingsPage(); mViewPager = bottomSheet.findViewById(R.id.viewpager); mTabLayout = bottomSheet.findViewById(R.id.tablayout); // Create PagerAdapter final PagerAdapter pagerAdapter = new PagerAdapter() { @Override public Object instantiateItem(ViewGroup container, int position) { final View page = mPages.get(position).second; container.addView(page); return page; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { if (object instanceof View) { container.removeView((View) object); } } @Override public int getCount() { return mPages.size(); } @Override public CharSequence getPageTitle(int position) { try { return mPages.get(position).first; } catch (IndexOutOfBoundsException e) { return null; } } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return (view == object); } }; // Add OnPageChangeListener to re-measure ViewPager's height mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { mViewPager.requestLayout(); } }); // Set PagerAdapter mViewPager.setAdapter(pagerAdapter); // Make TabLayout visible if there are more than one page if (mPages.size() > 1) { mTabLayout.setVisibility(View.VISIBLE); mTabLayout.setupWithViewPager(mViewPager); } mViewPager.setCurrentItem(0); } protected WallpaperConnectionListener getWallpaperConnectionListener() { return null; } @Override protected boolean isLoaded() { return mWallpaperConnection != null && mWallpaperConnection.isEngineReady(); } private void initInfoPage() { View pageInfo = InfoPageController.createView(getLayoutInflater()); mInfoPageController = new InfoPageController(pageInfo, mPreviewMode); mPages.add(Pair.create(getString(R.string.tab_info), pageInfo)); } private void initSettingsPage() { final Uri uriSettingsSlice = getSettingsSliceUri(mWallpaper.getWallpaperComponent()); if (uriSettingsSlice == null) { return; } final View pageSettings = getLayoutInflater().inflate(R.layout.preview_page_settings, null /* root */); mSettingsSliceView = pageSettings.findViewById(R.id.settings_slice); mSettingsSliceView.setMode(SliceView.MODE_LARGE); mSettingsSliceView.setScrollable(false); // Set LiveData for SliceView mSettingsLiveData = SliceLiveData.fromUri(requireContext() /* context */, uriSettingsSlice); mSettingsLiveData.observeForever(mSettingsSliceView); pageSettings.findViewById(R.id.preview_settings_pane_set_wallpaper_button) .setOnClickListener(this::onSetWallpaperClicked); mPages.add(Pair.create(getResources().getString(R.string.tab_customize), pageSettings)); } @Override protected CharSequence getExploreButtonLabel(Context context) { CharSequence exploreLabel = ((LiveWallpaperInfo) mWallpaper).getActionDescription(context); if (TextUtils.isEmpty(exploreLabel)) { exploreLabel = context.getString(mWallpaper.getActionLabelRes(context)); } return exploreLabel; } @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 int getBottomSheetResId() { return R.id.bottom_sheet; } @Override protected int getLoadingIndicatorResId() { return R.id.loading_indicator; } @Override protected void setCurrentWallpaper(int destination) { mWallpaperSetter.setCurrentWallpaper(getActivity(), mWallpaper, null, destination, 0, null, new SetWallpaperCallback() { @Override public void onSuccess() { finishActivityWithResultOk(); } @Override public void onError(@Nullable Throwable throwable) { showSetWallpaperErrorDialog(destination); } }); } @Override protected void setBottomSheetContentAlpha(float alpha) { mInfoPageController.setContentAlpha(alpha); } @Nullable protected String getDeleteAction(android.app.WallpaperInfo wallpaperInfo, @Nullable android.app.WallpaperInfo currentInfo) { 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 onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); menu.findItem(R.id.configure).setVisible(mSettingsIntent != null); menu.findItem(R.id.delete_wallpaper).setVisible(mDeleteIntent != null); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.configure) { if (getActivity() != null) { startActivity(mSettingsIntent); return true; } } else if (id == R.id.delete_wallpaper) { showDeleteConfirmDialog(); return true; } return super.onOptionsItemSelected(item); } private void showDeleteConfirmDialog() { final AlertDialog alertDialog = new AlertDialog.Builder( new ContextThemeWrapper(getContext(), getDeviceDefaultTheme())) .setMessage(R.string.delete_wallpaper_confirmation) .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); finishActivityWithResultOk(); } } private boolean isPackagePreInstalled(ApplicationInfo info) { if (info != null && (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { return true; } return false; } /** * Interface to be notified of connect/disconnect events from {@link WallpaperConnection} */ public interface WallpaperConnectionListener { /** * Called after the Wallpaper service has been bound. */ void onConnected(); /** * Called after the Wallpaper engine has been terminated and the service has been unbound. */ void onDisconnected(); } protected class WallpaperConnection extends IWallpaperConnection.Stub implements ServiceConnection { private final Activity mActivity; private final Intent mIntent; private final WallpaperConnectionListener mListener; private IWallpaperService mService; private IWallpaperEngine mEngine; private boolean mConnected; private boolean mIsVisible; private boolean mIsEngineVisible; private boolean mEngineReady; WallpaperConnection(Intent intent, Activity activity, @Nullable WallpaperConnectionListener listener) { mActivity = activity; mIntent = intent; mListener = listener; } public boolean connect() { synchronized (this) { if (!mActivity.bindService(mIntent, this, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT)) { return false; } mConnected = true; } if (mListener != null) { mListener.onConnected(); } return true; } public void disconnect() { synchronized (this) { mConnected = false; if (mEngine != null) { try { mEngine.destroy(); } catch (RemoteException e) { // Ignore } mEngine = null; } try { mActivity.unbindService(this); } catch (IllegalArgumentException e) { Log.w(TAG, "Can't unbind wallpaper service. " + "It might have crashed, just ignoring.", e); } mService = null; } if (mListener != null) { mListener.onDisconnected(); } } public void onServiceConnected(ComponentName name, IBinder service) { if (mWallpaperConnection == this) { mService = IWallpaperService.Stub.asInterface(service); try { View root = mActivity.getWindow().getDecorView(); int displayId = root.getDisplay().getDisplayId(); mService.attach(this, root.getWindowToken(), LayoutParams.TYPE_APPLICATION_MEDIA, true, root.getWidth(), root.getHeight(), new Rect(0, 0, 0, 0), displayId); } catch (RemoteException e) { Log.w(TAG, "Failed attaching wallpaper; clearing", e); } } } public void onServiceDisconnected(ComponentName name) { mService = null; mEngine = null; if (mWallpaperConnection == this) { Log.w(TAG, "Wallpaper service gone: " + name); } } public void attachEngine(IWallpaperEngine engine, int displayId) { synchronized (this) { if (mConnected) { mEngine = engine; if (mIsVisible) { setEngineVisibility(true); } } else { try { engine.destroy(); } catch (RemoteException e) { // Ignore } } } } public IWallpaperEngine getEngine() { return mEngine; } public ParcelFileDescriptor setWallpaper(String name) { return null; } @Override public void onWallpaperColorsChanged(WallpaperColors colors, int displayId) throws RemoteException { } @Override public void engineShown(IWallpaperEngine engine) { mLoadingScrim.post(() -> { mLoadingScrim.animate() .alpha(0f) .setDuration(220) .setStartDelay(300) .setInterpolator(AnimationUtils.loadInterpolator(mActivity, android.R.interpolator.fast_out_linear_in)) .withEndAction(() -> { if (mLoadingProgressBar != null) { mLoadingProgressBar.hide(); } mLoadingScrim.setVisibility(View.INVISIBLE); populateInfoPage(mInfoPageController); }); }); mEngineReady = true; } public boolean isEngineReady() { return mEngineReady; } public void setVisibility(boolean visible) { mIsVisible = visible; setEngineVisibility(visible); } private void setEngineVisibility(boolean visible) { if (mEngine != null && visible != mIsEngineVisible) { try { mEngine.setVisibility(visible); mIsEngineVisible = visible; } catch (RemoteException e) { Log.w(TAG, "Failure setting wallpaper visibility ", e); } } } } }