Merge "Reload LockscreenWallpaper and ImageWallpaper on background thread" into nyc-dev
This commit is contained in:
@@ -29,6 +29,7 @@ import android.graphics.Rect;
|
|||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
import android.graphics.Region.Op;
|
import android.graphics.Region.Op;
|
||||||
import android.opengl.GLUtils;
|
import android.opengl.GLUtils;
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.SystemProperties;
|
import android.os.SystemProperties;
|
||||||
import android.renderscript.Matrix4f;
|
import android.renderscript.Matrix4f;
|
||||||
import android.service.wallpaper.WallpaperService;
|
import android.service.wallpaper.WallpaperService;
|
||||||
@@ -155,6 +156,8 @@ public class ImageWallpaper extends WallpaperService {
|
|||||||
|
|
||||||
private int mLastRequestedWidth = -1;
|
private int mLastRequestedWidth = -1;
|
||||||
private int mLastRequestedHeight = -1;
|
private int mLastRequestedHeight = -1;
|
||||||
|
private AsyncTask<Void, Void, Bitmap> mLoader;
|
||||||
|
private boolean mNeedsDrawAfterLoadingWallpaper;
|
||||||
|
|
||||||
public DrawableEngine() {
|
public DrawableEngine() {
|
||||||
super();
|
super();
|
||||||
@@ -184,10 +187,9 @@ public class ImageWallpaper extends WallpaperService {
|
|||||||
super.onCreate(surfaceHolder);
|
super.onCreate(surfaceHolder);
|
||||||
|
|
||||||
mDefaultDisplay = getSystemService(WindowManager.class).getDefaultDisplay();
|
mDefaultDisplay = getSystemService(WindowManager.class).getDefaultDisplay();
|
||||||
|
|
||||||
updateSurfaceSize(surfaceHolder, getDefaultDisplayInfo());
|
|
||||||
|
|
||||||
setOffsetNotificationsEnabled(false);
|
setOffsetNotificationsEnabled(false);
|
||||||
|
|
||||||
|
updateSurfaceSize(surfaceHolder, getDefaultDisplayInfo(), false /* forDraw */);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -197,17 +199,19 @@ public class ImageWallpaper extends WallpaperService {
|
|||||||
mWallpaperManager.forgetLoadedWallpaper();
|
mWallpaperManager.forgetLoadedWallpaper();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSurfaceSize(SurfaceHolder surfaceHolder, DisplayInfo displayInfo) {
|
boolean updateSurfaceSize(SurfaceHolder surfaceHolder, DisplayInfo displayInfo,
|
||||||
|
boolean forDraw) {
|
||||||
|
boolean hasWallpaper = true;
|
||||||
|
|
||||||
// Load background image dimensions, if we haven't saved them yet
|
// Load background image dimensions, if we haven't saved them yet
|
||||||
if (mBackgroundWidth <= 0 || mBackgroundHeight <= 0) {
|
if (mBackgroundWidth <= 0 || mBackgroundHeight <= 0) {
|
||||||
// Need to load the image to get dimensions
|
// Need to load the image to get dimensions
|
||||||
mWallpaperManager.forgetLoadedWallpaper();
|
mWallpaperManager.forgetLoadedWallpaper();
|
||||||
updateWallpaperLocked();
|
loadWallpaper(forDraw);
|
||||||
if (mBackgroundWidth <= 0 || mBackgroundHeight <= 0) {
|
if (DEBUG) {
|
||||||
// Default to the display size if we can't find the dimensions
|
Log.d(TAG, "Reloading, redoing updateSurfaceSize later.");
|
||||||
mBackgroundWidth = displayInfo.logicalWidth;
|
|
||||||
mBackgroundHeight = displayInfo.logicalHeight;
|
|
||||||
}
|
}
|
||||||
|
hasWallpaper = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force the wallpaper to cover the screen in both dimensions
|
// Force the wallpaper to cover the screen in both dimensions
|
||||||
@@ -224,6 +228,7 @@ public class ImageWallpaper extends WallpaperService {
|
|||||||
} else {
|
} else {
|
||||||
surfaceHolder.setSizeFromLayout();
|
surfaceHolder.setSizeFromLayout();
|
||||||
}
|
}
|
||||||
|
return hasWallpaper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -299,6 +304,7 @@ public class ImageWallpaper extends WallpaperService {
|
|||||||
}
|
}
|
||||||
super.onSurfaceRedrawNeeded(holder);
|
super.onSurfaceRedrawNeeded(holder);
|
||||||
|
|
||||||
|
mLastSurfaceHeight = mLastSurfaceWidth = -1;
|
||||||
drawFrame();
|
drawFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +323,9 @@ public class ImageWallpaper extends WallpaperService {
|
|||||||
// should change
|
// should change
|
||||||
if (newRotation != mLastRotation) {
|
if (newRotation != mLastRotation) {
|
||||||
// Update surface size (if necessary)
|
// Update surface size (if necessary)
|
||||||
updateSurfaceSize(getSurfaceHolder(), displayInfo);
|
if (!updateSurfaceSize(getSurfaceHolder(), displayInfo, true /* forDraw */)) {
|
||||||
|
return; // had to reload wallpaper, will retry later
|
||||||
|
}
|
||||||
mRotationAtLastSurfaceSizeUpdate = newRotation;
|
mRotationAtLastSurfaceSizeUpdate = newRotation;
|
||||||
mDisplayWidthAtLastSurfaceSizeUpdate = displayInfo.logicalWidth;
|
mDisplayWidthAtLastSurfaceSizeUpdate = displayInfo.logicalWidth;
|
||||||
mDisplayHeightAtLastSurfaceSizeUpdate = displayInfo.logicalHeight;
|
mDisplayHeightAtLastSurfaceSizeUpdate = displayInfo.logicalHeight;
|
||||||
@@ -339,8 +347,8 @@ public class ImageWallpaper extends WallpaperService {
|
|||||||
}
|
}
|
||||||
mLastRotation = newRotation;
|
mLastRotation = newRotation;
|
||||||
|
|
||||||
// Load bitmap if it is not yet loaded or if it was loaded at a different size
|
// Load bitmap if it is not yet loaded
|
||||||
if (mBackground == null || surfaceDimensionsChanged) {
|
if (mBackground == null) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Reloading bitmap: mBackground, bgw, bgh, dw, dh = " +
|
Log.d(TAG, "Reloading bitmap: mBackground, bgw, bgh, dw, dh = " +
|
||||||
mBackground + ", " +
|
mBackground + ", " +
|
||||||
@@ -349,21 +357,12 @@ public class ImageWallpaper extends WallpaperService {
|
|||||||
dw + ", " + dh);
|
dw + ", " + dh);
|
||||||
}
|
}
|
||||||
mWallpaperManager.forgetLoadedWallpaper();
|
mWallpaperManager.forgetLoadedWallpaper();
|
||||||
updateWallpaperLocked();
|
loadWallpaper(true /* needDraw */);
|
||||||
if (mBackground == null) {
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Unable to load bitmap");
|
Log.d(TAG, "Reloading, resuming draw later");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (DEBUG) {
|
|
||||||
if (dw != mBackground.getWidth() || dh != mBackground.getHeight()) {
|
|
||||||
Log.d(TAG, "Surface != bitmap dimensions: surface w/h, bitmap w/h: " +
|
|
||||||
dw + ", " + dh + ", " + mBackground.getWidth() + ", " +
|
|
||||||
mBackground.getHeight());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center the scaled image
|
// Center the scaled image
|
||||||
mScale = Math.max(1f, Math.max(dw / (float) mBackground.getWidth(),
|
mScale = Math.max(1f, Math.max(dw / (float) mBackground.getWidth(),
|
||||||
@@ -422,25 +421,32 @@ public class ImageWallpaper extends WallpaperService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateWallpaperLocked() {
|
/**
|
||||||
Throwable exception = null;
|
* Loads the wallpaper on background thread and schedules updating the surface frame,
|
||||||
|
* and if {@param needsDraw} is set also draws a frame.
|
||||||
|
*
|
||||||
|
* If loading is already in-flight, subsequent loads are ignored (but needDraw is or-ed to
|
||||||
|
* the active request).
|
||||||
|
*/
|
||||||
|
private void loadWallpaper(boolean needsDraw) {
|
||||||
|
mNeedsDrawAfterLoadingWallpaper |= needsDraw;
|
||||||
|
if (mLoader != null) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Skipping loadWallpaper, already in flight ");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mLoader = new AsyncTask<Void, Void, Bitmap>() {
|
||||||
|
@Override
|
||||||
|
protected Bitmap doInBackground(Void... params) {
|
||||||
|
Throwable exception;
|
||||||
try {
|
try {
|
||||||
mBackground = null;
|
return mWallpaperManager.getBitmap();
|
||||||
mBackgroundWidth = -1;
|
} catch (RuntimeException | OutOfMemoryError e) {
|
||||||
mBackgroundHeight = -1;
|
|
||||||
mBackground = mWallpaperManager.getBitmap();
|
|
||||||
mBackgroundWidth = mBackground.getWidth();
|
|
||||||
mBackgroundHeight = mBackground.getHeight();
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
exception = e;
|
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
exception = e;
|
exception = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
mBackground = null;
|
|
||||||
mBackgroundWidth = -1;
|
|
||||||
mBackgroundHeight = -1;
|
|
||||||
// Note that if we do fail at this, and the default wallpaper can't
|
// Note that if we do fail at this, and the default wallpaper can't
|
||||||
// be loaded, we will go into a cycle. Don't do a build where the
|
// be loaded, we will go into a cycle. Don't do a build where the
|
||||||
// default wallpaper can't be loaded.
|
// default wallpaper can't be loaded.
|
||||||
@@ -451,8 +457,42 @@ public class ImageWallpaper extends WallpaperService {
|
|||||||
// now we're really screwed.
|
// now we're really screwed.
|
||||||
Log.w(TAG, "Unable reset to default wallpaper!", ex);
|
Log.w(TAG, "Unable reset to default wallpaper!", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return mWallpaperManager.getBitmap();
|
||||||
|
} catch (RuntimeException | OutOfMemoryError e) {
|
||||||
|
Log.w(TAG, "Unable to load default wallpaper!", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Bitmap b) {
|
||||||
|
mBackground = null;
|
||||||
|
mBackgroundWidth = -1;
|
||||||
|
mBackgroundHeight = -1;
|
||||||
|
|
||||||
|
if (b != null) {
|
||||||
|
mBackground = b;
|
||||||
|
mBackgroundWidth = mBackground.getWidth();
|
||||||
|
mBackgroundHeight = mBackground.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Wallpaper loaded: " + mBackground);
|
||||||
|
}
|
||||||
|
updateSurfaceSize(getSurfaceHolder(), getDefaultDisplayInfo(),
|
||||||
|
false /* forDraw */);
|
||||||
|
if (mNeedsDrawAfterLoadingWallpaper) {
|
||||||
|
drawFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
mLoader = null;
|
||||||
|
mNeedsDrawAfterLoadingWallpaper = false;
|
||||||
|
}
|
||||||
|
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
|
protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import android.graphics.BitmapFactory;
|
|||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
import android.graphics.drawable.DrawableWrapper;
|
import android.graphics.drawable.DrawableWrapper;
|
||||||
import android.os.Bundle;
|
import android.os.AsyncTask;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
@@ -46,9 +46,7 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen
|
|||||||
|
|
||||||
private static final String TAG = "LockscreenWallpaper";
|
private static final String TAG = "LockscreenWallpaper";
|
||||||
|
|
||||||
private final Context mContext;
|
|
||||||
private final PhoneStatusBar mBar;
|
private final PhoneStatusBar mBar;
|
||||||
private final IWallpaperManager mService;
|
|
||||||
private final WallpaperManager mWallpaperManager;
|
private final WallpaperManager mWallpaperManager;
|
||||||
private final Handler mH;
|
private final Handler mH;
|
||||||
|
|
||||||
@@ -58,68 +56,74 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen
|
|||||||
// The user selected in the UI, or null if no user is selected or UI doesn't support selecting
|
// The user selected in the UI, or null if no user is selected or UI doesn't support selecting
|
||||||
// users.
|
// users.
|
||||||
private UserHandle mSelectedUser;
|
private UserHandle mSelectedUser;
|
||||||
|
private AsyncTask<Void, Void, LoaderResult> mLoader;
|
||||||
|
|
||||||
public LockscreenWallpaper(Context ctx, PhoneStatusBar bar, Handler h) {
|
public LockscreenWallpaper(Context ctx, PhoneStatusBar bar, Handler h) {
|
||||||
mContext = ctx;
|
|
||||||
mBar = bar;
|
mBar = bar;
|
||||||
mH = h;
|
mH = h;
|
||||||
mService = IWallpaperManager.Stub.asInterface(
|
|
||||||
ServiceManager.getService(Context.WALLPAPER_SERVICE));
|
|
||||||
mWallpaperManager = (WallpaperManager) ctx.getSystemService(Context.WALLPAPER_SERVICE);
|
mWallpaperManager = (WallpaperManager) ctx.getSystemService(Context.WALLPAPER_SERVICE);
|
||||||
mCurrentUserId = ActivityManager.getCurrentUser();
|
mCurrentUserId = ActivityManager.getCurrentUser();
|
||||||
|
|
||||||
|
IWallpaperManager service = IWallpaperManager.Stub.asInterface(
|
||||||
|
ServiceManager.getService(Context.WALLPAPER_SERVICE));
|
||||||
try {
|
try {
|
||||||
mService.setLockWallpaperCallback(this);
|
service.setLockWallpaperCallback(this);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
Log.e(TAG, "System dead?" + e);
|
Log.e(TAG, "System dead?" + e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap getBitmap() {
|
public Bitmap getBitmap() {
|
||||||
try {
|
|
||||||
if (mCached) {
|
if (mCached) {
|
||||||
return mCache;
|
return mCache;
|
||||||
}
|
}
|
||||||
if (!mService.isWallpaperSupported(mContext.getOpPackageName())) {
|
if (!mWallpaperManager.isWallpaperSupported()) {
|
||||||
mCached = true;
|
mCached = true;
|
||||||
mCache = null;
|
mCache = null;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoaderResult result = loadBitmap(mCurrentUserId, mSelectedUser);
|
||||||
|
if (result.success) {
|
||||||
|
mCached = true;
|
||||||
|
mCache = result.bitmap;
|
||||||
|
}
|
||||||
|
return mCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoaderResult loadBitmap(int currentUserId, UserHandle selectedUser) {
|
||||||
|
// May be called on any thread - only use thread safe operations.
|
||||||
|
|
||||||
// Prefer the selected user (when specified) over the current user for the FLAG_SET_LOCK
|
// Prefer the selected user (when specified) over the current user for the FLAG_SET_LOCK
|
||||||
// wallpaper.
|
// wallpaper.
|
||||||
final int lockWallpaperUserId =
|
final int lockWallpaperUserId =
|
||||||
mSelectedUser != null ? mSelectedUser.getIdentifier() : mCurrentUserId;
|
selectedUser != null ? selectedUser.getIdentifier() : currentUserId;
|
||||||
ParcelFileDescriptor fd = mService.getWallpaper(null, WallpaperManager.FLAG_LOCK,
|
ParcelFileDescriptor fd = mWallpaperManager.getWallpaperFile(
|
||||||
new Bundle(), lockWallpaperUserId);
|
WallpaperManager.FLAG_LOCK, lockWallpaperUserId);
|
||||||
|
|
||||||
if (fd != null) {
|
if (fd != null) {
|
||||||
try {
|
try {
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
mCache = BitmapFactory.decodeFileDescriptor(
|
return LoaderResult.success(BitmapFactory.decodeFileDescriptor(
|
||||||
fd.getFileDescriptor(), null, options);
|
fd.getFileDescriptor(), null, options));
|
||||||
mCached = true;
|
|
||||||
return mCache;
|
|
||||||
} catch (OutOfMemoryError e) {
|
} catch (OutOfMemoryError e) {
|
||||||
Log.w(TAG, "Can't decode file", e);
|
Log.w(TAG, "Can't decode file", e);
|
||||||
return null;
|
return LoaderResult.fail();
|
||||||
} finally {
|
} finally {
|
||||||
IoUtils.closeQuietly(fd);
|
IoUtils.closeQuietly(fd);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mCached = true;
|
if (selectedUser != null && selectedUser.getIdentifier() != currentUserId) {
|
||||||
if (mSelectedUser != null && mSelectedUser.getIdentifier() != mCurrentUserId) {
|
|
||||||
// When selected user is different from the current user, show the selected
|
// When selected user is different from the current user, show the selected
|
||||||
// user's static wallpaper.
|
// user's static wallpaper.
|
||||||
mCache = mWallpaperManager.getBitmapAsUser(mSelectedUser.getIdentifier());
|
return LoaderResult.success(
|
||||||
|
mWallpaperManager.getBitmapAsUser(selectedUser.getIdentifier()));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// When there is no selected user, or it's same as the current user, show the
|
// When there is no selected user, or it's same as the current user, show the
|
||||||
// system (possibly dynamic) wallpaper for the selected user.
|
// system (possibly dynamic) wallpaper for the selected user.
|
||||||
mCache = null;
|
return LoaderResult.success(null);
|
||||||
}
|
}
|
||||||
return mCache;
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.e(TAG, "System dead?" + e);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,14 +139,16 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mSelectedUser = selectedUser;
|
mSelectedUser = selectedUser;
|
||||||
|
postUpdateWallpaper();
|
||||||
mH.removeCallbacks(this);
|
|
||||||
mH.post(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onWallpaperChanged() {
|
public void onWallpaperChanged() {
|
||||||
// Called on Binder thread.
|
// Called on Binder thread.
|
||||||
|
postUpdateWallpaper();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postUpdateWallpaper() {
|
||||||
mH.removeCallbacks(this);
|
mH.removeCallbacks(this);
|
||||||
mH.post(this);
|
mH.post(this);
|
||||||
}
|
}
|
||||||
@@ -150,10 +156,52 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Called in response to onWallpaperChanged on the main thread.
|
// Called in response to onWallpaperChanged on the main thread.
|
||||||
mCached = false;
|
|
||||||
mCache = null;
|
if (mLoader != null) {
|
||||||
getBitmap();
|
mLoader.cancel(false /* interrupt */);
|
||||||
mBar.updateMediaMetaData(true /* metaDataChanged */, true /* allowEnterAnimation */);
|
}
|
||||||
|
|
||||||
|
final int currentUser = mCurrentUserId;
|
||||||
|
final UserHandle selectedUser = mSelectedUser;
|
||||||
|
mLoader = new AsyncTask<Void, Void, LoaderResult>() {
|
||||||
|
@Override
|
||||||
|
protected LoaderResult doInBackground(Void... params) {
|
||||||
|
return loadBitmap(currentUser, selectedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(LoaderResult result) {
|
||||||
|
super.onPostExecute(result);
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.success) {
|
||||||
|
mCached = true;
|
||||||
|
mCache = result.bitmap;
|
||||||
|
mBar.updateMediaMetaData(
|
||||||
|
true /* metaDataChanged */, true /* allowEnterAnimation */);
|
||||||
|
}
|
||||||
|
mLoader = null;
|
||||||
|
}
|
||||||
|
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class LoaderResult {
|
||||||
|
public final boolean success;
|
||||||
|
public final Bitmap bitmap;
|
||||||
|
|
||||||
|
LoaderResult(boolean success, Bitmap bitmap) {
|
||||||
|
this.success = success;
|
||||||
|
this.bitmap = bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
static LoaderResult success(Bitmap b) {
|
||||||
|
return new LoaderResult(true, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LoaderResult fail() {
|
||||||
|
return new LoaderResult(false, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user