1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.wallpaper.picker;
17 
18 import android.annotation.SuppressLint;
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.WallpaperColors;
22 import android.app.WallpaperInfo;
23 import android.app.WallpaperManager;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.ServiceConnection;
28 import android.content.pm.ActivityInfo;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.PackageManager;
31 import android.content.pm.ServiceInfo;
32 import android.graphics.Rect;
33 import android.net.Uri;
34 import android.os.Bundle;
35 import android.os.IBinder;
36 import android.os.ParcelFileDescriptor;
37 import android.os.RemoteException;
38 import android.service.wallpaper.IWallpaperConnection;
39 import android.service.wallpaper.IWallpaperEngine;
40 import android.service.wallpaper.IWallpaperService;
41 import android.service.wallpaper.WallpaperService;
42 import android.service.wallpaper.WallpaperSettingsActivity;
43 import android.text.TextUtils;
44 import android.util.Log;
45 import android.util.Pair;
46 import android.view.ContextThemeWrapper;
47 import android.view.LayoutInflater;
48 import android.view.Menu;
49 import android.view.MenuInflater;
50 import android.view.MenuItem;
51 import android.view.View;
52 import android.view.ViewGroup;
53 import android.view.WindowManager.LayoutParams;
54 import android.view.animation.AnimationUtils;
55 
56 import androidx.annotation.NonNull;
57 import androidx.annotation.Nullable;
58 import androidx.lifecycle.LiveData;
59 import androidx.slice.Slice;
60 import androidx.slice.widget.SliceLiveData;
61 import androidx.slice.widget.SliceView;
62 import androidx.viewpager.widget.PagerAdapter;
63 import androidx.viewpager.widget.ViewPager;
64 
65 import com.android.wallpaper.R;
66 import com.android.wallpaper.compat.BuildCompat;
67 import com.android.wallpaper.model.LiveWallpaperInfo;
68 import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback;
69 
70 import com.google.android.material.tabs.TabLayout;
71 
72 import java.util.ArrayList;
73 import java.util.List;
74 
75 /**
76  * Fragment which displays the UI for previewing an individual live wallpaper, its attribution
77  * information and settings slices if available.
78  */
79 public class LivePreviewFragment extends PreviewFragment {
80 
81     private static final String TAG = "LivePreviewFragment";
82 
83     private static final String KEY_ACTION_DELETE_LIVE_WALLPAPER = "action_delete_live_wallpaper";
84     private static final String EXTRA_LIVE_WALLPAPER_INFO = "android.live_wallpaper.info";
85 
86     /**
87      * Instance of {@link WallpaperConnection} used to bind to the live wallpaper service to show
88      * it in this preview fragment.
89      * @see IWallpaperConnection
90      */
91     protected WallpaperConnection mWallpaperConnection;
92 
93     private Intent mWallpaperIntent;
94     private Intent mDeleteIntent;
95     private Intent mSettingsIntent;
96 
97     private List<Pair<String, View>> mPages;
98     private ViewPager mViewPager;
99     private TabLayout mTabLayout;
100     private SliceView mSettingsSliceView;
101     private LiveData<Slice> mSettingsLiveData;
102     private View mLoadingScrim;
103     private InfoPageController mInfoPageController;
104 
105     @Override
onCreate(Bundle savedInstanceState)106     public void onCreate(Bundle savedInstanceState) {
107         super.onCreate(savedInstanceState);
108         android.app.WallpaperInfo info = mWallpaper.getWallpaperComponent();
109         mWallpaperIntent = getWallpaperIntent(info);
110         setUpExploreIntent(null);
111 
112         android.app.WallpaperInfo currentWallpaper =
113                 WallpaperManager.getInstance(requireContext()).getWallpaperInfo();
114         String deleteAction = getDeleteAction(info, currentWallpaper);
115 
116         if (!TextUtils.isEmpty(deleteAction)) {
117             mDeleteIntent = new Intent(deleteAction);
118             mDeleteIntent.setPackage(info.getPackageName());
119             mDeleteIntent.putExtra(EXTRA_LIVE_WALLPAPER_INFO, info);
120         }
121 
122         String settingsActivity = getSettingsActivity(info);
123         if (settingsActivity != null) {
124             mSettingsIntent = new Intent();
125             mSettingsIntent.setComponent(new ComponentName(info.getPackageName(),
126                     settingsActivity));
127             mSettingsIntent.putExtra(WallpaperSettingsActivity.EXTRA_PREVIEW_MODE, true);
128             PackageManager pm = requireContext().getPackageManager();
129             ActivityInfo activityInfo = mSettingsIntent.resolveActivityInfo(pm, 0);
130             if (activityInfo == null) {
131                 Log.i(TAG, "Couldn't find wallpaper settings activity: " + settingsActivity);
132                 mSettingsIntent = null;
133             }
134         }
135     }
136 
137     @Nullable
getSettingsActivity(WallpaperInfo info)138     protected String getSettingsActivity(WallpaperInfo info) {
139         return info.getSettingsActivity();
140     }
141 
getWallpaperIntent(WallpaperInfo info)142     protected Intent getWallpaperIntent(WallpaperInfo info) {
143         return new Intent(WallpaperService.SERVICE_INTERFACE)
144                 .setClassName(info.getPackageName(), info.getServiceName());
145     }
146 
147     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)148     public View onCreateView(LayoutInflater inflater, ViewGroup container,
149             Bundle savedInstanceState) {
150         mPages = new ArrayList<>();
151         View view = super.onCreateView(inflater, container, savedInstanceState);
152         if (view == null) {
153             return null;
154         }
155 
156         Activity activity = requireActivity();
157 
158         mLoadingScrim = view.findViewById(R.id.loading);
159         setUpLoadingIndicator();
160 
161         mWallpaperConnection = new WallpaperConnection(mWallpaperIntent, activity,
162                 getWallpaperConnectionListener());
163         container.post(() -> {
164             if (!mWallpaperConnection.connect()) {
165                 mWallpaperConnection = null;
166             }
167         });
168 
169         return view;
170     }
171 
172     @Override
onDestroyView()173     public void onDestroyView() {
174         super.onDestroyView();
175         if (mSettingsLiveData != null && mSettingsLiveData.hasObservers()) {
176             mSettingsLiveData.removeObserver(mSettingsSliceView);
177             mSettingsLiveData = null;
178         }
179         if (mWallpaperConnection != null) {
180             mWallpaperConnection.disconnect();
181         }
182         mWallpaperConnection = null;
183         super.onDestroy();
184     }
185 
186     @Override
setUpBottomSheetView(ViewGroup bottomSheet)187     protected void setUpBottomSheetView(ViewGroup bottomSheet) {
188 
189         initInfoPage();
190         initSettingsPage();
191 
192         mViewPager = bottomSheet.findViewById(R.id.viewpager);
193         mTabLayout = bottomSheet.findViewById(R.id.tablayout);
194 
195         // Create PagerAdapter
196         final PagerAdapter pagerAdapter = new PagerAdapter() {
197             @Override
198             public Object instantiateItem(ViewGroup container, int position) {
199                 final View page = mPages.get(position).second;
200                 container.addView(page);
201                 return page;
202             }
203 
204             @Override
205             public void destroyItem(@NonNull ViewGroup container, int position,
206                     @NonNull Object object) {
207                 if (object instanceof View) {
208                     container.removeView((View) object);
209                 }
210             }
211 
212             @Override
213             public int getCount() {
214                 return mPages.size();
215             }
216 
217             @Override
218             public CharSequence getPageTitle(int position) {
219                 try {
220                     return mPages.get(position).first;
221                 } catch (IndexOutOfBoundsException e) {
222                     return null;
223                 }
224             }
225 
226             @Override
227             public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
228                 return (view == object);
229             }
230         };
231 
232         // Add OnPageChangeListener to re-measure ViewPager's height
233         mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
234             @Override
235             public void onPageSelected(int position) {
236                 mViewPager.requestLayout();
237             }
238         });
239 
240         // Set PagerAdapter
241         mViewPager.setAdapter(pagerAdapter);
242 
243         // Make TabLayout visible if there are more than one page
244         if (mPages.size() > 1) {
245             mTabLayout.setVisibility(View.VISIBLE);
246             mTabLayout.setupWithViewPager(mViewPager);
247         }
248         mViewPager.setCurrentItem(0);
249     }
250 
getWallpaperConnectionListener()251     protected WallpaperConnectionListener getWallpaperConnectionListener() {
252         return null;
253     }
254 
255     @Override
isLoaded()256     protected boolean isLoaded() {
257         return mWallpaperConnection != null && mWallpaperConnection.isEngineReady();
258     }
259 
initInfoPage()260     private void initInfoPage() {
261         View pageInfo = InfoPageController.createView(getLayoutInflater());
262         mInfoPageController = new InfoPageController(pageInfo, mPreviewMode);
263         mPages.add(Pair.create(getString(R.string.tab_info), pageInfo));
264     }
265 
initSettingsPage()266     private void initSettingsPage() {
267         final Uri uriSettingsSlice = getSettingsSliceUri(mWallpaper.getWallpaperComponent());
268         if (uriSettingsSlice == null) {
269             return;
270         }
271 
272         final View pageSettings = getLayoutInflater().inflate(R.layout.preview_page_settings,
273                 null /* root */);
274 
275         mSettingsSliceView = pageSettings.findViewById(R.id.settings_slice);
276         mSettingsSliceView.setMode(SliceView.MODE_LARGE);
277         mSettingsSliceView.setScrollable(false);
278 
279         // Set LiveData for SliceView
280         mSettingsLiveData = SliceLiveData.fromUri(requireContext() /* context */, uriSettingsSlice);
281         mSettingsLiveData.observeForever(mSettingsSliceView);
282 
283         pageSettings.findViewById(R.id.preview_settings_pane_set_wallpaper_button)
284                 .setOnClickListener(this::onSetWallpaperClicked);
285 
286         mPages.add(Pair.create(getResources().getString(R.string.tab_customize), pageSettings));
287     }
288 
289     @Override
getExploreButtonLabel(Context context)290     protected CharSequence getExploreButtonLabel(Context context) {
291         CharSequence exploreLabel = ((LiveWallpaperInfo) mWallpaper).getActionDescription(context);
292         if (TextUtils.isEmpty(exploreLabel)) {
293             exploreLabel = context.getString(mWallpaper.getActionLabelRes(context));
294         }
295         return exploreLabel;
296     }
297 
298     @SuppressLint("NewApi") //Already checking with isAtLeastQ
getSettingsSliceUri(android.app.WallpaperInfo info)299     protected Uri getSettingsSliceUri(android.app.WallpaperInfo info) {
300         if (BuildCompat.isAtLeastQ()) {
301             return info.getSettingsSliceUri();
302         }
303         return null;
304     }
305 
306     @Override
getLayoutResId()307     protected int getLayoutResId() {
308         return R.layout.fragment_live_preview;
309     }
310 
311     @Override
getBottomSheetResId()312     protected int getBottomSheetResId() {
313         return R.id.bottom_sheet;
314     }
315 
316     @Override
getLoadingIndicatorResId()317     protected int getLoadingIndicatorResId() {
318         return R.id.loading_indicator;
319     }
320 
321     @Override
setCurrentWallpaper(int destination)322     protected void setCurrentWallpaper(int destination) {
323         mWallpaperSetter.setCurrentWallpaper(getActivity(), mWallpaper, null,
324                 destination, 0, null, new SetWallpaperCallback() {
325                     @Override
326                     public void onSuccess() {
327                         finishActivityWithResultOk();
328                     }
329 
330                     @Override
331                     public void onError(@Nullable Throwable throwable) {
332                         showSetWallpaperErrorDialog(destination);
333                     }
334                 });
335     }
336 
337     @Override
setBottomSheetContentAlpha(float alpha)338     protected void setBottomSheetContentAlpha(float alpha) {
339         mInfoPageController.setContentAlpha(alpha);
340     }
341 
342 
343     @Nullable
getDeleteAction(android.app.WallpaperInfo wallpaperInfo, @Nullable android.app.WallpaperInfo currentInfo)344     protected String getDeleteAction(android.app.WallpaperInfo wallpaperInfo,
345             @Nullable android.app.WallpaperInfo currentInfo) {
346         ServiceInfo serviceInfo = wallpaperInfo.getServiceInfo();
347         if (!isPackagePreInstalled(serviceInfo.applicationInfo)) {
348             Log.d(TAG, "This wallpaper is not pre-installed: " + serviceInfo.name);
349             return null;
350         }
351 
352         ServiceInfo currentService = currentInfo == null ? null : currentInfo.getServiceInfo();
353         // A currently set Live wallpaper should not be deleted.
354         if (currentService != null && TextUtils.equals(serviceInfo.name, currentService.name)) {
355             return null;
356         }
357 
358         final Bundle metaData = serviceInfo.metaData;
359         if (metaData != null) {
360             return metaData.getString(KEY_ACTION_DELETE_LIVE_WALLPAPER);
361         }
362         return null;
363     }
364 
365     @Override
onResume()366     public void onResume() {
367         super.onResume();
368         if (mWallpaperConnection != null) {
369             mWallpaperConnection.setVisibility(true);
370         }
371     }
372 
373     @Override
onPause()374     public void onPause() {
375         super.onPause();
376         if (mWallpaperConnection != null) {
377             mWallpaperConnection.setVisibility(false);
378         }
379     }
380 
381     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)382     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
383         super.onCreateOptionsMenu(menu, inflater);
384         menu.findItem(R.id.configure).setVisible(mSettingsIntent != null);
385         menu.findItem(R.id.delete_wallpaper).setVisible(mDeleteIntent != null);
386     }
387 
388     @Override
onOptionsItemSelected(MenuItem item)389     public boolean onOptionsItemSelected(MenuItem item) {
390         int id = item.getItemId();
391         if (id == R.id.configure) {
392             if (getActivity() != null) {
393                 startActivity(mSettingsIntent);
394                 return true;
395             }
396         } else if (id == R.id.delete_wallpaper) {
397             showDeleteConfirmDialog();
398             return true;
399         }
400         return super.onOptionsItemSelected(item);
401     }
402 
showDeleteConfirmDialog()403     private void showDeleteConfirmDialog() {
404         final AlertDialog alertDialog = new AlertDialog.Builder(
405                 new ContextThemeWrapper(getContext(), getDeviceDefaultTheme()))
406                 .setMessage(R.string.delete_wallpaper_confirmation)
407                 .setPositiveButton(R.string.delete_live_wallpaper,
408                         (dialog, which) -> deleteLiveWallpaper())
409                 .setNegativeButton(android.R.string.cancel, null /* listener */)
410                 .create();
411         alertDialog.show();
412     }
413 
deleteLiveWallpaper()414     private void deleteLiveWallpaper() {
415         if (mDeleteIntent != null) {
416             requireContext().startService(mDeleteIntent);
417             finishActivityWithResultOk();
418         }
419     }
420 
isPackagePreInstalled(ApplicationInfo info)421     private boolean isPackagePreInstalled(ApplicationInfo info) {
422         if (info != null && (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
423             return true;
424         }
425         return false;
426     }
427 
428     /**
429      * Interface to be notified of connect/disconnect events from {@link WallpaperConnection}
430      */
431     public interface WallpaperConnectionListener {
432         /**
433          * Called after the Wallpaper service has been bound.
434          */
onConnected()435         void onConnected();
436 
437         /**
438          * Called after the Wallpaper engine has been terminated and the service has been unbound.
439          */
onDisconnected()440         void onDisconnected();
441     }
442 
443     protected class WallpaperConnection extends IWallpaperConnection.Stub
444             implements ServiceConnection {
445 
446         private final Activity mActivity;
447         private final Intent mIntent;
448         private final WallpaperConnectionListener mListener;
449         private IWallpaperService mService;
450         private IWallpaperEngine mEngine;
451         private boolean mConnected;
452         private boolean mIsVisible;
453         private boolean mIsEngineVisible;
454         private boolean mEngineReady;
455 
WallpaperConnection(Intent intent, Activity activity, @Nullable WallpaperConnectionListener listener)456         WallpaperConnection(Intent intent, Activity activity,
457                 @Nullable WallpaperConnectionListener listener) {
458             mActivity = activity;
459             mIntent = intent;
460             mListener = listener;
461         }
462 
connect()463         public boolean connect() {
464             synchronized (this) {
465                 if (!mActivity.bindService(mIntent, this,
466                         Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT)) {
467                     return false;
468                 }
469 
470                 mConnected = true;
471             }
472             if (mListener != null) {
473                 mListener.onConnected();
474             }
475             return true;
476         }
477 
disconnect()478         public void disconnect() {
479             synchronized (this) {
480                 mConnected = false;
481                 if (mEngine != null) {
482                     try {
483                         mEngine.destroy();
484                     } catch (RemoteException e) {
485                         // Ignore
486                     }
487                     mEngine = null;
488                 }
489                 try {
490                     mActivity.unbindService(this);
491                 } catch (IllegalArgumentException e) {
492                     Log.w(TAG, "Can't unbind wallpaper service. "
493                             + "It might have crashed, just ignoring.", e);
494                 }
495                 mService = null;
496             }
497             if (mListener != null) {
498                 mListener.onDisconnected();
499             }
500         }
501 
onServiceConnected(ComponentName name, IBinder service)502         public void onServiceConnected(ComponentName name, IBinder service) {
503             if (mWallpaperConnection == this) {
504                 mService = IWallpaperService.Stub.asInterface(service);
505                 try {
506                     View root = mActivity.getWindow().getDecorView();
507                     int displayId = root.getDisplay().getDisplayId();
508                     mService.attach(this, root.getWindowToken(),
509                             LayoutParams.TYPE_APPLICATION_MEDIA,
510                             true, root.getWidth(), root.getHeight(),
511                             new Rect(0, 0, 0, 0), displayId);
512                 } catch (RemoteException e) {
513                     Log.w(TAG, "Failed attaching wallpaper; clearing", e);
514                 }
515             }
516         }
517 
onServiceDisconnected(ComponentName name)518         public void onServiceDisconnected(ComponentName name) {
519             mService = null;
520             mEngine = null;
521             if (mWallpaperConnection == this) {
522                 Log.w(TAG, "Wallpaper service gone: " + name);
523             }
524         }
525 
attachEngine(IWallpaperEngine engine, int displayId)526         public void attachEngine(IWallpaperEngine engine, int displayId) {
527             synchronized (this) {
528                 if (mConnected) {
529                     mEngine = engine;
530                     if (mIsVisible) {
531                         setEngineVisibility(true);
532                     }
533                 } else {
534                     try {
535                         engine.destroy();
536                     } catch (RemoteException e) {
537                         // Ignore
538                     }
539                 }
540             }
541         }
542 
getEngine()543         public IWallpaperEngine getEngine() {
544             return mEngine;
545         }
546 
setWallpaper(String name)547         public ParcelFileDescriptor setWallpaper(String name) {
548             return null;
549         }
550 
551         @Override
onWallpaperColorsChanged(WallpaperColors colors, int displayId)552         public void onWallpaperColorsChanged(WallpaperColors colors, int displayId)
553                 throws RemoteException {
554 
555         }
556 
557         @Override
engineShown(IWallpaperEngine engine)558         public void engineShown(IWallpaperEngine engine)  {
559             mLoadingScrim.post(() -> {
560                 mLoadingScrim.animate()
561                         .alpha(0f)
562                         .setDuration(220)
563                         .setStartDelay(300)
564                         .setInterpolator(AnimationUtils.loadInterpolator(mActivity,
565                                 android.R.interpolator.fast_out_linear_in))
566                         .withEndAction(() -> {
567                             if (mLoadingProgressBar != null) {
568                                 mLoadingProgressBar.hide();
569                             }
570                             mLoadingScrim.setVisibility(View.INVISIBLE);
571                             populateInfoPage(mInfoPageController);
572                         });
573             });
574             mEngineReady = true;
575         }
576 
isEngineReady()577         public boolean isEngineReady() {
578             return mEngineReady;
579         }
580 
setVisibility(boolean visible)581         public void setVisibility(boolean visible) {
582             mIsVisible = visible;
583             setEngineVisibility(visible);
584         }
585 
setEngineVisibility(boolean visible)586         private void setEngineVisibility(boolean visible) {
587             if (mEngine != null && visible != mIsEngineVisible) {
588                 try {
589                     mEngine.setVisibility(visible);
590                     mIsEngineVisible = visible;
591                 } catch (RemoteException e) {
592                     Log.w(TAG, "Failure setting wallpaper visibility ", e);
593                 }
594             }
595         }
596     }
597 }
598