Align system time 12-24 hour format in the time slot information
align the time format configuration in the Settings -> System -> Date/Time to show 12- or 24- hour format in the usage time slot screenshot: https://screenshot.googleplex.com/3w2SFvBLpC5oHBs screenshot: https://screenshot.googleplex.com/86uCq6R4PKEg9RD screenshot: https://screenshot.googleplex.com/3easrEFMQdZEjGP screenshot: https://screenshot.googleplex.com/7dPWVJNTbSShFPa Bug: 187783891 Test: make SettingsRoboTests Change-Id: I9af0a69bd7c67562526bd5ee74a657635068ef44
This commit is contained in:
@@ -24,6 +24,7 @@ import android.os.Bundle;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.text.format.DateFormat;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
@@ -47,7 +48,6 @@ import com.android.settingslib.core.lifecycle.events.OnResume;
|
|||||||
import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
|
import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
|
||||||
import com.android.settingslib.utils.StringUtil;
|
import com.android.settingslib.utils.StringUtil;
|
||||||
|
|
||||||
import java.time.Clock;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -88,6 +88,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
|||||||
@VisibleForTesting long[] mBatteryHistoryKeys;
|
@VisibleForTesting long[] mBatteryHistoryKeys;
|
||||||
@VisibleForTesting int mTrapezoidIndex = BatteryChartView.SELECTED_INDEX_INVALID;
|
@VisibleForTesting int mTrapezoidIndex = BatteryChartView.SELECTED_INDEX_INVALID;
|
||||||
|
|
||||||
|
private boolean mIs24HourFormat = false;
|
||||||
|
|
||||||
private final String mPreferenceKey;
|
private final String mPreferenceKey;
|
||||||
private final SettingsActivity mActivity;
|
private final SettingsActivity mActivity;
|
||||||
private final InstrumentedPreferenceFragment mFragment;
|
private final InstrumentedPreferenceFragment mFragment;
|
||||||
@@ -110,6 +112,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
|||||||
mActivity = activity;
|
mActivity = activity;
|
||||||
mFragment = fragment;
|
mFragment = fragment;
|
||||||
mPreferenceKey = preferenceKey;
|
mPreferenceKey = preferenceKey;
|
||||||
|
mIs24HourFormat = DateFormat.is24HourFormat(context);
|
||||||
mNotAllowShowSummaryPackages = context.getResources()
|
mNotAllowShowSummaryPackages = context.getResources()
|
||||||
.getTextArray(R.array.allowlist_hide_summary_in_battery_usage);
|
.getTextArray(R.array.allowlist_hide_summary_in_battery_usage);
|
||||||
mNotAllowShowEntryPackages = context.getResources()
|
mNotAllowShowEntryPackages = context.getResources()
|
||||||
@@ -144,6 +147,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
|||||||
BatteryDiffEntry.clearCache();
|
BatteryDiffEntry.clearCache();
|
||||||
Log.d(TAG, "clear icon and label cache since uiMode is changed");
|
Log.d(TAG, "clear icon and label cache since uiMode is changed");
|
||||||
}
|
}
|
||||||
|
mIs24HourFormat = DateFormat.is24HourFormat(mContext);
|
||||||
mMetricsFeatureProvider.action(mPrefContext, SettingsEnums.OPEN_BATTERY_USAGE);
|
mMetricsFeatureProvider.action(mPrefContext, SettingsEnums.OPEN_BATTERY_USAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,10 +497,10 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final String fromHour = ConvertUtils.utcToLocalTimeHour(
|
final String fromHour = ConvertUtils.utcToLocalTimeHour(
|
||||||
mBatteryHistoryKeys[mTrapezoidIndex * 2]);
|
mBatteryHistoryKeys[mTrapezoidIndex * 2], mIs24HourFormat);
|
||||||
final String toHour = ConvertUtils.utcToLocalTimeHour(
|
final String toHour = ConvertUtils.utcToLocalTimeHour(
|
||||||
mBatteryHistoryKeys[(mTrapezoidIndex + 1) * 2]);
|
mBatteryHistoryKeys[(mTrapezoidIndex + 1) * 2], mIs24HourFormat);
|
||||||
return String.format("%s-%s", fromHour, toHour);
|
return String.format("%s - %s", fromHour, toHour);
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@@ -563,21 +567,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
|||||||
if (mBatteryChartView == null || mBatteryHistoryKeys == null) {
|
if (mBatteryChartView == null || mBatteryHistoryKeys == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
long latestTimestamp =
|
final long latestTimestamp =
|
||||||
mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1];
|
mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1];
|
||||||
// Uses the current time if we don't have history data.
|
mBatteryChartView.setLatestTimestamp(latestTimestamp);
|
||||||
if (latestTimestamp == 0) {
|
|
||||||
latestTimestamp = Clock.systemUTC().millis();
|
|
||||||
}
|
|
||||||
// Generates timestamp label for chart graph (every 8 hours).
|
|
||||||
final long timeSlotOffset = DateUtils.HOUR_IN_MILLIS * 8;
|
|
||||||
final String[] timestampLabels = new String[4];
|
|
||||||
for (int index = 0; index < timestampLabels.length; index++) {
|
|
||||||
timestampLabels[index] =
|
|
||||||
ConvertUtils.utcToLocalTimeHour(
|
|
||||||
latestTimestamp - (3 - index) * timeSlotOffset);
|
|
||||||
}
|
|
||||||
mBatteryChartView.setTimestamps(timestampLabels);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String utcToLocalTime(long[] timestamps) {
|
private static String utcToLocalTime(long[] timestamps) {
|
||||||
|
@@ -25,6 +25,8 @@ import android.graphics.Paint;
|
|||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.text.format.DateFormat;
|
||||||
|
import android.text.format.DateUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
@@ -40,6 +42,7 @@ import com.android.settings.R;
|
|||||||
import com.android.settings.overlay.FeatureFactory;
|
import com.android.settings.overlay.FeatureFactory;
|
||||||
import com.android.settingslib.Utils;
|
import com.android.settingslib.Utils;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -74,6 +77,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
|||||||
private boolean mIsSlotsClickabled;
|
private boolean mIsSlotsClickabled;
|
||||||
|
|
||||||
@VisibleForTesting int mSelectedIndex;
|
@VisibleForTesting int mSelectedIndex;
|
||||||
|
@VisibleForTesting String[] mTimestamps;
|
||||||
|
|
||||||
// Colors for drawing the trapezoid shape and dividers.
|
// Colors for drawing the trapezoid shape and dividers.
|
||||||
private int mTrapezoidColor;
|
private int mTrapezoidColor;
|
||||||
@@ -84,7 +88,6 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
|||||||
private final Rect[] mPercentageBounds =
|
private final Rect[] mPercentageBounds =
|
||||||
new Rect[] {new Rect(), new Rect(), new Rect()};
|
new Rect[] {new Rect(), new Rect(), new Rect()};
|
||||||
// For drawing the timestamp information.
|
// For drawing the timestamp information.
|
||||||
private String[] mTimestamps;
|
|
||||||
private final Rect[] mTimestampsBounds =
|
private final Rect[] mTimestampsBounds =
|
||||||
new Rect[] {new Rect(), new Rect(), new Rect(), new Rect()};
|
new Rect[] {new Rect(), new Rect(), new Rect(), new Rect()};
|
||||||
|
|
||||||
@@ -116,6 +119,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
|||||||
setSelectedIndex(SELECTED_INDEX_ALL);
|
setSelectedIndex(SELECTED_INDEX_ALL);
|
||||||
setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT);
|
setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT);
|
||||||
setClickable(false);
|
setClickable(false);
|
||||||
|
setLatestTimestamp(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the total trapezoid count for drawing. */
|
/** Sets the total trapezoid count for drawing. */
|
||||||
@@ -182,12 +186,21 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
|||||||
requestLayout();
|
requestLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets timestamps for drawing into x-axis information. */
|
/** Sets the latest timestamp for drawing into x-axis information. */
|
||||||
public void setTimestamps(String[] timestamps) {
|
public void setLatestTimestamp(long latestTimestamp) {
|
||||||
mTimestamps = timestamps;
|
if (latestTimestamp == 0) {
|
||||||
if (timestamps != null
|
latestTimestamp = Clock.systemUTC().millis();
|
||||||
&& timestamps.length != DEFAULT_TIMESTAMP_COUNT) {
|
}
|
||||||
mTimestamps = null;
|
if (mTimestamps == null) {
|
||||||
|
mTimestamps = new String[DEFAULT_TIMESTAMP_COUNT];
|
||||||
|
}
|
||||||
|
final long timeSlotOffset = DateUtils.HOUR_IN_MILLIS * 8;
|
||||||
|
final boolean is24HourFormat = DateFormat.is24HourFormat(getContext());
|
||||||
|
for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
|
||||||
|
mTimestamps[index] =
|
||||||
|
ConvertUtils.utcToLocalTimeHour(
|
||||||
|
latestTimestamp - (3 - index) * timeSlotOffset,
|
||||||
|
is24HourFormat);
|
||||||
}
|
}
|
||||||
requestLayout();
|
requestLayout();
|
||||||
}
|
}
|
||||||
|
@@ -71,6 +71,7 @@ public final class ConvertUtils {
|
|||||||
|
|
||||||
private static String sZoneId;
|
private static String sZoneId;
|
||||||
private static String sZoneIdForHour;
|
private static String sZoneIdForHour;
|
||||||
|
private static boolean sIs24HourFormat;
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static SimpleDateFormat sSimpleDateFormat;
|
static SimpleDateFormat sSimpleDateFormat;
|
||||||
@@ -134,11 +135,15 @@ public final class ConvertUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Converts UTC timestamp to local time hour data. */
|
/** Converts UTC timestamp to local time hour data. */
|
||||||
public static String utcToLocalTimeHour(long timestamp) {
|
public static String utcToLocalTimeHour(long timestamp, boolean is24HourFormat) {
|
||||||
final String currentZoneId = TimeZone.getDefault().getID();
|
final String currentZoneId = TimeZone.getDefault().getID();
|
||||||
if (!currentZoneId.equals(sZoneIdForHour) || sSimpleDateFormatForHour == null) {
|
if (!currentZoneId.equals(sZoneIdForHour)
|
||||||
|
|| sIs24HourFormat != is24HourFormat
|
||||||
|
|| sSimpleDateFormatForHour == null) {
|
||||||
sZoneIdForHour = currentZoneId;
|
sZoneIdForHour = currentZoneId;
|
||||||
sSimpleDateFormatForHour = new SimpleDateFormat("h aa", Locale.ENGLISH);
|
sIs24HourFormat = is24HourFormat;
|
||||||
|
sSimpleDateFormatForHour = new SimpleDateFormat(
|
||||||
|
sIs24HourFormat ? "HH" : "h aa", Locale.ENGLISH);
|
||||||
}
|
}
|
||||||
return sSimpleDateFormatForHour.format(new Date(timestamp))
|
return sSimpleDateFormatForHour.format(new Date(timestamp))
|
||||||
.toLowerCase(Locale.getDefault());
|
.toLowerCase(Locale.getDefault());
|
||||||
|
@@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
|
|||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Matchers.anyInt;
|
import static org.mockito.Matchers.anyInt;
|
||||||
|
import static org.mockito.Matchers.anyLong;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
@@ -604,7 +605,7 @@ public final class BatteryChartPreferenceControllerTest {
|
|||||||
mBatteryChartPreferenceController.setTimestampLabel();
|
mBatteryChartPreferenceController.setTimestampLabel();
|
||||||
|
|
||||||
verify(mBatteryChartPreferenceController.mBatteryChartView, never())
|
verify(mBatteryChartPreferenceController.mBatteryChartView, never())
|
||||||
.setTimestamps(any());
|
.setLatestTimestamp(anyLong());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -613,19 +614,11 @@ public final class BatteryChartPreferenceControllerTest {
|
|||||||
mBatteryChartPreferenceController.mBatteryChartView =
|
mBatteryChartPreferenceController.mBatteryChartView =
|
||||||
spy(new BatteryChartView(mContext));
|
spy(new BatteryChartView(mContext));
|
||||||
setUpBatteryHistoryKeys();
|
setUpBatteryHistoryKeys();
|
||||||
// Generates the expected result.
|
|
||||||
final String[] expectedResults = new String[4];
|
|
||||||
final long timeSlotOffset = DateUtils.HOUR_IN_MILLIS * 8;
|
|
||||||
for (int index = 0; index < expectedResults.length; index++) {
|
|
||||||
expectedResults[index] =
|
|
||||||
ConvertUtils.utcToLocalTimeHour(
|
|
||||||
1619247636826L - (3 - index) * timeSlotOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
mBatteryChartPreferenceController.setTimestampLabel();
|
mBatteryChartPreferenceController.setTimestampLabel();
|
||||||
|
|
||||||
verify(mBatteryChartPreferenceController.mBatteryChartView)
|
verify(mBatteryChartPreferenceController.mBatteryChartView)
|
||||||
.setTimestamps(expectedResults);
|
.setLatestTimestamp(1619247636826L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -638,7 +631,7 @@ public final class BatteryChartPreferenceControllerTest {
|
|||||||
mBatteryChartPreferenceController.setTimestampLabel();
|
mBatteryChartPreferenceController.setTimestampLabel();
|
||||||
|
|
||||||
verify(mBatteryChartPreferenceController.mBatteryChartView)
|
verify(mBatteryChartPreferenceController.mBatteryChartView)
|
||||||
.setTimestamps(any());
|
.setLatestTimestamp(anyLong());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -709,7 +702,7 @@ public final class BatteryChartPreferenceControllerTest {
|
|||||||
private void setUpBatteryHistoryKeys() {
|
private void setUpBatteryHistoryKeys() {
|
||||||
mBatteryChartPreferenceController.mBatteryHistoryKeys =
|
mBatteryChartPreferenceController.mBatteryHistoryKeys =
|
||||||
new long[] {1619196786769L, 0L, 1619247636826L};
|
new long[] {1619196786769L, 0L, 1619247636826L};
|
||||||
ConvertUtils.utcToLocalTimeHour(/*timestamp=*/ 0);
|
ConvertUtils.utcToLocalTimeHour(/*timestamp=*/ 0, /*is24HourFormat=*/ false);
|
||||||
// Simulates the locale in GMT.
|
// Simulates the locale in GMT.
|
||||||
ConvertUtils.sSimpleDateFormatForHour
|
ConvertUtils.sSimpleDateFormatForHour
|
||||||
.setTimeZone(TimeZone.getTimeZone("GMT"));
|
.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||||
|
@@ -41,6 +41,7 @@ import org.robolectric.RuntimeEnvironment;
|
|||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
public final class BatteryChartViewTest {
|
public final class BatteryChartViewTest {
|
||||||
@@ -227,4 +228,20 @@ public final class BatteryChartViewTest {
|
|||||||
verify(mBatteryChartView.mHandler)
|
verify(mBatteryChartView.mHandler)
|
||||||
.postDelayed(mBatteryChartView.mUpdateClickableStateRun, 500L);
|
.postDelayed(mBatteryChartView.mUpdateClickableStateRun, 500L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetLatestTimestamp_generateExpectedTimestamps() {
|
||||||
|
final long timestamp = 1619196786769L;
|
||||||
|
ConvertUtils.sSimpleDateFormatForHour = null;
|
||||||
|
// Invokes the method first to create the SimpleDateFormat.
|
||||||
|
ConvertUtils.utcToLocalTimeHour(/*timestamp=*/ 0, /*is24HourFormat=*/ false);
|
||||||
|
ConvertUtils.sSimpleDateFormatForHour
|
||||||
|
.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
|
||||||
|
final String[] expectedTimestamps =
|
||||||
|
new String[] {"9 am", "5 pm", "1 am", "9 am"};
|
||||||
|
|
||||||
|
mBatteryChartView.setLatestTimestamp(timestamp);
|
||||||
|
|
||||||
|
assertThat(mBatteryChartView.mTimestamps).isEqualTo(expectedTimestamps);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -316,28 +316,41 @@ public final class ConvertUtilsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUtcToLocalTime_returnExpectedResult() {
|
public void testUtcToLocalTime_returnExpectedResult() {
|
||||||
final long timestamp = 1619196786769L;
|
final long timestamp = 1619196786769L;
|
||||||
ConvertUtils.sSimpleDateFormat = null;
|
ConvertUtils.sSimpleDateFormat = null;
|
||||||
// Invokes the method first to create the SimpleDateFormat.
|
// Invokes the method first to create the SimpleDateFormat.
|
||||||
ConvertUtils.utcToLocalTime(/*timestamp=*/ 0);
|
ConvertUtils.utcToLocalTime(/*timestamp=*/ 0);
|
||||||
ConvertUtils.sSimpleDateFormat
|
ConvertUtils.sSimpleDateFormat
|
||||||
.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
|
.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
|
||||||
|
|
||||||
assertThat(ConvertUtils.utcToLocalTime(timestamp))
|
assertThat(ConvertUtils.utcToLocalTime(timestamp))
|
||||||
.isEqualTo("Apr 23,2021 09:53:06");
|
.isEqualTo("Apr 23,2021 09:53:06");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUtcToLocalTmeHour_returnExpectedResult() {
|
public void testUtcToLocalTimeHour_12HourFormat_returnExpectedResult() {
|
||||||
final long timestamp = 1619196786769L;
|
final long timestamp = 1619196786769L;
|
||||||
ConvertUtils.sSimpleDateFormatForHour = null;
|
ConvertUtils.sSimpleDateFormatForHour = null;
|
||||||
// Invokes the method first to create the SimpleDateFormat.
|
// Invokes the method first to create the SimpleDateFormat.
|
||||||
ConvertUtils.utcToLocalTimeHour(/*timestamp=*/ 0);
|
ConvertUtils.utcToLocalTimeHour(/*timestamp=*/ 0, /*is24HourFormat=*/ false);
|
||||||
ConvertUtils.sSimpleDateFormatForHour
|
ConvertUtils.sSimpleDateFormatForHour
|
||||||
.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
|
.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
|
||||||
|
|
||||||
assertThat(ConvertUtils.utcToLocalTimeHour(timestamp))
|
assertThat(ConvertUtils.utcToLocalTimeHour(
|
||||||
.isEqualTo("9 am");
|
timestamp, /*is24HourFormat=*/ false)).isEqualTo("9 am");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUtcToLocalTimeHour_24HourFormat_returnExpectedResult() {
|
||||||
|
final long timestamp = 1619196786769L;
|
||||||
|
ConvertUtils.sSimpleDateFormatForHour = null;
|
||||||
|
// Invokes the method first to create the SimpleDateFormat.
|
||||||
|
ConvertUtils.utcToLocalTimeHour(/*timestamp=*/ 0, /*is24HourFormat=*/ true);
|
||||||
|
ConvertUtils.sSimpleDateFormatForHour
|
||||||
|
.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
|
||||||
|
|
||||||
|
assertThat(ConvertUtils.utcToLocalTimeHour(
|
||||||
|
timestamp, /*is24HourFormat=*/ true)).isEqualTo("09");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BatteryHistEntry createBatteryHistEntry(
|
private static BatteryHistEntry createBatteryHistEntry(
|
||||||
|
Reference in New Issue
Block a user