ref: ApkManifest
This commit is contained in:
@@ -1038,7 +1038,7 @@
|
|||||||
repositoryURL = "https://github.com/relikd/AndroidXML";
|
repositoryURL = "https://github.com/relikd/AndroidXML";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = exactVersion;
|
||||||
version = 1.3.0;
|
version = 1.4.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/relikd/AndroidXML",
|
"location" : "https://github.com/relikd/AndroidXML",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "fd522d612f24ee813c80b1a1b0f6bd311b2735c3",
|
"revision" : "d9fe646bcc3b05548aebbd20b4eee0af675c129f",
|
||||||
"version" : "1.3.0"
|
"version" : "1.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,16 +16,9 @@ extension QLThumbnailReply {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ThumbnailProvider: QLThumbnailProvider {
|
class ThumbnailProvider: QLThumbnailProvider {
|
||||||
|
|
||||||
// TODO: sadly, this does not seem to work for .xcarchive and .appex
|
|
||||||
// Probably overwritten by Apple somehow
|
|
||||||
|
|
||||||
override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) {
|
override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) {
|
||||||
let meta = MetaInfo(request.fileURL)
|
let meta = MetaInfo(request.fileURL)
|
||||||
guard let appPlist = meta.readPlistApp(iconOnly: true) else {
|
let img = AppIcon(meta).extractImageForThumbnail().withRoundCorners()
|
||||||
return
|
|
||||||
}
|
|
||||||
let img = AppIcon(meta).extractImage(from: appPlist).withRoundCorners()
|
|
||||||
|
|
||||||
// First way: Draw the thumbnail into the current context, set up with UIKit's coordinate system.
|
// First way: Draw the thumbnail into the current context, set up with UIKit's coordinate system.
|
||||||
let reply = QLThumbnailReply(contextSize: request.maximumSize, currentContextDrawing: { () -> Bool in
|
let reply = QLThumbnailReply(contextSize: request.maximumSize, currentContextDrawing: { () -> Bool in
|
||||||
|
|||||||
@@ -13,17 +13,27 @@ struct AppIcon {
|
|||||||
self.meta = meta
|
self.meta = meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convenience getter to extract app icon regardless of bundle-type.
|
||||||
|
func extractImageForThumbnail() -> NSImage {
|
||||||
|
switch meta.type {
|
||||||
|
case .IPA, .Archive, .Extension:
|
||||||
|
extractImage(from: meta.readPlistApp())
|
||||||
|
case .APK:
|
||||||
|
extractImage(from: meta.readApkIconOnly())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract image from Android app bundle.
|
||||||
|
func extractImage(from manifest: ApkManifest?) -> NSImage {
|
||||||
|
if let data = manifest?.appIconData, let img = NSImage(data: data) {
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
return defaultIcon()
|
||||||
|
}
|
||||||
|
|
||||||
/// Try multiple methods to extract image.
|
/// Try multiple methods to extract image.
|
||||||
/// This method will always return an image even if none is found, in which case it returns the default image.
|
/// This method will always return an image even if none is found, in which case it returns the default image.
|
||||||
func extractImage(from appPlist: PlistDict?) -> NSImage {
|
func extractImage(from appPlist: PlistDict?) -> NSImage {
|
||||||
if meta.type == .APK {
|
|
||||||
if let iconPath = appPlist?["appIcon"] as? String,
|
|
||||||
let data = meta.zipFile!.unzipFile(iconPath),
|
|
||||||
let img = NSImage(data: data) {
|
|
||||||
return img
|
|
||||||
}
|
|
||||||
return defaultIcon()
|
|
||||||
}
|
|
||||||
// no need to unwrap the plist, and most .ipa should include the Artwork anyway
|
// no need to unwrap the plist, and most .ipa should include the Artwork anyway
|
||||||
if meta.type == .IPA {
|
if meta.type == .IPA {
|
||||||
if let data = meta.zipFile!.unzipFile("iTunesArtwork") {
|
if let data = meta.zipFile!.unzipFile("iTunesArtwork") {
|
||||||
|
|||||||
@@ -4,12 +4,30 @@ import os // OSLog
|
|||||||
|
|
||||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "MetaInfo+Apk")
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "MetaInfo+Apk")
|
||||||
|
|
||||||
|
/// Representation of `AndroidManifest.xml`
|
||||||
|
struct ApkManifest {
|
||||||
|
var packageId: String? = nil
|
||||||
|
var appName: String? = nil
|
||||||
|
var appIcon: String? = nil
|
||||||
|
/// Computed property
|
||||||
|
var appIconData: Data? = nil
|
||||||
|
var versionName: String? = nil
|
||||||
|
var versionCode: String? = nil
|
||||||
|
var sdkVerMin: String? = nil
|
||||||
|
var sdkVerTarget: String? = nil
|
||||||
|
|
||||||
|
var featuresRequired: [String] = []
|
||||||
|
var featuresOptional: [String] = []
|
||||||
|
var permissions: [String] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Full Manifest
|
// MARK: - Full Manifest
|
||||||
|
|
||||||
extension MetaInfo {
|
extension MetaInfo {
|
||||||
/// Extract `AndroidManifest.xml` and parse its content
|
/// Extract `AndroidManifest.xml` and parse its content
|
||||||
func readApkManifest() -> PlistDict? {
|
func readApkManifest() -> ApkManifest? {
|
||||||
|
assert(type == .APK)
|
||||||
guard let data = self.readPayloadFile("AndroidManifest.xml", osxSubdir: nil) else {
|
guard let data = self.readPayloadFile("AndroidManifest.xml", osxSubdir: nil) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -42,14 +60,9 @@ extension MetaInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var rv = storage.result
|
var rv = storage.result
|
||||||
guard let _ = rv["packageId"] else {
|
os_log(.debug, log: log, "[apk] resolving %{public}@", String(describing: rv))
|
||||||
return nil
|
rv.resolve(zipFile!)
|
||||||
}
|
os_log(.debug, log: log, "[apk] resolved name: \"%{public}@\" icon: %{public}@", rv.appName ?? "", rv.appIcon ?? "-")
|
||||||
os_log(.debug, log: log, "[apk] resolving resources name: %{public}@ icon: %{public}@", String(describing: rv["appName"]), String(describing: rv["appIcon"]))
|
|
||||||
let resolved = self.resolveResources([(ResourceType.Name, rv["appName"] as? String), (ResourceType.Icon, rv["appIcon"] as? String)])
|
|
||||||
os_log(.debug, log: log, "[apk] resolved %{public}@", String(describing: resolved))
|
|
||||||
rv["appName"] = resolved[0]
|
|
||||||
rv["appIcon"] = resolved[1]
|
|
||||||
return rv
|
return rv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,48 +70,41 @@ extension MetaInfo {
|
|||||||
/// Wrapper to use same code for binary-xml and string-xml parsing
|
/// Wrapper to use same code for binary-xml and string-xml parsing
|
||||||
private class ApkXmlManifestParser: NSObject, XMLParserDelegate {
|
private class ApkXmlManifestParser: NSObject, XMLParserDelegate {
|
||||||
private var _scope: [String] = []
|
private var _scope: [String] = []
|
||||||
private var _rv: [String: String] = [:]
|
var result = ApkManifest()
|
||||||
private var _perm: [String] = []
|
|
||||||
private var _featOpt: [String] = []
|
|
||||||
private var _featReq: [String] = []
|
|
||||||
var result: PlistDict {
|
|
||||||
(_rv as PlistDict).merging([
|
|
||||||
"permissions": _perm,
|
|
||||||
"featuresOptional": _featOpt,
|
|
||||||
"featuresRequired": _featReq,
|
|
||||||
]) { $1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attrs: [String : String] = [:]) {
|
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attrs: [String : String] = [:]) {
|
||||||
// keep in sync with ALLOWED_TAGS above
|
// keep in sync with ALLOWED_TAGS above
|
||||||
switch elementName {
|
switch elementName {
|
||||||
case "manifest":
|
case "manifest":
|
||||||
if _scope == [] {
|
if _scope == [] {
|
||||||
_rv["packageId"] = attrs["package"] // "org.bundle.id"
|
result.packageId = attrs["package"] // "org.bundle.id"
|
||||||
_rv["versionName"] = attrs["android:versionName"] // "7.62.3"
|
result.versionName = attrs["android:versionName"] // "7.62.3"
|
||||||
_rv["versionCode"] = attrs["android:versionCode"] // "160700"
|
result.versionCode = attrs["android:versionCode"] // "160700"
|
||||||
// attrs["platformBuildVersionCode"] // "35"
|
// attrs["platformBuildVersionCode"] // "35"
|
||||||
// attrs["platformBuildVersionName"] // "15"
|
// attrs["platformBuildVersionName"] // "15"
|
||||||
}
|
}
|
||||||
case "application":
|
case "application":
|
||||||
if _scope == ["manifest"] {
|
if _scope == ["manifest"] {
|
||||||
_rv["appName"] = attrs["android:label"] // @resource-ref
|
result.appName = attrs["android:label"] // @resource-ref
|
||||||
_rv["appIcon"] = attrs["android:icon"] // @resource-ref
|
result.appIcon = attrs["android:icon"] // @resource-ref
|
||||||
}
|
}
|
||||||
case "uses-permission", "uses-permission-sdk-23":
|
case "uses-permission", "uses-permission-sdk-23":
|
||||||
// no "permission" because that will produce duplicates with "uses-permission"
|
// no "permission" because that will produce duplicates with "uses-permission"
|
||||||
if _scope == ["manifest"], let name = attrs["android:name"] {
|
if _scope == ["manifest"], let name = attrs["android:name"] {
|
||||||
_perm.append(name)
|
result.permissions.append(name)
|
||||||
}
|
}
|
||||||
case "uses-feature":
|
case "uses-feature":
|
||||||
if _scope == ["manifest"], let name = attrs["android:name"] {
|
if _scope == ["manifest"], let name = attrs["android:name"] {
|
||||||
let optional = attrs["android:required"] == "false"
|
if attrs["android:required"] == "false" {
|
||||||
optional ? _featOpt.append(name) : _featReq.append(name)
|
result.featuresOptional.append(name)
|
||||||
|
} else {
|
||||||
|
result.featuresRequired.append(name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "uses-sdk":
|
case "uses-sdk":
|
||||||
if _scope == ["manifest"] {
|
if _scope == ["manifest"] {
|
||||||
_rv["sdkVerMin"] = attrs["android:minSdkVersion"] ?? "1" // "21"
|
result.sdkVerMin = attrs["android:minSdkVersion"] ?? "1" // "21"
|
||||||
_rv["sdkVerTarget"] = attrs["android:targetSdkVersion"] // "35"
|
result.sdkVerTarget = attrs["android:targetSdkVersion"] // "35"
|
||||||
}
|
}
|
||||||
default: break // ignore
|
default: break // ignore
|
||||||
}
|
}
|
||||||
@@ -115,26 +121,25 @@ private class ApkXmlManifestParser: NSObject, XMLParserDelegate {
|
|||||||
|
|
||||||
extension MetaInfo {
|
extension MetaInfo {
|
||||||
/// Same as `readApkManifest()` but only extract `appIcon`.
|
/// Same as `readApkManifest()` but only extract `appIcon`.
|
||||||
func readApkIconOnly() -> PlistDict? {
|
func readApkIconOnly() -> ApkManifest? {
|
||||||
|
assert(type == .APK)
|
||||||
|
var rv = ApkManifest()
|
||||||
guard let data = self.readPayloadFile("AndroidManifest.xml", osxSubdir: nil) else {
|
guard let data = self.readPayloadFile("AndroidManifest.xml", osxSubdir: nil) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var icon: String? = nil
|
|
||||||
if let xml = try? AndroidXML.init(data: data) {
|
if let xml = try? AndroidXML.init(data: data) {
|
||||||
let parser = xml.parseXml()
|
let parser = xml.parseXml()
|
||||||
try? parser.iterElements({ startTag, attributes in
|
try? parser.iterElements({ startTag, attributes in
|
||||||
if startTag == "application" {
|
if startTag == "application" {
|
||||||
icon = try? attributes.asDictStr()["android:icon"]
|
rv.appIcon = try? attributes.get("android:icon")?.resolve(parser.stringPool)
|
||||||
}
|
}
|
||||||
}) {_ in}
|
}) {_ in}
|
||||||
} else {
|
} else {
|
||||||
// fallback to xml-string parser
|
// fallback to xml-string parser
|
||||||
icon = ApkXmlIconParser().run(data)
|
rv.appIcon = ApkXmlIconParser().run(data)
|
||||||
}
|
}
|
||||||
if let icon = self.resolveResources([(.Icon, icon)])[0] {
|
rv.resolve(zipFile!)
|
||||||
return ["appIcon": icon]
|
return rv
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,25 +165,21 @@ private class ApkXmlIconParser: NSObject, XMLParserDelegate {
|
|||||||
|
|
||||||
// MARK: - Resolve resource
|
// MARK: - Resolve resource
|
||||||
|
|
||||||
private extension MetaInfo {
|
private extension ApkManifest {
|
||||||
// currently there are only two types of resources, "android:icon" and "android:label"
|
mutating func resolve(_ zip: ZipFile) {
|
||||||
enum ResourceType {
|
guard let data = zip.unzipFile("resources.arsc"),
|
||||||
case Name
|
|
||||||
case Icon
|
|
||||||
}
|
|
||||||
func resolveResources(_ ids: [(ResourceType, String?)]) -> [String?] {
|
|
||||||
guard let data = self.readPayloadFile("resources.arsc", osxSubdir: nil),
|
|
||||||
let xml = try? AndroidXML.init(data: data), xml.type == .Table else {
|
let xml = try? AndroidXML.init(data: data), xml.type == .Table else {
|
||||||
return ids.map { _ in nil }
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let parser = xml.parseTable()
|
let parser = xml.parseTable()
|
||||||
return ids.map { typ, val in
|
if let val = appName, let ref = try? TblTableRef(val) {
|
||||||
guard let val, let ref = try? TblTableRef(val) else {
|
appName = parser.getName(ref)
|
||||||
return nil
|
}
|
||||||
}
|
if let val = appIcon, let ref = try? TblTableRef(val) {
|
||||||
switch typ {
|
if let iconPath = parser.getIconDirect(ref) ?? parser.getIconIndirect(ref) {
|
||||||
case .Name: return parser.getName(ref)
|
appIcon = iconPath
|
||||||
case .Icon: return parser.getIconDirect(ref) ?? parser.getIconIndirect(ref)
|
appIconData = zip.unzipFile(iconPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,12 +88,12 @@ struct MetaInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read app default `Info.plist`. (used for both, Preview and Thumbnail)
|
/// Read app default `Info.plist`. (used for both, Preview and Thumbnail)
|
||||||
func readPlistApp(iconOnly: Bool = false) -> PlistDict? {
|
func readPlistApp() -> PlistDict? {
|
||||||
switch self.type {
|
switch self.type {
|
||||||
case .IPA, .Archive, .Extension:
|
case .IPA, .Archive, .Extension:
|
||||||
return self.readPayloadFile("Info.plist", osxSubdir: nil)?.asPlistOrNil()
|
return self.readPayloadFile("Info.plist", osxSubdir: nil)?.asPlistOrNil()
|
||||||
case .APK:
|
case .APK:
|
||||||
return iconOnly ? self.readApkIconOnly() : self.readApkManifest()
|
return nil // not applicable for Android
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ extension PreviewGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Process info stored in `Info.plist`
|
/// Process info stored in `Info.plist`
|
||||||
mutating func procAppInfoAndroid(_ appPlist: PlistDict) {
|
mutating func procAppInfoAndroid(_ manifest: ApkManifest) {
|
||||||
let featuresRequired = appPlist["featuresRequired"] as! [String]
|
let featReq = manifest.featuresRequired
|
||||||
let featuresOptional = appPlist["featuresOptional"] as! [String]
|
let featOpt = manifest.featuresOptional
|
||||||
let permissions = appPlist["permissions"] as! [String]
|
let perms = manifest.permissions
|
||||||
|
|
||||||
func asList(_ list: [String]) -> String {
|
func asList(_ list: [String]) -> String {
|
||||||
"<pre>\(list.joined(separator: "\n"))</pre>"
|
"<pre>\(list.joined(separator: "\n"))</pre>"
|
||||||
@@ -58,21 +58,21 @@ extension PreviewGenerator {
|
|||||||
}
|
}
|
||||||
self.apply([
|
self.apply([
|
||||||
"AppInfoHidden": CLASS_VISIBLE,
|
"AppInfoHidden": CLASS_VISIBLE,
|
||||||
"AppName": appPlist["appName"] as? String ?? "",
|
"AppName": manifest.appName ?? "",
|
||||||
"AppVersion": appPlist["versionName"] as? String ?? "",
|
"AppVersion": manifest.versionName ?? "",
|
||||||
"AppBuildVer": appPlist["versionCode"] as? String ?? "",
|
"AppBuildVer": manifest.versionCode ?? "",
|
||||||
"AppId": appPlist["packageId"] as? String ?? "",
|
"AppId": manifest.packageId ?? "",
|
||||||
|
|
||||||
"ApkFeaturesRequiredHidden": featuresRequired.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
"ApkFeaturesRequiredHidden": featReq.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||||
"ApkFeaturesRequiredList": asList(featuresRequired),
|
"ApkFeaturesRequiredList": asList(featReq),
|
||||||
"ApkFeaturesOptionalHidden": featuresOptional.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
"ApkFeaturesOptionalHidden": featOpt.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||||
"ApkFeaturesOptionalList": asList(featuresOptional),
|
"ApkFeaturesOptionalList": asList(featOpt),
|
||||||
"ApkPermissionsHidden": permissions.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
"ApkPermissionsHidden": perms.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||||
"ApkPermissionsList": asList(permissions),
|
"ApkPermissionsList": asList(perms),
|
||||||
|
|
||||||
"AppDeviceFamily": "Android",
|
"AppDeviceFamily": "Android",
|
||||||
"AppSDK": resolveSDK(appPlist["sdkVerTarget"] as? String),
|
"AppSDK": resolveSDK(manifest.sdkVerTarget),
|
||||||
"AppMinOS": resolveSDK(appPlist["sdkVerMin"] as? String),
|
"AppMinOS": resolveSDK(manifest.sdkVerMin),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,15 +23,12 @@ struct PreviewGenerator {
|
|||||||
|
|
||||||
init(_ meta: MetaInfo) throws {
|
init(_ meta: MetaInfo) throws {
|
||||||
self.meta = meta
|
self.meta = meta
|
||||||
guard let plistApp = meta.readPlistApp() else {
|
|
||||||
let isAndroid = meta.type == .APK
|
|
||||||
throw RuntimeError(isAndroid ? "AndroidManifest.xml not found" : "Info.plist not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
data["QuickLookTitle"] = stringForFileType(meta)
|
|
||||||
|
|
||||||
switch meta.type {
|
switch meta.type {
|
||||||
case .IPA, .Archive, .Extension:
|
case .IPA, .Archive, .Extension:
|
||||||
|
guard let plistApp = meta.readPlistApp() else {
|
||||||
|
throw RuntimeError("Info.plist not found")
|
||||||
|
}
|
||||||
procAppInfoApple(plistApp, isOSX: meta.isOSX)
|
procAppInfoApple(plistApp, isOSX: meta.isOSX)
|
||||||
if meta.type == .IPA {
|
if meta.type == .IPA {
|
||||||
procItunesMeta(meta.readPlistItunes())
|
procItunesMeta(meta.readPlistItunes())
|
||||||
@@ -43,14 +40,21 @@ struct PreviewGenerator {
|
|||||||
let plistProvision = meta.readPlistProvision()
|
let plistProvision = meta.readPlistProvision()
|
||||||
procEntitlements(meta, plistApp, plistProvision)
|
procEntitlements(meta, plistApp, plistProvision)
|
||||||
procProvision(plistProvision, isOSX: meta.isOSX)
|
procProvision(plistProvision, isOSX: meta.isOSX)
|
||||||
|
// App Icon (last, because the image uses a lot of memory)
|
||||||
|
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp).withRoundCorners().asBase64()
|
||||||
|
|
||||||
case .APK:
|
case .APK:
|
||||||
procAppInfoAndroid(plistApp)
|
guard let manifest = meta.readApkManifest() else {
|
||||||
|
throw RuntimeError("AndroidManifest.xml not found")
|
||||||
|
}
|
||||||
|
procAppInfoAndroid(manifest)
|
||||||
|
// App Icon (last, because the image uses a lot of memory)
|
||||||
|
data["AppIcon"] = AppIcon(meta).extractImage(from: manifest).withRoundCorners().asBase64()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data["QuickLookTitle"] = stringForFileType(meta)
|
||||||
procFileInfo(meta.url)
|
procFileInfo(meta.url)
|
||||||
procFooterInfo()
|
procFooterInfo()
|
||||||
// App Icon (last, because the image uses a lot of memory)
|
|
||||||
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp).withRoundCorners().asBase64()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func apply(_ values: [String: String]) {
|
mutating func apply(_ values: [String: String]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user