Rewrite app installation

Fix  #4960
This commit is contained in:
南宫雪珊 2021-12-14 21:20:29 +08:00 committed by GitHub
parent 5a49bd3ac9
commit baa19f0ccf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 160 additions and 395 deletions

View File

@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"

View File

@ -1,303 +0,0 @@
package com.topjohnwu.magisk;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import io.michaelrocks.paranoid.Obfuscate;
/**
* Modified from androidx.core.content.FileProvider
*/
@Obfuscate
public class FileProvider extends ContentProvider {
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
private static final File DEVICE_ROOT = new File("/");
private static final HashMap<String, PathStrategy> sCache = new HashMap<>();
private PathStrategy mStrategy;
@Override
public boolean onCreate() {
return true;
}
@Override
public void attachInfo(Context context, ProviderInfo info) {
super.attachInfo(context, info);
if (info.exported) {
throw new SecurityException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
mStrategy = getPathStrategy(context, info.authority.split(";")[0]);
}
public static Uri getUriForFile(Context context, String authority, File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final File file = mStrategy.getFileForUri(uri);
if (projection == null) {
projection = COLUMNS;
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = file.getName();
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = file.length();
}
}
cols = copyOf(cols, i);
values = copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
@Override
public String getType(Uri uri) {
final File file = mStrategy.getFileForUri(uri);
final int lastDot = file.getName().lastIndexOf('.');
if (lastDot >= 0) {
final String extension = file.getName().substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) {
return mime;
}
}
return "application/octet-stream";
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("No external inserts");
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("No external updates");
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final File file = mStrategy.getFileForUri(uri);
return file.delete() ? 1 : 0;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
final File file = mStrategy.getFileForUri(uri);
final int fileMode = modeToMode(mode);
return ParcelFileDescriptor.open(file, fileMode);
}
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
strat = sCache.get(authority);
if (strat == null) {
strat = createPathStrategy(context, authority);
sCache.put(authority, strat);
}
}
return strat;
}
private static PathStrategy createPathStrategy(Context context, String authority) {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
strat.addRoot("root_files", buildPath(DEVICE_ROOT, "."));
strat.addRoot("internal_files", buildPath(context.getFilesDir(), "."));
strat.addRoot("cache_files", buildPath(context.getCacheDir(), "."));
strat.addRoot("external_files", buildPath(Environment.getExternalStorageDirectory(), "."));
File[] externalFilesDirs = context.getExternalFilesDirs(null);
if (externalFilesDirs.length > 0) {
strat.addRoot("external_file_files", buildPath(externalFilesDirs[0], "."));
}
File[] externalCacheDirs = context.getExternalCacheDirs();
if (externalCacheDirs.length > 0) {
strat.addRoot("external_cache_files", buildPath(externalCacheDirs[0], "."));
}
File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
strat.addRoot("external_media_files", buildPath(externalMediaDirs[0], "."));
}
return strat;
}
interface PathStrategy {
Uri getUriForFile(File file);
File getFileForUri(Uri uri);
}
static class SimplePathStrategy implements PathStrategy {
private final String mAuthority;
private final HashMap<String, File> mRoots = new HashMap<>();
SimplePathStrategy(String authority) {
mAuthority = authority;
}
void addRoot(String name, File root) {
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Name must not be empty");
}
try {
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}
mRoots.put(name, root);
}
@Override
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
@Override
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
final int splitIndex = path.indexOf('/', 1);
final String tag = Uri.decode(path.substring(1, splitIndex));
path = Uri.decode(path.substring(splitIndex + 1));
final File root = mRoots.get(tag);
if (root == null) {
throw new IllegalArgumentException("Unable to find configured root for " + uri);
}
File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException("Resolved path jumped beyond configured root");
}
return file;
}
}
private static int modeToMode(String mode) {
int modeBits;
if ("r".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
} else if ("w".equals(mode) || "wt".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
} else if ("wa".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_APPEND;
} else if ("rw".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE;
} else if ("rwt".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
} else {
throw new IllegalArgumentException("Invalid mode: " + mode);
}
return modeBits;
}
private static File buildPath(File base, String... segments) {
File cur = base;
for (String segment : segments) {
if (segment != null) {
cur = new File(cur, segment);
}
}
return cur;
}
private static String[] copyOf(String[] original, int newLength) {
final String[] result = new String[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
private static Object[] copyOf(Object[] original, int newLength) {
final Object[] result = new Object[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
}

View File

@ -1,51 +1,121 @@
package com.topjohnwu.magisk.utils;
import android.app.Activity;
import static android.content.pm.PackageInstaller.EXTRA_STATUS;
import static android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID;
import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION;
import static android.content.pm.PackageInstaller.STATUS_SUCCESS;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInstaller.Session;
import android.content.pm.PackageInstaller.SessionParams;
import android.net.Uri;
import android.os.Build;
import com.topjohnwu.magisk.FileProvider;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.michaelrocks.paranoid.Obfuscate;
@Obfuscate
public class APKInstall {
public final class APKInstall {
// @WorkerThread
public static void installapk(Context context, File apk) {
//noinspection InlinedApi
var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
var action = APKInstall.class.getName();
var intent = new Intent(action).setPackage(context.getPackageName());
var pending = PendingIntent.getBroadcast(context, 0, intent, flag);
public static Intent installIntent(Context c, File apk) {
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 24) {
intent.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk));
} else {
//noinspection ResultOfMethodCallIgnored SetWorldReadable
apk.setReadable(true, false);
intent.setData(Uri.fromFile(apk));
var installer = context.getPackageManager().getPackageInstaller();
var params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
}
try (Session session = installer.openSession(installer.createSession(params))) {
OutputStream out = session.openWrite(apk.getName(), 0, apk.length());
try (var in = new FileInputStream(apk); out) {
transfer(in, out);
}
session.commit(pending.getIntentSender());
} catch (IOException e) {
Log.e(APKInstall.class.getSimpleName(), "", e);
}
return intent;
}
public static void install(Context c, File apk) {
c.startActivity(installIntent(c, apk));
public static void transfer(InputStream in, OutputStream out) throws IOException {
int size = 8192;
var buffer = new byte[size];
int read;
while ((read = in.read(buffer, 0, size)) >= 0) {
out.write(buffer, 0, read);
}
}
public static void registerInstallReceiver(Context c, BroadcastReceiver r) {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
public static InstallReceiver register(Context context, String packageName, Runnable onSuccess) {
var receiver = new InstallReceiver(context, packageName, onSuccess);
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package");
c.getApplicationContext().registerReceiver(r, filter);
context.registerReceiver(receiver, filter);
context.registerReceiver(receiver, new IntentFilter(APKInstall.class.getName()));
return receiver;
}
public static void installHideResult(Activity c, File apk) {
Intent intent = installIntent(c, apk);
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
c.startActivityForResult(intent, 0); // Ignore result, use install receiver
public static class InstallReceiver extends BroadcastReceiver {
private final Context context;
private final String packageName;
private final Runnable onSuccess;
private final CountDownLatch latch = new CountDownLatch(1);
private Intent intent = null;
private InstallReceiver(Context context, String packageName, Runnable onSuccess) {
this.context = context;
this.packageName = packageName;
this.onSuccess = onSuccess;
}
@Override
public void onReceive(Context c, Intent i) {
if (Intent.ACTION_PACKAGE_ADDED.equals(i.getAction())) {
Uri data = i.getData();
if (data == null || onSuccess == null) return;
String pkg = data.getSchemeSpecificPart();
if (pkg.equals(packageName)) {
onSuccess.run();
context.unregisterReceiver(this);
}
return;
}
int status = i.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
switch (status) {
case STATUS_PENDING_USER_ACTION:
intent = i.getParcelableExtra(Intent.EXTRA_INTENT);
break;
case STATUS_SUCCESS:
if (onSuccess != null) onSuccess.run();
default:
context.unregisterReceiver(this);
}
latch.countDown();
}
// @WorkerThread @Nullable
public Intent waitIntent() {
try {
//noinspection ResultOfMethodCallIgnored
latch.await(5, TimeUnit.SECONDS);
} catch (Exception ignored) {
}
return intent;
}
}
}

View File

@ -1,18 +1,20 @@
package com.topjohnwu.magisk.core
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.content.pm.ProviderInfo
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import java.io.File
open class Provider : FileProvider() {
class Provider : ContentProvider() {
override fun attachInfo(context: Context, info: ProviderInfo?) {
override fun attachInfo(context: Context, info: ProviderInfo) {
super.attachInfo(context.wrap(), info)
}
@ -36,4 +38,11 @@ open class Provider : FileProvider() {
fun PREFS_URI(pkg: String) =
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
}
override fun onCreate() = true
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
}

View File

@ -73,8 +73,12 @@ sealed class Subject : Parcelable {
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
override fun pendingIntent(context: Context) =
APKInstall.installIntent(context, file.toFile()).toPending(context)
override fun pendingIntent(context: Context): PendingIntent {
val receiver = APKInstall.register(context, null, null)
APKInstall.installapk(context, file.toFile())
val intent = receiver.waitIntent() ?: Intent()
return intent.toPending(context)
}
}
@SuppressLint("InlinedApi")

View File

@ -1,7 +1,6 @@
package com.topjohnwu.magisk.core.tasks
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
@ -27,7 +26,6 @@ import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.lang.ref.WeakReference
import java.security.SecureRandom
object HideAPK {
@ -41,8 +39,6 @@ object HideAPK {
const val MAX_LABEL_LENGTH = 32
private val svc get() = ServiceLocator.networkService
private val Context.APK_URI get() = Provider.APK_URI(packageName)
private val Context.PREFS_URI get() = Provider.PREFS_URI(packageName)
private fun genPackageName(): String {
val random = SecureRandom()
@ -92,35 +88,16 @@ object HideAPK {
return true
}
private class WaitPackageReceiver(
private val pkg: String,
activity: Activity
) : BroadcastReceiver() {
private val activity = WeakReference(activity)
private fun launchApp(): Unit = activity.get()?.run {
val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.putExtra(Const.Key.PREV_PKG, packageName)
startActivity(intent)
finish()
} ?: Unit
override fun onReceive(context: Context, intent: Intent) {
when (intent.action ?: return) {
Intent.ACTION_PACKAGE_REPLACED, Intent.ACTION_PACKAGE_ADDED -> {
val newPkg = intent.data?.encodedSchemeSpecificPart.orEmpty()
if (newPkg == pkg) {
context.unregisterReceiver(this)
launchApp()
}
}
}
}
private fun launchApp(activity: Activity, pkg: String) {
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
val self = activity.packageName
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
activity.grantUriPermission(pkg, Provider.APK_URI(self), flag)
activity.grantUriPermission(pkg, Provider.PREFS_URI(self), flag)
intent.putExtra(Const.Key.PREV_PKG, self)
activity.startActivity(intent)
activity.finish()
}
private suspend fun patchAndHide(activity: Activity, label: String): Boolean {
@ -141,9 +118,14 @@ object HideAPK {
return false
// Install and auto launch app
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(pkg, activity))
if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
APKInstall.installHideResult(activity, repack)
val receiver = APKInstall.register(activity, pkg) {
launchApp(activity, pkg)
}
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
if (!Shell.su(cmd).exec().isSuccess) {
APKInstall.installapk(activity, repack)
receiver.waitIntent()?.let { activity.startActivity(it) }
}
return true
}
@ -157,8 +139,8 @@ object HideAPK {
val result = withContext(Dispatchers.IO) {
patchAndHide(activity, label)
}
dialog.dismiss()
if (!result) {
dialog.dismiss()
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
}
}
@ -171,11 +153,15 @@ object HideAPK {
show()
}
val apk = DynAPK.current(activity)
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(APPLICATION_ID, activity))
Shell.su("adb_pm_install $apk").submit {
val receiver = APKInstall.register(activity, APPLICATION_ID) {
launchApp(activity, APPLICATION_ID)
dialog.dismiss()
if (!it.isSuccess)
APKInstall.installHideResult(activity, apk)
}
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
Shell.su(cmd).submit(Shell.EXECUTOR) { ret ->
if (ret.isSuccess) return@submit
APKInstall.installapk(activity, apk)
receiver.waitIntent()?.let { activity.startActivity(it) }
}
}
}

View File

@ -129,9 +129,11 @@ adb_pm_install() {
local tmp=/data/local/tmp/temp.apk
cp -f "$1" $tmp
chmod 644 $tmp
su 2000 -c pm install $tmp || pm install $tmp
su 2000 -c pm install $tmp || pm install $tmp || su 1000 -c pm install $tmp
local res=$?
rm -f $tmp
# Note: change this will kill self
[ $res != 0 ] && appops set "$2" REQUEST_INSTALL_PACKAGES allow
return $res
}

View File

@ -105,7 +105,7 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add(Component(
"com.topjohnwu.magisk.core.Provider",
"FileProvider",
"dummy.DummyProvider",
"""
|<provider
| android:name="%s"

View File

@ -29,5 +29,4 @@
-allowaccessmodification
-keepclassmembers class com.topjohnwu.magisk.dummy.* { <init>(); }
-keepclassmembers class com.topjohnwu.magisk.DownloadActivity { <init>(); }
-keepclassmembers class com.topjohnwu.magisk.FileProvider { <init>(); }
-keepclassmembers class com.topjohnwu.magisk.DelegateRootService { <init>(); }

View File

@ -12,6 +12,7 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
@ -79,6 +80,7 @@ public class DownloadActivity extends Activity {
private void error(Throwable e) {
Log.e(getClass().getSimpleName(), "", e);
Toast.makeText(themed, e.getMessage(), Toast.LENGTH_LONG).show();
finish();
}
@ -111,22 +113,22 @@ public class DownloadActivity extends Activity {
private void dlAPK() {
dialog = ProgressDialog.show(themed, getString(dling), getString(dling) + " " + APP_NAME, true);
Runnable onSuccess = () -> {
dialog.dismiss();
Toast.makeText(themed, relaunch_app, Toast.LENGTH_LONG).show();
finish();
};
// Download and upgrade the app
File apk = dynLoad ? DynAPK.current(this) : new File(getCacheDir(), "manager.apk");
request(apkLink).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR).getAsFile(apk, file -> {
if (dynLoad) {
DynLoad.setup(this);
runOnUiThread(() -> {
dialog.dismiss();
Toast.makeText(themed, relaunch_app, Toast.LENGTH_LONG).show();
finish();
});
onSuccess.run();
} else {
runOnUiThread(() -> {
dialog.dismiss();
APKInstall.install(this, file);
finish();
});
var receiver = APKInstall.register(this, BuildConfig.APPLICATION_ID, onSuccess);
APKInstall.installapk(this, file);
Intent intent = receiver.waitIntent();
if (intent != null) startActivity(intent);
}
});
}
@ -141,15 +143,10 @@ public class DownloadActivity extends Activity {
InputStream is = new CipherInputStream(new ByteArrayInputStream(Bytes.res()), cipher);
try (InputStream gzip = new GZIPInputStream(is);
OutputStream out = new FileOutputStream(apk)) {
byte[] buf = new byte[4096];
for (int read; (read = gzip.read(buf)) >= 0;) {
out.write(buf, 0, read);
}
APKInstall.transfer(gzip, out);
}
DynAPK.addAssetPath(getResources().getAssets(), apk.getPath());
} catch (Exception e) {
// Should not happen
e.printStackTrace();
} catch (Exception ignored) {
}
}