First effort (untested) at doing scheduled repo updates
This commit is contained in:
parent
53a9e0e796
commit
9c193f237d
@ -15,9 +15,20 @@
|
|||||||
<activity android:name="Settings" />
|
<activity android:name="Settings" />
|
||||||
<activity android:name="AppDetails" />
|
<activity android:name="AppDetails" />
|
||||||
<activity android:name="Preferences" />
|
<activity android:name="Preferences" />
|
||||||
|
|
||||||
|
<receiver android:name="StartupReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<category android:name="android.intent.category.HOME" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service android:name="UpdateService" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -404,6 +404,8 @@ public class DB {
|
|||||||
// have 'updated' set to false at this point, and we will only set
|
// have 'updated' set to false at this point, and we will only set
|
||||||
// it to true when we see the app/apk in a repository. Thus, at the
|
// it to true when we see the app/apk in a repository. Thus, at the
|
||||||
// end, any that are still false can be removed.
|
// end, any that are still false can be removed.
|
||||||
|
// TODO: Need to ensure that UI and UpdateService can't both be doing
|
||||||
|
// an update at the same time.
|
||||||
updateApps = getApps(null, null, true);
|
updateApps = getApps(null, null, true);
|
||||||
Log.d("FDroid", "AppUpdate: " + updateApps.size()
|
Log.d("FDroid", "AppUpdate: " + updateApps.size()
|
||||||
+ " apps before starting.");
|
+ " apps before starting.");
|
||||||
|
@ -19,27 +19,11 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
|
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
|
||||||
import javax.xml.parsers.SAXParser;
|
|
||||||
import javax.xml.parsers.SAXParserFactory;
|
|
||||||
|
|
||||||
import org.xml.sax.InputSource;
|
|
||||||
import org.xml.sax.SAXException;
|
|
||||||
import org.xml.sax.XMLReader;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
|
|
||||||
import android.R.drawable;
|
import android.R.drawable;
|
||||||
@ -144,7 +128,6 @@ public class FDroid extends TabActivity implements OnItemClickListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String LOCAL_PATH = "/sdcard/.fdroid";
|
private String LOCAL_PATH = "/sdcard/.fdroid";
|
||||||
private String XML_PATH = LOCAL_PATH + "/remapklst.xml";
|
|
||||||
|
|
||||||
private static final int REQUEST_APPDETAILS = 0;
|
private static final int REQUEST_APPDETAILS = 0;
|
||||||
private static final int REQUEST_MANAGEREPOS = 1;
|
private static final int REQUEST_MANAGEREPOS = 1;
|
||||||
@ -168,8 +151,6 @@ public class FDroid extends TabActivity implements OnItemClickListener {
|
|||||||
|
|
||||||
private ProgressDialog pd;
|
private ProgressDialog pd;
|
||||||
|
|
||||||
private Context mctx = this;
|
|
||||||
|
|
||||||
private static final String TAB_IN = "INST";
|
private static final String TAB_IN = "INST";
|
||||||
private static final String TAB_UN = "UNIN";
|
private static final String TAB_UN = "UNIN";
|
||||||
private static final String TAB_UP = "UPDT";
|
private static final String TAB_UP = "UPDT";
|
||||||
@ -405,20 +386,7 @@ public class FDroid extends TabActivity implements OnItemClickListener {
|
|||||||
|| netstate.getNetworkInfo(0).getState() == NetworkInfo.State.CONNECTED) {
|
|| netstate.getNetworkInfo(0).getState() == NetworkInfo.State.CONNECTED) {
|
||||||
new Thread() {
|
new Thread() {
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
RepoXMLHandler.doUpdates(db);
|
||||||
db.beginUpdate();
|
|
||||||
Vector<DB.Repo> repos = db.getRepos();
|
|
||||||
for (DB.Repo repo : repos) {
|
|
||||||
if (repo.inuse) {
|
|
||||||
downloadRepoIndex(repo.address);
|
|
||||||
xmlPass(repo.address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
db.endUpdate();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.d("FDroid", "Exception while updating - "
|
|
||||||
+ e.getMessage());
|
|
||||||
}
|
|
||||||
update_handler.sendEmptyMessage(0);
|
update_handler.sendEmptyMessage(0);
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
@ -431,60 +399,6 @@ public class FDroid extends TabActivity implements OnItemClickListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Pass XML info to BD a xml file must exists...
|
|
||||||
*/
|
|
||||||
private void xmlPass(String srv) {
|
|
||||||
SAXParserFactory spf = SAXParserFactory.newInstance();
|
|
||||||
try {
|
|
||||||
SAXParser sp = spf.newSAXParser();
|
|
||||||
XMLReader xr = sp.getXMLReader();
|
|
||||||
RepoXMLHandler handler = new RepoXMLHandler(this, srv, db);
|
|
||||||
xr.setContentHandler(handler);
|
|
||||||
|
|
||||||
InputStreamReader isr = new FileReader(new File(XML_PATH));
|
|
||||||
InputSource is = new InputSource(isr);
|
|
||||||
xr.parse(is);
|
|
||||||
File xml_file = new File(XML_PATH);
|
|
||||||
xml_file.delete();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (SAXException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (ParserConfigurationException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download a repo index to a temporary file on the SD card.
|
|
||||||
private void downloadRepoIndex(String srv) {
|
|
||||||
try {
|
|
||||||
BufferedInputStream getit = new BufferedInputStream(new URL(srv
|
|
||||||
+ "/index.xml").openStream());
|
|
||||||
|
|
||||||
File file_teste = new File(XML_PATH);
|
|
||||||
if (file_teste.exists())
|
|
||||||
file_teste.delete();
|
|
||||||
|
|
||||||
FileOutputStream saveit = new FileOutputStream(XML_PATH);
|
|
||||||
BufferedOutputStream bout = new BufferedOutputStream(saveit, 1024);
|
|
||||||
byte data[] = new byte[1024];
|
|
||||||
|
|
||||||
int readed = getit.read(data, 0, 1024);
|
|
||||||
while (readed != -1) {
|
|
||||||
bout.write(data, 0, readed);
|
|
||||||
readed = getit.read(data, 0, 1024);
|
|
||||||
}
|
|
||||||
bout.close();
|
|
||||||
getit.close();
|
|
||||||
saveit.close();
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
Message msg = new Message();
|
|
||||||
msg.obj = new String(srv);
|
|
||||||
error_handler.sendMessage(msg);
|
|
||||||
} catch (Exception e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Handlers for thread functions that need to access GUI
|
* Handlers for thread functions that need to access GUI
|
||||||
@ -498,25 +412,6 @@ public class FDroid extends TabActivity implements OnItemClickListener {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private Handler error_handler = new Handler() {
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message msg) {
|
|
||||||
if (pd.isShowing())
|
|
||||||
pd.dismiss();
|
|
||||||
AlertDialog p = new AlertDialog.Builder(mctx).create();
|
|
||||||
p.setTitle(getString(R.string.connection_timeout));
|
|
||||||
p.setIcon(android.R.drawable.ic_dialog_alert);
|
|
||||||
p.setMessage(getString(R.string.connection_error_msg) + ": < "
|
|
||||||
+ msg.obj.toString() + " >");
|
|
||||||
p.setButton(getString(R.string.ok),
|
|
||||||
new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
p.show();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handler for a click on one of the items in an application list. Pops
|
// Handler for a click on one of the items in an application list. Pops
|
||||||
// up a dialog that shows the details of the application and all its
|
// up a dialog that shows the details of the application and all its
|
||||||
|
@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Ciaran Gultnieks, ciaran@ciarang.com
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -23,18 +23,24 @@ import java.io.BufferedInputStream;
|
|||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Vector;
|
||||||
|
|
||||||
|
import javax.xml.parsers.SAXParser;
|
||||||
|
import javax.xml.parsers.SAXParserFactory;
|
||||||
|
|
||||||
import org.xml.sax.Attributes;
|
import org.xml.sax.Attributes;
|
||||||
|
import org.xml.sax.InputSource;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
|
import org.xml.sax.XMLReader;
|
||||||
import org.xml.sax.helpers.DefaultHandler;
|
import org.xml.sax.helpers.DefaultHandler;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
public class RepoXMLHandler extends DefaultHandler {
|
public class RepoXMLHandler extends DefaultHandler {
|
||||||
|
|
||||||
Context mctx;
|
|
||||||
String mserver;
|
String mserver;
|
||||||
|
|
||||||
private DB db;
|
private DB db;
|
||||||
@ -43,8 +49,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
private DB.Apk curapk = null;
|
private DB.Apk curapk = null;
|
||||||
private String curel = null;
|
private String curel = null;
|
||||||
|
|
||||||
public RepoXMLHandler(Context ctx, String srv, DB db) {
|
public RepoXMLHandler(String srv, DB db) {
|
||||||
mctx = ctx;
|
|
||||||
mserver = srv;
|
mserver = srv;
|
||||||
this.db = db;
|
this.db = db;
|
||||||
}
|
}
|
||||||
@ -173,4 +178,61 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String LOCAL_PATH = "/sdcard/.fdroid";
|
||||||
|
private static String XML_PATH = LOCAL_PATH + "/repotemp.xml";
|
||||||
|
|
||||||
|
public static void doUpdates(DB db) {
|
||||||
|
db.beginUpdate();
|
||||||
|
Vector<DB.Repo> repos = db.getRepos();
|
||||||
|
for (DB.Repo repo : repos) {
|
||||||
|
if (repo.inuse) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
File f = new File(XML_PATH);
|
||||||
|
if (f.exists())
|
||||||
|
f.delete();
|
||||||
|
|
||||||
|
// Download the index file from the repo...
|
||||||
|
BufferedInputStream getit = new BufferedInputStream(
|
||||||
|
new URL(repo.address + "/index.xml").openStream());
|
||||||
|
|
||||||
|
FileOutputStream saveit = new FileOutputStream(XML_PATH);
|
||||||
|
BufferedOutputStream bout = new BufferedOutputStream(
|
||||||
|
saveit, 1024);
|
||||||
|
byte data[] = new byte[1024];
|
||||||
|
|
||||||
|
int readed = getit.read(data, 0, 1024);
|
||||||
|
while (readed != -1) {
|
||||||
|
bout.write(data, 0, readed);
|
||||||
|
readed = getit.read(data, 0, 1024);
|
||||||
|
}
|
||||||
|
bout.close();
|
||||||
|
getit.close();
|
||||||
|
saveit.close();
|
||||||
|
|
||||||
|
// Process the index...
|
||||||
|
SAXParserFactory spf = SAXParserFactory.newInstance();
|
||||||
|
SAXParser sp = spf.newSAXParser();
|
||||||
|
XMLReader xr = sp.getXMLReader();
|
||||||
|
RepoXMLHandler handler = new RepoXMLHandler(repo.address, db);
|
||||||
|
xr.setContentHandler(handler);
|
||||||
|
|
||||||
|
InputStreamReader isr = new FileReader(new File(XML_PATH));
|
||||||
|
InputSource is = new InputSource(isr);
|
||||||
|
xr.parse(is);
|
||||||
|
File xml_file = new File(XML_PATH);
|
||||||
|
xml_file.delete();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.d("FDroid", "Exception updating from " + repo.address
|
||||||
|
+ " - " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.endUpdate();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
32
src/org/fdroid/fdroid/StartupReceiver.java
Normal file
32
src/org/fdroid/fdroid/StartupReceiver.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Ciaran Gultnieks, ciaran@ciarang.com
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
public class StartupReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context ctx, Intent intent) {
|
||||||
|
UpdateService.schedule(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
120
src/org/fdroid/fdroid/UpdateService.java
Normal file
120
src/org/fdroid/fdroid/UpdateService.java
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Ciaran Gultnieks, ciaran@ciarang.com
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public class UpdateService extends Service {
|
||||||
|
|
||||||
|
// Schedule (or cancel schedule for) this service, according to the
|
||||||
|
// current preferences. Should be called a) at boot, or b) if the preference
|
||||||
|
// is changed.
|
||||||
|
// TODO: What if we get upgraded?
|
||||||
|
public static void schedule(Context ctx) {
|
||||||
|
|
||||||
|
SharedPreferences prefs = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(ctx);
|
||||||
|
String sint = prefs.getString("updateInterval", "0");
|
||||||
|
int interval = Integer.parseInt(sint);
|
||||||
|
|
||||||
|
Intent intent = new Intent(ctx, UpdateService.class);
|
||||||
|
PendingIntent pending = PendingIntent.getService(ctx, 0, intent, 0);
|
||||||
|
|
||||||
|
AlarmManager alarm = (AlarmManager) ctx
|
||||||
|
.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
alarm.cancel(pending);
|
||||||
|
if (interval > 0) {
|
||||||
|
alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
|
||||||
|
SystemClock.elapsedRealtime() + 5000,
|
||||||
|
AlarmManager.INTERVAL_HOUR, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// For API levels <5
|
||||||
|
@Override
|
||||||
|
public void onStart(Intent intent, int startId) {
|
||||||
|
handleCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For API levels >=5
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
handleCommand();
|
||||||
|
return START_REDELIVER_INTENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCommand() {
|
||||||
|
|
||||||
|
new Thread() {
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
// See if it's time to actually do anything yet...
|
||||||
|
SharedPreferences prefs = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(getBaseContext());
|
||||||
|
long lastUpdate = prefs.getLong("lastUpdateCheck", 0);
|
||||||
|
String sint = prefs.getString("updateInterval", "0");
|
||||||
|
int interval = Integer.parseInt(sint);
|
||||||
|
if (interval == 0)
|
||||||
|
return;
|
||||||
|
if (lastUpdate + (interval * 60 * 60) > System
|
||||||
|
.currentTimeMillis())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Make sure we have a connection...
|
||||||
|
ConnectivityManager netstate = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
if (netstate.getNetworkInfo(1).getState() != NetworkInfo.State.CONNECTED
|
||||||
|
&& netstate.getNetworkInfo(0).getState() != NetworkInfo.State.CONNECTED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Do the update...
|
||||||
|
DB db = null;
|
||||||
|
try {
|
||||||
|
db = new DB(getBaseContext());
|
||||||
|
RepoXMLHandler.doUpdates(db);
|
||||||
|
} catch(Exception e) {
|
||||||
|
Log.d("FDroid","Exception during handleCommand() - " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
if (db != null)
|
||||||
|
db.close();
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user