Merge branch 'swap-crash-serving-fdroid' into 'master'
Fix crash when trying to swap. See merge request !563
This commit is contained in:
commit
312bc9f503
@ -250,8 +250,10 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
|
|||||||
|
|
||||||
private void checkRepoAddress() {
|
private void checkRepoAddress() {
|
||||||
if (repoAddress == null || apkName == null) {
|
if (repoAddress == null || apkName == null) {
|
||||||
throw new IllegalStateException("Apk needs to have both Schema.ApkTable.Cols.REPO_ADDRESS and "
|
throw new IllegalStateException(
|
||||||
+ "Schema.ApkTable.Cols.NAME set in order to calculate URL.");
|
"Apk needs to have both Schema.ApkTable.Cols.REPO_ADDRESS and " +
|
||||||
|
"Schema.ApkTable.Cols.NAME set in order to calculate URL " +
|
||||||
|
"[package: " + packageName + ", versionCode: " + versionCode + ", repoId: " + repoId + "]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,14 +114,10 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static List<Apk> findByPackageName(Context context, String packageName) {
|
public static List<Apk> findByPackageName(Context context, String packageName) {
|
||||||
return findByPackageName(context, packageName, Cols.ALL);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Apk> findByPackageName(Context context, String packageName, String[] projection) {
|
|
||||||
ContentResolver resolver = context.getContentResolver();
|
ContentResolver resolver = context.getContentResolver();
|
||||||
final Uri uri = getAppUri(packageName);
|
final Uri uri = getAppUri(packageName);
|
||||||
final String sort = "apk." + Cols.VERSION_CODE + " DESC";
|
final String sort = "apk." + Cols.VERSION_CODE + " DESC";
|
||||||
Cursor cursor = resolver.query(uri, projection, null, null, sort);
|
Cursor cursor = resolver.query(uri, Cols.ALL, null, null, sort);
|
||||||
return cursorToList(cursor);
|
return cursorToList(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,13 +128,15 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
return cursorToList(cursor);
|
return cursorToList(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Apk get(Context context, Uri uri) {
|
@NonNull
|
||||||
return get(context, uri, Cols.ALL);
|
public static List<Apk> findAppVersionsByRepo(Context context, App app, Repo repo) {
|
||||||
|
ContentResolver resolver = context.getContentResolver();
|
||||||
|
final Uri uri = getRepoUri(repo.getId(), app.packageName);
|
||||||
|
Cursor cursor = resolver.query(uri, Cols.ALL, null, null, null);
|
||||||
|
return cursorToList(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Apk get(Context context, Uri uri, String[] fields) {
|
private static Apk cursorToApk(Cursor cursor) {
|
||||||
ContentResolver resolver = context.getContentResolver();
|
|
||||||
Cursor cursor = resolver.query(uri, fields, null, null, null);
|
|
||||||
Apk apk = null;
|
Apk apk = null;
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
if (cursor.getCount() > 0) {
|
if (cursor.getCount() > 0) {
|
||||||
@ -150,6 +148,12 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
return apk;
|
return apk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Apk get(Context context, Uri uri) {
|
||||||
|
ContentResolver resolver = context.getContentResolver();
|
||||||
|
Cursor cursor = resolver.query(uri, Cols.ALL, null, null, null);
|
||||||
|
return cursorToApk(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static List<Apk> findApksByHash(Context context, String apkHash) {
|
public static List<Apk> findApksByHash(Context context, String apkHash) {
|
||||||
ContentResolver resolver = context.getContentResolver();
|
ContentResolver resolver = context.getContentResolver();
|
||||||
@ -167,10 +171,12 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
private static final int CODE_APK_ROW_ID = CODE_APKS + 1;
|
private static final int CODE_APK_ROW_ID = CODE_APKS + 1;
|
||||||
static final int CODE_APK_FROM_ANY_REPO = CODE_APK_ROW_ID + 1;
|
static final int CODE_APK_FROM_ANY_REPO = CODE_APK_ROW_ID + 1;
|
||||||
static final int CODE_APK_FROM_REPO = CODE_APK_FROM_ANY_REPO + 1;
|
static final int CODE_APK_FROM_REPO = CODE_APK_FROM_ANY_REPO + 1;
|
||||||
|
private static final int CODE_REPO_APP = CODE_APK_FROM_REPO + 1;
|
||||||
|
|
||||||
private static final String PROVIDER_NAME = "ApkProvider";
|
private static final String PROVIDER_NAME = "ApkProvider";
|
||||||
protected static final String PATH_APK_FROM_ANY_REPO = "apk-any-repo";
|
protected static final String PATH_APK_FROM_ANY_REPO = "apk-any-repo";
|
||||||
protected static final String PATH_APK_FROM_REPO = "apk-from-repo";
|
protected static final String PATH_APK_FROM_REPO = "apk-from-repo";
|
||||||
|
protected static final String PATH_REPO_APP = "repo-app";
|
||||||
private static final String PATH_APKS = "apks";
|
private static final String PATH_APKS = "apks";
|
||||||
private static final String PATH_APP = "app";
|
private static final String PATH_APP = "app";
|
||||||
private static final String PATH_REPO = "repo";
|
private static final String PATH_REPO = "repo";
|
||||||
@ -187,6 +193,7 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
PACKAGE_FIELDS.put(Cols.Package.PACKAGE_NAME, PackageTable.Cols.PACKAGE_NAME);
|
PACKAGE_FIELDS.put(Cols.Package.PACKAGE_NAME, PackageTable.Cols.PACKAGE_NAME);
|
||||||
|
|
||||||
MATCHER.addURI(getAuthority(), PATH_REPO + "/#", CODE_REPO);
|
MATCHER.addURI(getAuthority(), PATH_REPO + "/#", CODE_REPO);
|
||||||
|
MATCHER.addURI(getAuthority(), PATH_REPO_APP + "/#/*", CODE_REPO_APP);
|
||||||
MATCHER.addURI(getAuthority(), PATH_APK_FROM_ANY_REPO + "/#/*/*", CODE_APK_FROM_ANY_REPO);
|
MATCHER.addURI(getAuthority(), PATH_APK_FROM_ANY_REPO + "/#/*/*", CODE_APK_FROM_ANY_REPO);
|
||||||
MATCHER.addURI(getAuthority(), PATH_APK_FROM_ANY_REPO + "/#/*", CODE_APK_FROM_ANY_REPO);
|
MATCHER.addURI(getAuthority(), PATH_APK_FROM_ANY_REPO + "/#/*", CODE_APK_FROM_ANY_REPO);
|
||||||
MATCHER.addURI(getAuthority(), PATH_APK_FROM_REPO + "/#/#", CODE_APK_FROM_REPO);
|
MATCHER.addURI(getAuthority(), PATH_APK_FROM_REPO + "/#/#", CODE_APK_FROM_REPO);
|
||||||
@ -227,6 +234,15 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Uri getRepoUri(long repoId, String packageName) {
|
||||||
|
return getContentUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendPath(PATH_REPO_APP)
|
||||||
|
.appendPath(Long.toString(repoId))
|
||||||
|
.appendPath(packageName)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
public static Uri getApkFromAnyRepoUri(Apk apk) {
|
public static Uri getApkFromAnyRepoUri(Apk apk) {
|
||||||
return getApkFromAnyRepoUri(apk.packageName, apk.versionCode, null);
|
return getApkFromAnyRepoUri(apk.packageName, apk.versionCode, null);
|
||||||
}
|
}
|
||||||
@ -427,6 +443,13 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
QuerySelection query = new QuerySelection(selection, selectionArgs);
|
QuerySelection query = new QuerySelection(selection, selectionArgs);
|
||||||
|
|
||||||
switch (MATCHER.match(uri)) {
|
switch (MATCHER.match(uri)) {
|
||||||
|
case CODE_REPO_APP:
|
||||||
|
List<String> uriSegments = uri.getPathSegments();
|
||||||
|
Long repoId = Long.parseLong(uriSegments.get(1));
|
||||||
|
String packageName = uriSegments.get(2);
|
||||||
|
query = query.add(queryRepo(repoId)).add(queryPackage(packageName));
|
||||||
|
break;
|
||||||
|
|
||||||
case CODE_LIST:
|
case CODE_LIST:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ import org.fdroid.fdroid.localrepo.SwapService;
|
|||||||
import org.fdroid.fdroid.net.Downloader;
|
import org.fdroid.fdroid.net.Downloader;
|
||||||
import org.fdroid.fdroid.net.DownloaderService;
|
import org.fdroid.fdroid.net.DownloaderService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
@ -233,8 +234,13 @@ public class SwapAppsView extends ListView implements
|
|||||||
private class ViewHolder {
|
private class ViewHolder {
|
||||||
|
|
||||||
private final LocalBroadcastManager localBroadcastManager;
|
private final LocalBroadcastManager localBroadcastManager;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
private App app;
|
private App app;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Apk apk;
|
||||||
|
|
||||||
ProgressBar progressView;
|
ProgressBar progressView;
|
||||||
TextView nameView;
|
TextView nameView;
|
||||||
ImageView iconView;
|
ImageView iconView;
|
||||||
@ -290,8 +296,11 @@ public class SwapAppsView extends ListView implements
|
|||||||
public void onChange(boolean selfChange) {
|
public void onChange(boolean selfChange) {
|
||||||
Activity activity = getActivity();
|
Activity activity = getActivity();
|
||||||
if (activity != null) {
|
if (activity != null) {
|
||||||
app = AppProvider.Helper.findSpecificApp(getActivity().getContentResolver(),
|
app = AppProvider.Helper.findSpecificApp(
|
||||||
app.packageName, app.repoId, AppMetadataTable.Cols.ALL);
|
getActivity().getContentResolver(),
|
||||||
|
app.packageName,
|
||||||
|
app.repoId,
|
||||||
|
AppMetadataTable.Cols.ALL);
|
||||||
resetView();
|
resetView();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -305,14 +314,19 @@ public class SwapAppsView extends ListView implements
|
|||||||
if (this.app == null || !this.app.packageName.equals(app.packageName)) {
|
if (this.app == null || !this.app.packageName.equals(app.packageName)) {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
|
|
||||||
Context context = getContext();
|
List<Apk> availableApks = ApkProvider.Helper.findAppVersionsByRepo(getActivity(), app, repo);
|
||||||
Apk apk = ApkProvider.Helper.findApkFromAnyRepo(context,
|
if (availableApks.size() > 0) {
|
||||||
app.packageName, app.suggestedVersionCode);
|
// Swap repos only add one version of an app, so we will just ask for the first apk.
|
||||||
String urlString = apk.getUrl();
|
this.apk = availableApks.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO unregister receivers? or will they just die with this instance
|
if (apk != null) {
|
||||||
localBroadcastManager.registerReceiver(downloadReceiver,
|
String urlString = apk.getUrl();
|
||||||
DownloaderService.getIntentFilter(urlString));
|
|
||||||
|
// TODO unregister receivers? or will they just die with this instance
|
||||||
|
IntentFilter downloadFilter = DownloaderService.getIntentFilter(urlString);
|
||||||
|
localBroadcastManager.registerReceiver(downloadReceiver, downloadFilter);
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: Instead of continually unregistering and re-registering the observer
|
// NOTE: Instead of continually unregistering and re-registering the observer
|
||||||
// (with a different URI), this could equally be done by only having one
|
// (with a different URI), this could equally be done by only having one
|
||||||
@ -364,8 +378,8 @@ public class SwapAppsView extends ListView implements
|
|||||||
OnClickListener installListener = new OnClickListener() {
|
OnClickListener installListener = new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
if (app.hasUpdates() || app.compatible) {
|
if (apk != null && (app.hasUpdates() || app.compatible)) {
|
||||||
getActivity().install(app);
|
getActivity().install(app, apk);
|
||||||
showProgress();
|
showProgress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,6 @@ import org.fdroid.fdroid.Preferences;
|
|||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.data.Apk;
|
import org.fdroid.fdroid.data.Apk;
|
||||||
import org.fdroid.fdroid.data.ApkProvider;
|
|
||||||
import org.fdroid.fdroid.data.App;
|
import org.fdroid.fdroid.data.App;
|
||||||
import org.fdroid.fdroid.data.NewRepoConfig;
|
import org.fdroid.fdroid.data.NewRepoConfig;
|
||||||
import org.fdroid.fdroid.installer.InstallManagerService;
|
import org.fdroid.fdroid.installer.InstallManagerService;
|
||||||
@ -766,8 +765,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void install(@NonNull final App app) {
|
public void install(@NonNull final App app, @NonNull final Apk apk) {
|
||||||
final Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
|
|
||||||
Uri downloadUri = Uri.parse(apk.getUrl());
|
Uri downloadUri = Uri.parse(apk.getUrl());
|
||||||
localBroadcastManager.registerReceiver(installReceiver,
|
localBroadcastManager.registerReceiver(installReceiver,
|
||||||
Installer.getInstallIntentFilter(downloadUri));
|
Installer.getInstallIntentFilter(downloadUri));
|
||||||
|
@ -9,6 +9,7 @@ import org.fdroid.fdroid.data.ApkProvider;
|
|||||||
import org.fdroid.fdroid.data.App;
|
import org.fdroid.fdroid.data.App;
|
||||||
import org.fdroid.fdroid.data.AppProvider;
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||||
|
import org.fdroid.fdroid.data.Repo;
|
||||||
import org.fdroid.fdroid.data.Schema.ApkTable;
|
import org.fdroid.fdroid.data.Schema.ApkTable;
|
||||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
||||||
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
|
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
|
||||||
@ -181,6 +182,12 @@ public class Assert {
|
|||||||
return insertApp(context, packageName, name, new ContentValues());
|
return insertApp(context, packageName, name, new ContentValues());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static App insertApp(Context context, String packageName, String name, Repo repo) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(AppMetadataTable.Cols.REPO_ID, repo.getId());
|
||||||
|
return insertApp(context, packageName, name, values);
|
||||||
|
}
|
||||||
|
|
||||||
public static App insertApp(Context context, String packageName, String name, ContentValues additionalValues) {
|
public static App insertApp(Context context, String packageName, String name, ContentValues additionalValues) {
|
||||||
|
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
|
@ -6,7 +6,10 @@ import android.content.ContentValues;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.ContextWrapper;
|
import android.content.ContextWrapper;
|
||||||
import android.content.pm.ProviderInfo;
|
import android.content.pm.ProviderInfo;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.data.Apk;
|
||||||
|
import org.fdroid.fdroid.data.ApkProvider;
|
||||||
import org.fdroid.fdroid.data.App;
|
import org.fdroid.fdroid.data.App;
|
||||||
import org.fdroid.fdroid.data.AppProvider;
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.data.Repo;
|
import org.fdroid.fdroid.data.Repo;
|
||||||
@ -81,10 +84,14 @@ public class TestUtils {
|
|||||||
assertEquals(message, formatSigForDebugging(expected), formatSigForDebugging(actual));
|
assertEquals(message, formatSigForDebugging(expected), formatSigForDebugging(actual));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void insertApk(Context context, App app, int versionCode, String signature) {
|
public static Apk insertApk(Context context, App app, int versionCode, String signature) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(Schema.ApkTable.Cols.SIGNATURE, signature);
|
values.put(Schema.ApkTable.Cols.SIGNATURE, signature);
|
||||||
Assert.insertApk(context, app, versionCode, values);
|
|
||||||
|
long repoId = app.repoId > 0 ? app.repoId : 1;
|
||||||
|
values.put(Schema.ApkTable.Cols.REPO_ID, repoId);
|
||||||
|
Uri uri = Assert.insertApk(context, app, versionCode, values);
|
||||||
|
return ApkProvider.Helper.findByUri(context, uri, Schema.ApkTable.Cols.ALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static App insertApp(Context context, String packageName, String appName, int upstreamVersionCode,
|
public static App insertApp(Context context, String packageName, String appName, int upstreamVersionCode,
|
||||||
|
@ -6,6 +6,7 @@ import android.database.Cursor;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import org.fdroid.fdroid.Assert;
|
import org.fdroid.fdroid.Assert;
|
||||||
import org.fdroid.fdroid.BuildConfig;
|
import org.fdroid.fdroid.BuildConfig;
|
||||||
|
import org.fdroid.fdroid.TestUtils;
|
||||||
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
|
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
|
||||||
import org.fdroid.fdroid.data.Schema.RepoTable;
|
import org.fdroid.fdroid.data.Schema.RepoTable;
|
||||||
import org.fdroid.fdroid.mock.MockApk;
|
import org.fdroid.fdroid.mock.MockApk;
|
||||||
@ -271,6 +272,27 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
assertBelongsToApp(thingoApks, "com.apk.thingo");
|
assertBelongsToApp(thingoApks, "com.apk.thingo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findApksForAppInSpecificRepo() {
|
||||||
|
Repo fdroidRepo = RepoProvider.Helper.findByAddress(context, "https://f-droid.org/repo");
|
||||||
|
Repo swapRepo = RepoProviderTest.insertRepo(context, "http://192.168.1.3/fdroid/repo", "", "22", "", true);
|
||||||
|
|
||||||
|
App officialFDroid = insertApp(context, "org.fdroid.fdroid", "F-Droid (Official)", fdroidRepo);
|
||||||
|
TestUtils.insertApk(context, officialFDroid, 4, TestUtils.FDROID_SIG);
|
||||||
|
TestUtils.insertApk(context, officialFDroid, 5, TestUtils.FDROID_SIG);
|
||||||
|
|
||||||
|
App debugSwapFDroid = insertApp(context, "org.fdroid.fdroid", "F-Droid (Debug)", swapRepo);
|
||||||
|
TestUtils.insertApk(context, debugSwapFDroid, 6, TestUtils.THIRD_PARTY_SIG);
|
||||||
|
|
||||||
|
List<Apk> foundOfficialApks = ApkProvider.Helper.findAppVersionsByRepo(context, officialFDroid, fdroidRepo);
|
||||||
|
assertEquals(2, foundOfficialApks.size());
|
||||||
|
|
||||||
|
List<Apk> debugSwapApks = ApkProvider.Helper.findAppVersionsByRepo(context, officialFDroid, swapRepo);
|
||||||
|
assertEquals(1, debugSwapApks.size());
|
||||||
|
assertEquals(debugSwapFDroid.getId(), debugSwapApks.get(0).appId);
|
||||||
|
assertEquals(6, debugSwapApks.get(0).versionCode);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdate() {
|
public void testUpdate() {
|
||||||
|
|
||||||
|
@ -256,11 +256,17 @@ public class RepoProviderTest extends FDroidProviderTest {
|
|||||||
|
|
||||||
public static Repo insertRepo(Context context, String address, String description,
|
public static Repo insertRepo(Context context, String address, String description,
|
||||||
String fingerprint, @Nullable String name) {
|
String fingerprint, @Nullable String name) {
|
||||||
|
return insertRepo(context, address, description, fingerprint, name, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Repo insertRepo(Context context, String address, String description,
|
||||||
|
String fingerprint, @Nullable String name, boolean isSwap) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(RepoTable.Cols.ADDRESS, address);
|
values.put(RepoTable.Cols.ADDRESS, address);
|
||||||
values.put(RepoTable.Cols.DESCRIPTION, description);
|
values.put(RepoTable.Cols.DESCRIPTION, description);
|
||||||
values.put(RepoTable.Cols.FINGERPRINT, fingerprint);
|
values.put(RepoTable.Cols.FINGERPRINT, fingerprint);
|
||||||
values.put(RepoTable.Cols.NAME, name);
|
values.put(RepoTable.Cols.NAME, name);
|
||||||
|
values.put(RepoTable.Cols.IS_SWAP, isSwap);
|
||||||
|
|
||||||
RepoProvider.Helper.insert(context, values);
|
RepoProvider.Helper.insert(context, values);
|
||||||
return RepoProvider.Helper.findByAddress(context, address);
|
return RepoProvider.Helper.findByAddress(context, address);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user