blob: c346d137aee59f41cc3b8aa4473943de0cc91c9e [file] [log] [blame]
/*
* Copyright (C) 2009 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.livepicker;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.WallpaperColors;
import android.app.WallpaperInfo;
import android.app.WallpaperManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources.NotFoundException;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
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.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowManager.LayoutParams;
import android.view.animation.AnimationUtils;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
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.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback;
import com.google.android.material.tabs.TabLayout;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class LiveWallpaperPreview extends Activity {
static final String EXTRA_LIVE_WALLPAPER_INFO = "android.live_wallpaper.info";
private static final String LOG_TAG = "LiveWallpaperPreview";
private static final boolean SHOW_FAKE_DATA = false;
private WallpaperManager mWallpaperManager;
private WallpaperConnection mWallpaperConnection;
private Intent mWallpaperIntent;
private Intent mSettingsIntent;
private Intent mDeleteIntent;
private View mLoading;
private View mViewBottomPane;
private BottomSheetBehavior mBottomSheetBehavior;
private ViewPager mViewPager;
private TabLayout mTabLayout;
private CheckBox mPreview;
protected final List<Pair<String, View>> mPages = new ArrayList<>();
private SliceView mSliceViewSettings;
private LiveData<Slice> mLiveDataSettings;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
init();
}
protected void init() {
WallpaperInfo info = getIntent().getParcelableExtra(EXTRA_LIVE_WALLPAPER_INFO);
if (info == null) {
finish();
return;
}
initUI(info, null /* deleteAction */);
}
protected void initUI(WallpaperInfo info, @Nullable String deleteAction) {
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
setContentView(R.layout.live_wallpaper_preview);
mLoading = findViewById(R.id.loading);
final String packageName = info.getPackageName();
mWallpaperIntent = new Intent(WallpaperService.SERVICE_INTERFACE)
.setClassName(info.getPackageName(), info.getServiceName());
final String settingsActivity = info.getSettingsActivity();
if (settingsActivity != null) {
mSettingsIntent = new Intent();
mSettingsIntent.setComponent(new ComponentName(packageName, settingsActivity));
mSettingsIntent.putExtra(WallpaperSettingsActivity.EXTRA_PREVIEW_MODE, true);
final PackageManager pm = getPackageManager();
final ActivityInfo activityInfo = mSettingsIntent.resolveActivityInfo(pm, 0);
if (activityInfo == null) {
Log.e(LOG_TAG, "Couldn't find settings activity: " + settingsActivity);
mSettingsIntent = null;
}
}
final Toolbar toolbar = findViewById(R.id.toolbar);
setActionBar(toolbar);
getActionBar().setDisplayHomeAsUpEnabled(true);
getActionBar().setDisplayShowTitleEnabled(false);
final Drawable backArrow = getDrawable(R.drawable.ic_arrow_back_white_24dp);
backArrow.setAutoMirrored(true);
toolbar.setNavigationIcon(backArrow);
mWallpaperManager = WallpaperManager.getInstance(this);
mWallpaperConnection = new WallpaperConnection(mWallpaperIntent);
getWindow().getDecorView().post(new Runnable() {
public void run() {
if (!mWallpaperConnection.connect()) {
mWallpaperConnection = null;
}
}
});
if (!TextUtils.isEmpty(deleteAction)) {
mDeleteIntent = new Intent(deleteAction);
mDeleteIntent.setPackage(info.getPackageName());
mDeleteIntent.putExtra(EXTRA_LIVE_WALLPAPER_INFO, info);
}
initInfoPage(info);
initSettingsPage(info);
populateBottomPane();
}
private void initInfoPage(WallpaperInfo info) {
final View pageInfo = getLayoutInflater().inflate(R.layout.page_info, null /* root */);
final TextView attributionTitle = pageInfo.findViewById(
R.id.preview_attribution_pane_title);
final TextView attributionAuthor = pageInfo.findViewById(
R.id.preview_attribution_pane_author);
final TextView attributionDescription = pageInfo.findViewById(
R.id.preview_attribution_pane_description);
final Button attributionExploreButton = pageInfo.findViewById(
R.id.preview_attribution_pane_explore_button);
final View spacer = pageInfo.findViewById(R.id.spacer);
final Button setWallpaperButton = pageInfo.findViewById(
R.id.preview_attribution_pane_set_wallpaper_button);
setWallpaperButton.setOnClickListener(this::setLiveWallpaper);
mPages.add(Pair.create(getString(R.string.tab_info), pageInfo));
if (SHOW_FAKE_DATA) {
attributionTitle.setText("Diorama, Yosemite");
attributionTitle.setVisibility(View.VISIBLE);
attributionAuthor.setText("Live Earth Collection - Android Earth");
attributionAuthor.setVisibility(View.VISIBLE);
attributionDescription.setText("Lorem ipsum dolor sit amet, consectetur adipiscing"
+ " elit. Sed imperdiet et mauris molestie laoreet. Proin volutpat elit nec"
+ " magna tempus, ac aliquet lectus volutpat.");
attributionDescription.setVisibility(View.VISIBLE);
attributionExploreButton.setText("Explore");
attributionExploreButton.setVisibility(View.VISIBLE);
spacer.setVisibility(View.VISIBLE);
return;
}
final PackageManager pm = getPackageManager();
// Set attribution title
final CharSequence title = info.loadLabel(pm);
if (!TextUtils.isEmpty(title)) {
attributionTitle.setText(title);
attributionTitle.setVisibility(View.VISIBLE);
}
// Don't show other meta data if attribute showMetadataInPreview is set to False
if (!info.getShowMetadataInPreview()) {
return;
}
// Set attribution author
try {
final CharSequence author = info.loadAuthor(pm);
if (!TextUtils.isEmpty(author)) {
attributionAuthor.setText(author);
attributionAuthor.setVisibility(View.VISIBLE);
}
} catch (NotFoundException e) {
// It's expected if the live wallpaper doesn't provide this information
}
// Set attribution description
try {
final CharSequence description = info.loadDescription(pm);
if (!TextUtils.isEmpty(description)) {
attributionDescription.setText(description);
attributionDescription.setVisibility(View.VISIBLE);
}
} catch (NotFoundException e) {
// It's expected if the live wallpaper doesn't provide this information
}
// Set context information
try {
final Uri contextUri = info.loadContextUri(pm);
if (contextUri != null) {
final Intent intent = new Intent(Intent.ACTION_VIEW, contextUri);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
attributionExploreButton.setOnClickListener(v -> {
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(LOG_TAG, "Couldn't find activity for context link.", e);
}
});
attributionExploreButton.setVisibility(View.VISIBLE);
spacer.setVisibility(View.VISIBLE);
// Update context description string if it's provided
final CharSequence contextDescription = info.loadContextDescription(pm);
if (!TextUtils.isEmpty(contextDescription)) {
attributionExploreButton.setText(contextDescription);
}
}
} catch (NotFoundException e) {
// It's expected if the wallpaper doesn't provide this information
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
protected Uri getSettingsSliceUri(@NonNull WallpaperInfo info) {
return info.getSettingsSliceUri();
}
private void initSettingsPage(WallpaperInfo info) {
final Uri uriSettingsSlice = getSettingsSliceUri(info);
if (uriSettingsSlice == null) {
return;
}
final View pageSettings = getLayoutInflater().inflate(R.layout.page_settings,
null /* root */);
final Button setWallpaperButton = pageSettings.findViewById(
R.id.preview_attribution_pane_set_wallpaper_button);
mSliceViewSettings = pageSettings.findViewById(R.id.settings_slice);
mSliceViewSettings.setMode(SliceView.MODE_LARGE);
mSliceViewSettings.setScrollable(false);
// Set LiveData for SliceView
mLiveDataSettings = SliceLiveData.fromUri(this /* context */, uriSettingsSlice);
mLiveDataSettings.observeForever(mSliceViewSettings);
setWallpaperButton.setOnClickListener(this::setLiveWallpaper);
mPages.add(Pair.create(getResources().getString(R.string.tab_customize), pageSettings));
}
private void populateBottomPane() {
mViewBottomPane = findViewById(R.id.bottom_pane);
mViewPager = findViewById(R.id.viewpager);
mTabLayout = findViewById(R.id.tablayout);
mBottomSheetBehavior = BottomSheetBehavior.from(mViewBottomPane);
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
// Create PagerAdapter
final PagerAdapter pagerAdapter = new PagerAdapter() {
@NonNull
@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);
}
// Initializes a rounded rectangle outline and clips the upper corners to be rounded.
mViewBottomPane.setOutlineProvider(new ViewOutlineProvider() {
private final int radius = getResources().getDimensionPixelSize(
R.dimen.preview_viewpager_round_radius);
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0 /* left */, 0 /* top */, view.getWidth(),
view.getHeight() + radius, radius);
}
});
mViewBottomPane.setClipToOutline(true);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_preview, menu);
setupPreviewMenu(menu);
menu.findItem(R.id.configure).setVisible(mSettingsIntent != null);
menu.findItem(R.id.delete_wallpaper).setVisible(mDeleteIntent != null);
return super.onCreateOptionsMenu(menu);
}
private void setupPreviewMenu(Menu menu) {
mPreview = (CheckBox) menu.findItem(R.id.preview).getActionView();
mPreview.setOnClickListener(this::setPreviewBehavior);
BottomSheetCallback callback = new BottomSheetCallback() {
@Override
public void onStateChanged(View bottomSheet, int newState) {
switch (newState) {
case BottomSheetBehavior.STATE_COLLAPSED:
setPreviewChecked(true /* checked */);
break;
case BottomSheetBehavior.STATE_EXPANDED:
setPreviewChecked(false /* checked */);
break;
}
}
@Override
public void onSlide(View bottomSheet, float slideOffset) {
mTabLayout.setAlpha(slideOffset);
mViewPager.setAlpha(slideOffset);
}
};
mBottomSheetBehavior.setBottomSheetCallback(callback);
int state = mBottomSheetBehavior.getState();
callback.onStateChanged(mViewBottomPane, state);
switch (state) {
case BottomSheetBehavior.STATE_COLLAPSED:
callback.onSlide(mViewBottomPane, 0f);
break;
case BottomSheetBehavior.STATE_EXPANDED:
callback.onSlide(mViewBottomPane, 1f);
break;
}
}
private void setPreviewChecked(boolean checked) {
if (mPreview != null) {
mPreview.setChecked(checked);
int resId = checked ? R.string.expand_attribution_panel
: R.string.collapse_attribution_panel;
mPreview.setContentDescription(getResources().getString(resId));
}
}
private void setPreviewBehavior(final View v) {
CheckBox checkbox = (CheckBox) v;
if (checkbox.isChecked()) {
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
public void setLiveWallpaper(final View v) {
if (mWallpaperManager.getWallpaperInfo() != null
&& mWallpaperManager.getWallpaperId(WallpaperManager.FLAG_LOCK) < 0) {
// The lock screen does not have a distinct wallpaper and the current wallpaper is a
// live wallpaper, so since we cannot preserve any static imagery on the lock screen,
// set the live wallpaper directly without giving the user a destination option.
try {
setLiveWallpaper(v.getRootView().getWindowToken());
setResult(RESULT_OK);
} catch (RuntimeException e) {
Log.w(LOG_TAG, "Failure setting wallpaper", e);
}
finish();
} else {
// Otherwise, prompt to either set on home or both home and lock screen.
final Context themedContext = new ContextThemeWrapper(this /* base */,
android.R.style.Theme_DeviceDefault_Settings);
new AlertDialog.Builder(themedContext)
.setTitle(R.string.set_live_wallpaper)
.setAdapter(new WallpaperTargetAdapter(themedContext),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
setLiveWallpaper(v.getRootView().getWindowToken());
if (which == 1) {
// "Home screen and lock screen"; clear the lock
// screen so it
// shows through to the live wallpaper on home.
mWallpaperManager.clear(WallpaperManager.FLAG_LOCK);
}
setResult(RESULT_OK);
} catch (RuntimeException | IOException e) {
Log.w(LOG_TAG, "Failure setting wallpaper", e);
}
finish();
}
})
.show();
}
}
private void setLiveWallpaper(IBinder windowToken) {
mWallpaperManager.setWallpaperComponent(mWallpaperIntent.getComponent());
mWallpaperManager.setWallpaperOffsetSteps(0.5f /* xStep */, 0.0f /* yStep */);
mWallpaperManager.setWallpaperOffsets(windowToken, 0.5f /* xOffset */, 0.0f /* yOffset */);
}
@VisibleForTesting
void deleteLiveWallpaper() {
if (mDeleteIntent != null) {
startService(mDeleteIntent);
finish();
}
}
private void showDeleteConfirmDialog() {
final AlertDialog alertDialog = new AlertDialog.Builder(this /* context */,
R.style.AlertDialogStyle)
.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();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.configure) {
startActivity(mSettingsIntent);
return true;
} else if (id == R.id.delete_wallpaper) {
showDeleteConfirmDialog();
return true;
} else if (id == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@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
protected void onDestroy () {
if (mLiveDataSettings != null && mLiveDataSettings.hasObservers()) {
mLiveDataSettings.removeObserver(mSliceViewSettings);
mLiveDataSettings = null;
}
if (mWallpaperConnection != null) {
mWallpaperConnection.disconnect();
}
mWallpaperConnection = null;
super.onDestroy();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
boolean handled = getWindow().superDispatchTouchEvent(ev);
if (!handled) {
handled = onTouchEvent(ev);
}
if (!handled && mWallpaperConnection != null && mWallpaperConnection.mEngine != null) {
MotionEvent dup = MotionEvent.obtainNoHistory(ev);
try {
mWallpaperConnection.mEngine.dispatchPointer(dup);
} catch (RemoteException e) {
}
int action = ev.getActionMasked();
try {
if (action == MotionEvent.ACTION_UP) {
mWallpaperConnection.mEngine.dispatchWallpaperCommand(
WallpaperManager.COMMAND_TAP,
(int) ev.getX(), (int) ev.getY(), 0, null);
} else if (action == MotionEvent.ACTION_POINTER_UP) {
int pointerIndex = ev.getActionIndex();
mWallpaperConnection.mEngine.dispatchWallpaperCommand(
WallpaperManager.COMMAND_SECONDARY_TAP,
(int) ev.getX(pointerIndex), (int) ev.getY(pointerIndex), 0, null);
}
} catch (RemoteException e) {
}
}
return handled;
}
class WallpaperConnection extends IWallpaperConnection.Stub implements ServiceConnection {
final Intent mIntent;
IWallpaperService mService;
IWallpaperEngine mEngine;
boolean mConnected;
boolean mIsVisible;
boolean mIsEngineVisible;
WallpaperConnection(Intent intent) {
mIntent = intent;
}
public boolean connect() {
synchronized (this) {
if (!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 {
unbindService(this);
} catch (IllegalArgumentException e) {
Log.w(LOG_TAG, "Can't unbind wallpaper service. "
+ "It might have crashed, just ignoring.", e);
}
mService = null;
}
}
@Override
public void onLocalWallpaperColorsChanged(RectF area,
WallpaperColors colors, int displayId) {
}
public void onServiceConnected(ComponentName name, IBinder service) {
if (mWallpaperConnection == this) {
mService = IWallpaperService.Stub.asInterface(service);
try {
final int displayId = getWindow().getDecorView().getDisplay().getDisplayId();
final View root = getWindow().getDecorView();
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(LOG_TAG, "Failed attaching wallpaper; clearing", e);
}
}
}
public void onServiceDisconnected(ComponentName name) {
mService = null;
mEngine = null;
if (mWallpaperConnection == this) {
Log.w(LOG_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) throws RemoteException {
mLoading.post(() -> {
mLoading.animate()
.alpha(0f)
.setDuration(220)
.setStartDelay(300)
.setInterpolator(AnimationUtils.loadInterpolator(LiveWallpaperPreview.this,
android.R.interpolator.fast_out_linear_in))
.withEndAction(() -> mLoading.setVisibility(View.INVISIBLE));
});
}
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(LOG_TAG, "Failure setting wallpaper visibility ", e);
}
}
}
}
private static class WallpaperTargetAdapter extends ArrayAdapter<CharSequence> {
public WallpaperTargetAdapter(Context context) {
super(context, R.layout.wallpaper_target_dialog_item,
context.getResources().getTextArray(R.array.which_wallpaper_options));
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView tv = (TextView) super.getView(position, convertView, parent);
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(
position == 0 ? R.drawable.ic_home : R.drawable.ic_device, 0, 0, 0);
return tv;
}
}
}