| /* |
| * 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 com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED; |
| |
| import android.annotation.SuppressLint; |
| import android.app.Activity; |
| import android.app.WallpaperColors; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.content.pm.ApplicationInfo; |
| 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.text.TextUtils; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.view.ContextThemeWrapper; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.WindowManager.LayoutParams; |
| import android.view.animation.AnimationUtils; |
| import android.widget.Button; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| 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.android.wallpaper.widget.MaterialProgressDrawable; |
| |
| import com.google.android.material.bottomsheet.BottomSheetBehavior; |
| 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 |
| */ |
| private WallpaperConnection mWallpaperConnection; |
| |
| private Intent mWallpaperIntent; |
| |
| private List<Pair<String, View>> mPages; |
| private ImageView mLoadingIndicator; |
| private TextView mAttributionTitle; |
| private TextView mAttributionSubtitle1; |
| private TextView mAttributionSubtitle2; |
| private Button mExploreButton; |
| private Button mSetWallpaperButton; |
| private ViewPager mViewPager; |
| private TabLayout mTabLayout; |
| private SliceView mSettingsSliceView; |
| private LiveData<Slice> mSettingsLiveData; |
| private View mSpacer; |
| private View mLoadingScrim; |
| private MaterialProgressDrawable mProgressDrawable; |
| |
| /** |
| * Creates and returns new instance of {@link LivePreviewFragment} with the provided wallpaper |
| * set as an argument. |
| */ |
| public static LivePreviewFragment newInstance( |
| LiveWallpaperInfo wallpaperInfo, @PreviewMode int mode, boolean testingModeEnabled) { |
| Bundle args = new Bundle(); |
| args.putParcelable(ARG_WALLPAPER, wallpaperInfo); |
| args.putInt(ARG_PREVIEW_MODE, mode); |
| args.putBoolean(ARG_TESTING_MODE_ENABLED, testingModeEnabled); |
| |
| LivePreviewFragment fragment = new LivePreviewFragment(); |
| fragment.setArguments(args); |
| return fragment; |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| android.app.WallpaperInfo info = mWallpaper.getWallpaperComponent(); |
| mWallpaperIntent = new Intent(WallpaperService.SERVICE_INTERFACE) |
| .setClassName(info.getPackageName(), info.getServiceName()); |
| setUpExploreIntent(null); |
| } |
| |
| @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); |
| mLoadingIndicator = view.findViewById(R.id.loading_indicator); |
| setUpLoadingIndicator(); |
| |
| mWallpaperConnection = new WallpaperConnection(mWallpaperIntent, activity); |
| 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); |
| } |
| |
| private void setUpLoadingIndicator() { |
| Context context = requireContext(); |
| mProgressDrawable = new MaterialProgressDrawable(context.getApplicationContext(), |
| mLoadingIndicator); |
| mProgressDrawable.setAlpha(255); |
| mProgressDrawable.setBackgroundColor(getResources().getColor(R.color.material_white_100, |
| context.getTheme())); |
| mProgressDrawable.setColorSchemeColors(getAttrColor( |
| new ContextThemeWrapper(context, getDeviceDefaultTheme()), |
| android.R.attr.colorAccent)); |
| mProgressDrawable.updateSizes(MaterialProgressDrawable.LARGE); |
| mLoadingIndicator.setImageDrawable(mProgressDrawable); |
| |
| // We don't want to show the spinner every time we load a wallpaper if it loads quickly; |
| // instead, only start showing the spinner after 100 ms |
| mLoadingIndicator.postDelayed(() -> { |
| if ((mWallpaperConnection == null || !mWallpaperConnection.isEngineReady()) |
| && !mTestingModeEnabled) { |
| mLoadingIndicator.setVisibility(View.VISIBLE); |
| mLoadingIndicator.setAlpha(1f); |
| if (mProgressDrawable != null) { |
| mProgressDrawable.start(); |
| } |
| } |
| }, 100); |
| } |
| |
| private void initInfoPage() { |
| View pageInfo = getLayoutInflater().inflate(R.layout.preview_page_info, null /* root */); |
| |
| mAttributionTitle = pageInfo.findViewById(R.id.preview_attribution_pane_title); |
| mAttributionSubtitle1 = pageInfo.findViewById(R.id.preview_attribution_pane_subtitle1); |
| mAttributionSubtitle2 = pageInfo.findViewById(R.id.preview_attribution_pane_subtitle2); |
| mSpacer = pageInfo.findViewById(R.id.spacer); |
| |
| mExploreButton = pageInfo.findViewById(R.id.preview_attribution_pane_explore_button); |
| mSetWallpaperButton = pageInfo.findViewById( |
| R.id.preview_attribution_pane_set_wallpaper_button); |
| |
| 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); |
| |
| mPages.add(Pair.create(getResources().getString(R.string.tab_customize), pageSettings)); |
| } |
| |
| private void populateAttributionPane() { |
| final Context context = getContext(); |
| |
| final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet); |
| |
| List<String> attributions = mWallpaper.getAttributions(context); |
| if (attributions.size() > 0 && attributions.get(0) != null) { |
| mAttributionTitle.setText(attributions.get(0)); |
| } |
| |
| if (attributions.size() > 1 && attributions.get(1) != null) { |
| mAttributionSubtitle1.setVisibility(View.VISIBLE); |
| mAttributionSubtitle1.setText(attributions.get(1)); |
| } |
| |
| if (attributions.size() > 2 && attributions.get(2) != null) { |
| mAttributionSubtitle2.setVisibility(View.VISIBLE); |
| mAttributionSubtitle2.setText(attributions.get(2)); |
| } |
| |
| setUpSetWallpaperButton(mSetWallpaperButton); |
| |
| setUpExploreButton(mExploreButton); |
| |
| if (mExploreButton.getVisibility() == View.VISIBLE |
| && mSetWallpaperButton.getVisibility() == View.VISIBLE) { |
| mSpacer.setVisibility(View.VISIBLE); |
| } else { |
| mSpacer.setVisibility(View.GONE); |
| } |
| |
| mBottomSheet.setVisibility(View.VISIBLE); |
| |
| // Initialize the state of the BottomSheet based on the current state because if the initial |
| // and current state are the same, the state change listener won't fire and set the correct |
| // arrow asset and text alpha. |
| if (mBottomSheetInitialState == STATE_EXPANDED) { |
| setPreviewChecked(false); |
| mAttributionTitle.setAlpha(1f); |
| mAttributionSubtitle1.setAlpha(1f); |
| mAttributionSubtitle2.setAlpha(1f); |
| } else { |
| setPreviewChecked(true); |
| mAttributionTitle.setAlpha(0f); |
| mAttributionSubtitle1.setAlpha(0f); |
| mAttributionSubtitle2.setAlpha(0f); |
| } |
| |
| bottomSheetBehavior.setState(mBottomSheetInitialState); |
| } |
| |
| @SuppressLint("NewApi") //Already checking with isAtLeastQ |
| private 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 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) { |
| mExploreButton.setAlpha(alpha); |
| mAttributionTitle.setAlpha(alpha); |
| mAttributionSubtitle1.setAlpha(alpha); |
| mAttributionSubtitle2.setAlpha(alpha); |
| } |
| |
| @Override |
| protected void setUpExploreButton(Button exploreButton) { |
| super.setUpExploreButton(exploreButton); |
| if (exploreButton.getVisibility() != View.VISIBLE) { |
| return; |
| } |
| Context context = requireContext(); |
| CharSequence exploreLabel = ((LiveWallpaperInfo) mWallpaper).getActionDescription(context); |
| if (!TextUtils.isEmpty(exploreLabel)) { |
| exploreButton.setText(exploreLabel); |
| } |
| } |
| |
| @Nullable |
| private String getDeleteAction(ServiceInfo serviceInfo, |
| @Nullable ServiceInfo currentService) { |
| if (!isPackagePreInstalled(serviceInfo.applicationInfo)) { |
| Log.d(TAG, "This wallpaper is not pre-installed: " + serviceInfo.name); |
| return null; |
| } |
| |
| // 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); |
| } |
| } |
| |
| private boolean isPackagePreInstalled(ApplicationInfo info) { |
| if (info != null && (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { |
| return true; |
| } |
| return false; |
| } |
| |
| private class WallpaperConnection extends IWallpaperConnection.Stub |
| implements ServiceConnection { |
| private final Activity mActivity; |
| private final Intent mIntent; |
| private IWallpaperService mService; |
| private IWallpaperEngine mEngine; |
| private boolean mConnected; |
| private boolean mIsVisible; |
| private boolean mIsEngineVisible; |
| private boolean mEngineReady; |
| |
| WallpaperConnection(Intent intent, Activity activity) { |
| mActivity = activity; |
| mIntent = intent; |
| } |
| |
| public boolean connect() { |
| synchronized (this) { |
| if (!mActivity.bindService(mIntent, this, Context.BIND_AUTO_CREATE)) { |
| return false; |
| } |
| |
| mConnected = true; |
| 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; |
| } |
| } |
| |
| 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 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 (mLoadingIndicator != null) { |
| mLoadingIndicator.setVisibility(View.GONE); |
| } |
| if (mProgressDrawable != null) { |
| mProgressDrawable.stop(); |
| } |
| mLoadingScrim.setVisibility(View.INVISIBLE); |
| populateAttributionPane(); |
| }); |
| }); |
| 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); |
| } |
| } |
| } |
| } |
| } |