Removing released features
Bug: 179224463 Test: Presubmit Change-Id: I6aa989fa8e52398112cca2c7f4bed0ae69881f7b
This commit is contained in:
@@ -29,7 +29,6 @@ import com.android.launcher3.util.ActivityTracker;
|
||||
*/
|
||||
public class HotseatEduActivity extends Activity {
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -49,7 +48,7 @@ public class HotseatEduActivity extends Activity {
|
||||
@Override
|
||||
public boolean init(BaseActivity activity, boolean alreadyOnHome) {
|
||||
QuickstepLauncher launcher = (QuickstepLauncher) activity;
|
||||
if (launcher != null && launcher.getHotseatPredictionController() != null) {
|
||||
if (launcher != null) {
|
||||
launcher.getHotseatPredictionController().showEdu();
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.launcher3.hybridhotseat;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.launcher3.logging.FileLog;
|
||||
import com.android.launcher3.util.Executors;
|
||||
import com.android.launcher3.util.MainThreadInitializedObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.DateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Helper class to allow hot seat file logging
|
||||
*/
|
||||
public class HotseatFileLog {
|
||||
|
||||
public static final int LOG_DAYS = 10;
|
||||
private static final String FILE_NAME_PREFIX = "hotseat-log-";
|
||||
private static final DateFormat DATE_FORMAT =
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
|
||||
public static final MainThreadInitializedObject<HotseatFileLog> INSTANCE =
|
||||
new MainThreadInitializedObject<>(HotseatFileLog::new);
|
||||
|
||||
|
||||
private final Handler mHandler = new Handler(
|
||||
Executors.createAndStartNewLooper("hotseat-logger"));
|
||||
private final File mLogsDir;
|
||||
private PrintWriter mCurrentWriter;
|
||||
private String mFileName;
|
||||
|
||||
private HotseatFileLog(Context context) {
|
||||
mLogsDir = context.getFilesDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints log values to disk
|
||||
*/
|
||||
public void log(String tag, String msg) {
|
||||
String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg);
|
||||
|
||||
mHandler.post(() -> {
|
||||
synchronized (this) {
|
||||
PrintWriter writer = getWriter();
|
||||
if (writer != null) {
|
||||
writer.println(out);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private PrintWriter getWriter() {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
String fName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % 10);
|
||||
if (fName.equals(mFileName)) return mCurrentWriter;
|
||||
|
||||
boolean append = false;
|
||||
File logFile = new File(mLogsDir, fName);
|
||||
if (logFile.exists()) {
|
||||
Calendar modifiedTime = Calendar.getInstance();
|
||||
modifiedTime.setTimeInMillis(logFile.lastModified());
|
||||
|
||||
// If the file was modified more that 36 hours ago, purge the file.
|
||||
// We use instead of 24 to account for day-365 followed by day-1
|
||||
modifiedTime.add(Calendar.HOUR, 36);
|
||||
append = cal.before(modifiedTime);
|
||||
}
|
||||
|
||||
|
||||
if (mCurrentWriter != null) {
|
||||
mCurrentWriter.close();
|
||||
}
|
||||
try {
|
||||
mCurrentWriter = new PrintWriter(new FileWriter(logFile, append));
|
||||
mFileName = fName;
|
||||
} catch (Exception ex) {
|
||||
Log.e("HotseatLogs", "Error writing logs to file", ex);
|
||||
closeWriter();
|
||||
}
|
||||
return mCurrentWriter;
|
||||
}
|
||||
|
||||
|
||||
private synchronized void closeWriter() {
|
||||
mFileName = null;
|
||||
if (mCurrentWriter != null) {
|
||||
mCurrentWriter.close();
|
||||
}
|
||||
mCurrentWriter = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of all log files
|
||||
*/
|
||||
public synchronized File[] getLogFiles() {
|
||||
File[] files = new File[LOG_DAYS + FileLog.LOG_DAYS];
|
||||
//include file log files here
|
||||
System.arraycopy(FileLog.getLogFiles(), 0, files, 0, FileLog.LOG_DAYS);
|
||||
|
||||
closeWriter();
|
||||
for (int i = 0; i < LOG_DAYS; i++) {
|
||||
files[FileLog.LOG_DAYS + i] = new File(mLogsDir, FILE_NAME_PREFIX + i);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.ComponentName;
|
||||
import android.os.Process;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -40,7 +39,6 @@ import com.android.launcher3.Hotseat;
|
||||
import com.android.launcher3.InvariantDeviceProfile;
|
||||
import com.android.launcher3.LauncherSettings;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.Utilities;
|
||||
import com.android.launcher3.anim.AnimationSuccessListener;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.dragndrop.DragController;
|
||||
@@ -446,16 +444,6 @@ public class HotseatPredictionController implements DragController.DragListener,
|
||||
* Logs rank info based on current list of predicted items
|
||||
*/
|
||||
public void logLaunchedAppRankingInfo(@NonNull ItemInfo itemInfo, InstanceId instanceId) {
|
||||
if (Utilities.IS_DEBUG_DEVICE) {
|
||||
final String pkg = itemInfo.getTargetComponent() != null
|
||||
? itemInfo.getTargetComponent().getPackageName() : "unknown";
|
||||
HotseatFileLog.INSTANCE.get(mLauncher).log("UserEvent",
|
||||
"appLaunch: packageName:" + pkg + ",isWorkApp:" + (itemInfo.user != null
|
||||
&& !Process.myUserHandle().equals(itemInfo.user))
|
||||
+ ",launchLocation:" + itemInfo.container);
|
||||
}
|
||||
|
||||
|
||||
ComponentName targetCN = itemInfo.getTargetComponent();
|
||||
if (targetCN == null) {
|
||||
return;
|
||||
|
||||
@@ -160,7 +160,7 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView implements
|
||||
public boolean performAccessibilityAction(int action, ItemInfo info) {
|
||||
QuickstepLauncher launcher = Launcher.cast(Launcher.getLauncher(getContext()));
|
||||
if (action == PIN_PREDICTION) {
|
||||
if (launcher == null || launcher.getHotseatPredictionController() == null) {
|
||||
if (launcher == null) {
|
||||
return false;
|
||||
}
|
||||
HotseatPredictionController controller = launcher.getHotseatPredictionController();
|
||||
|
||||
@@ -49,7 +49,6 @@ import com.android.launcher3.allapps.AllAppsContainerView;
|
||||
import com.android.launcher3.allapps.search.SearchAdapterProvider;
|
||||
import com.android.launcher3.anim.AnimatorPlaybackController;
|
||||
import com.android.launcher3.appprediction.PredictionRowView;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.hybridhotseat.HotseatPredictionController;
|
||||
import com.android.launcher3.logging.InstanceId;
|
||||
import com.android.launcher3.logging.StatsLogManager.StatsLogger;
|
||||
@@ -104,9 +103,7 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
|
||||
@Override
|
||||
protected void setupViews() {
|
||||
super.setupViews();
|
||||
if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
|
||||
mHotseatPredictionController = new HotseatPredictionController(this);
|
||||
}
|
||||
mHotseatPredictionController = new HotseatPredictionController(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -141,9 +138,7 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
|
||||
}
|
||||
logger.log(LAUNCHER_APP_LAUNCH_TAP);
|
||||
|
||||
if (mHotseatPredictionController != null) {
|
||||
mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
|
||||
}
|
||||
mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,10 +161,8 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
|
||||
|
||||
@Override
|
||||
public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
|
||||
if (mHotseatPredictionController != null) {
|
||||
// Only pause is taskbar controller is not present
|
||||
mHotseatPredictionController.setPauseUIUpdate(getTaskbarController() == null);
|
||||
}
|
||||
// Only pause is taskbar controller is not present
|
||||
mHotseatPredictionController.setPauseUIUpdate(getTaskbarController() == null);
|
||||
return super.startActivitySafely(v, intent, item);
|
||||
}
|
||||
|
||||
@@ -181,7 +174,7 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
|
||||
onStateOrResumeChanging((getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0);
|
||||
}
|
||||
|
||||
if (mHotseatPredictionController != null && ((changeBits & ACTIVITY_STATE_STARTED) != 0
|
||||
if (((changeBits & ACTIVITY_STATE_STARTED) != 0
|
||||
|| (changeBits & getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) {
|
||||
mHotseatPredictionController.setPauseUIUpdate(false);
|
||||
}
|
||||
@@ -195,12 +188,8 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
|
||||
|
||||
@Override
|
||||
public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
|
||||
if (mHotseatPredictionController != null) {
|
||||
return Stream.concat(super.getSupportedShortcuts(),
|
||||
Stream.of(mHotseatPredictionController));
|
||||
} else {
|
||||
return super.getSupportedShortcuts();
|
||||
}
|
||||
return Stream.concat(
|
||||
super.getSupportedShortcuts(), Stream.of(mHotseatPredictionController));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,8 +216,7 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
|
||||
mAllAppsPredictions = item;
|
||||
getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
|
||||
.setPredictedApps(item.items);
|
||||
} else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION
|
||||
&& mHotseatPredictionController != null) {
|
||||
} else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
|
||||
mHotseatPredictionController.setPredictedItems(item);
|
||||
}
|
||||
}
|
||||
@@ -246,10 +234,7 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
getAppsView().getSearchUiManager().destroy();
|
||||
if (mHotseatPredictionController != null) {
|
||||
mHotseatPredictionController.destroy();
|
||||
mHotseatPredictionController = null;
|
||||
}
|
||||
mHotseatPredictionController.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package com.android.quickstep.util;
|
||||
|
||||
import static com.android.launcher3.config.FeatureFlags.ENABLE_SYSTEM_VELOCITY_PROVIDER;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.view.MotionEvent;
|
||||
@@ -51,7 +49,7 @@ public class MotionPauseDetector {
|
||||
private final Alarm mForcePauseTimeout;
|
||||
private final boolean mMakePauseHarderToTrigger;
|
||||
private final Context mContext;
|
||||
private final VelocityProvider mVelocityProvider;
|
||||
private final SystemVelocityProvider mVelocityProvider;
|
||||
|
||||
private Float mPreviousVelocity = null;
|
||||
|
||||
@@ -88,8 +86,7 @@ public class MotionPauseDetector {
|
||||
mForcePauseTimeout = new Alarm();
|
||||
mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
|
||||
mMakePauseHarderToTrigger = makePauseHarderToTrigger;
|
||||
mVelocityProvider = ENABLE_SYSTEM_VELOCITY_PROVIDER.get()
|
||||
? new SystemVelocityProvider(axis) : new LinearVelocityProvider(axis);
|
||||
mVelocityProvider = new SystemVelocityProvider(axis);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,8 +121,8 @@ public class MotionPauseDetector {
|
||||
mForcePauseTimeout.setAlarm(mMakePauseHarderToTrigger
|
||||
? HARDER_TRIGGER_TIMEOUT
|
||||
: FORCE_PAUSE_TIMEOUT);
|
||||
Float newVelocity = mVelocityProvider.addMotionEvent(ev, pointerIndex);
|
||||
if (newVelocity != null && mPreviousVelocity != null) {
|
||||
float newVelocity = mVelocityProvider.addMotionEvent(ev, pointerIndex);
|
||||
if (mPreviousVelocity != null) {
|
||||
checkMotionPaused(newVelocity, mPreviousVelocity, ev.getEventTime());
|
||||
}
|
||||
mPreviousVelocity = newVelocity;
|
||||
@@ -210,58 +207,7 @@ public class MotionPauseDetector {
|
||||
default void onMotionPauseChanged(boolean isPaused) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to abstract out velocity calculations
|
||||
*/
|
||||
protected interface VelocityProvider {
|
||||
|
||||
/**
|
||||
* Adds a new motion events, and returns the velocity at this point, or null if
|
||||
* the velocity is not available
|
||||
*/
|
||||
Float addMotionEvent(MotionEvent ev, int pointer);
|
||||
|
||||
/**
|
||||
* Clears all stored motion event records
|
||||
*/
|
||||
void clear();
|
||||
}
|
||||
|
||||
private static class LinearVelocityProvider implements VelocityProvider {
|
||||
|
||||
private Long mPreviousTime = null;
|
||||
private Float mPreviousPosition = null;
|
||||
|
||||
private final int mAxis;
|
||||
|
||||
LinearVelocityProvider(int axis) {
|
||||
mAxis = axis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float addMotionEvent(MotionEvent ev, int pointer) {
|
||||
long time = ev.getEventTime();
|
||||
float position = ev.getAxisValue(mAxis, pointer);
|
||||
Float velocity = null;
|
||||
|
||||
if (mPreviousTime != null && mPreviousPosition != null) {
|
||||
long changeInTime = Math.max(1, time - mPreviousTime);
|
||||
float changeInPosition = position - mPreviousPosition;
|
||||
velocity = changeInPosition / changeInTime;
|
||||
}
|
||||
mPreviousTime = time;
|
||||
mPreviousPosition = position;
|
||||
return velocity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
mPreviousTime = null;
|
||||
mPreviousPosition = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SystemVelocityProvider implements VelocityProvider {
|
||||
private static class SystemVelocityProvider {
|
||||
|
||||
private final VelocityTracker mVelocityTracker;
|
||||
private final int mAxis;
|
||||
@@ -271,8 +217,11 @@ public class MotionPauseDetector {
|
||||
mAxis = axis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float addMotionEvent(MotionEvent ev, int pointer) {
|
||||
/**
|
||||
* Adds a new motion events, and returns the velocity at this point, or null if
|
||||
* the velocity is not available
|
||||
*/
|
||||
public float addMotionEvent(MotionEvent ev, int pointer) {
|
||||
mVelocityTracker.addMovement(ev);
|
||||
mVelocityTracker.computeCurrentVelocity(1); // px / ms
|
||||
return mAxis == MotionEvent.AXIS_X
|
||||
@@ -280,7 +229,9 @@ public class MotionPauseDetector {
|
||||
: mVelocityTracker.getYVelocity(pointer);
|
||||
}
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Clears all stored motion event records
|
||||
*/
|
||||
public void clear() {
|
||||
mVelocityTracker.clear();
|
||||
}
|
||||
|
||||
@@ -66,8 +66,7 @@ public class QuickstepOnboardingPrefs extends OnboardingPrefs<QuickstepLauncher>
|
||||
});
|
||||
}
|
||||
|
||||
if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() && !hasReachedMaxCount(
|
||||
HOTSEAT_DISCOVERY_TIP_COUNT)) {
|
||||
if (!hasReachedMaxCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
|
||||
stateManager.addStateListener(new StateListener<LauncherState>() {
|
||||
boolean mFromAllApps = false;
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ package com.android.launcher3;
|
||||
|
||||
import static com.android.launcher3.Utilities.getDevicePrefs;
|
||||
import static com.android.launcher3.Utilities.getPointString;
|
||||
import static com.android.launcher3.config.FeatureFlags.APPLY_CONFIG_AT_RUNTIME;
|
||||
import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
|
||||
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
|
||||
import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
|
||||
@@ -174,8 +173,7 @@ public class InvariantDeviceProfile {
|
||||
.putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(numColumns, numRows))
|
||||
.apply();
|
||||
|
||||
mConfigMonitor = new ConfigMonitor(context,
|
||||
APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
|
||||
mConfigMonitor = new ConfigMonitor(context, this::onConfigChanged);
|
||||
mOverlayMonitor = new OverlayMonitor(context);
|
||||
}
|
||||
|
||||
@@ -317,11 +315,6 @@ public class InvariantDeviceProfile {
|
||||
mChangeListeners.remove(listener);
|
||||
}
|
||||
|
||||
private void killProcess(Context context) {
|
||||
Log.e("ConfigMonitor", "restarting launcher");
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
}
|
||||
|
||||
public void verifyConfigChangedInBackground(final Context context) {
|
||||
String savedIconMaskPath = getDevicePrefs(context).getString(KEY_ICON_PATH_REF, "");
|
||||
// Good place to check if grid size changed in themepicker when launcher was dead.
|
||||
|
||||
@@ -72,9 +72,6 @@ public final class FeatureFlags {
|
||||
"PROMISE_APPS_NEW_INSTALLS", true,
|
||||
"Adds a promise icon to the home screen for new install sessions.");
|
||||
|
||||
public static final BooleanFlag APPLY_CONFIG_AT_RUNTIME = getDebugFlag(
|
||||
"APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
|
||||
|
||||
public static final BooleanFlag QUICKSTEP_SPRINGS = getDebugFlag(
|
||||
"QUICKSTEP_SPRINGS", true, "Enable springs for quickstep animations");
|
||||
|
||||
@@ -133,9 +130,6 @@ public final class FeatureFlags {
|
||||
"ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
|
||||
"Allow Launcher to handle nav bar gestures while Assistant is running over it");
|
||||
|
||||
public static final BooleanFlag ENABLE_HYBRID_HOTSEAT = getDebugFlag(
|
||||
"ENABLE_HYBRID_HOTSEAT", true, "Fill gaps in hotseat with predicted apps");
|
||||
|
||||
public static final BooleanFlag HOTSEAT_MIGRATE_TO_FOLDER = getDebugFlag(
|
||||
"HOTSEAT_MIGRATE_TO_FOLDER", false, "Should move hotseat items into a folder");
|
||||
|
||||
@@ -175,10 +169,6 @@ public final class FeatureFlags {
|
||||
"Replace Smartspace with the enhanced version. "
|
||||
+ "Ignored if ENABLE_SMARTSPACE_UNIVERSAL is enabled.");
|
||||
|
||||
public static final BooleanFlag ENABLE_SYSTEM_VELOCITY_PROVIDER = getDebugFlag(
|
||||
"ENABLE_SYSTEM_VELOCITY_PROVIDER", true,
|
||||
"Use system VelocityTracker's algorithm for motion pause detection.");
|
||||
|
||||
public static final BooleanFlag ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS =
|
||||
getDebugFlag(
|
||||
"ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS", false,
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.util.Pair;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.util.IOUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@@ -43,7 +42,7 @@ public final class FileLog {
|
||||
private static Handler sHandler = null;
|
||||
private static File sLogsDirectory = null;
|
||||
|
||||
public static final int LOG_DAYS = FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() ? 10 : 4;
|
||||
public static final int LOG_DAYS = 4;
|
||||
|
||||
public static void setDir(File logsDir) {
|
||||
if (ENABLED) {
|
||||
|
||||
Reference in New Issue
Block a user