Remove in app module download

This commit is contained in:
NekoInverter 2020-07-19 12:56:52 +08:00
parent abf4c9b546
commit 5c0a688ee5
No known key found for this signature in database
GPG Key ID: 280D6CCCF95715F9
15 changed files with 50 additions and 934 deletions

View File

@ -77,14 +77,6 @@
<data android:scheme="package" />
</intent-filter>
</receiver>
<receiver
android:name=".receivers.DownloadReceiver"
android:exported="true"
android:permission="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
<receiver
android:name=".util.NotificationUtil$RebootReceiver"
android:exported="false" />

View File

@ -1,13 +1,8 @@
package org.meowcat.edxposed.manager;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.util.TypedValue;
@ -24,23 +19,15 @@ import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.ListFragment;
import com.google.android.material.snackbar.Snackbar;
import org.meowcat.edxposed.manager.repo.Module;
import org.meowcat.edxposed.manager.repo.ModuleVersion;
import org.meowcat.edxposed.manager.repo.ReleaseType;
import org.meowcat.edxposed.manager.repo.RepoParser;
import org.meowcat.edxposed.manager.util.DownloadsUtil;
import org.meowcat.edxposed.manager.util.HashUtil;
import org.meowcat.edxposed.manager.util.InstallApkUtil;
import org.meowcat.edxposed.manager.util.ModuleUtil.InstalledModule;
import org.meowcat.edxposed.manager.util.RepoLoader;
import org.meowcat.edxposed.manager.util.chrome.LinkTransformationMethod;
import org.meowcat.edxposed.manager.widget.DownloadView;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.text.DateFormat;
import java.util.Date;
@ -72,7 +59,7 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
getListView().addHeaderView(txtHeader);
}
VersionsAdapter sAdapter = new VersionsAdapter(activity, activity.getInstalledModule(), activity.findViewById(R.id.snackbar));
VersionsAdapter sAdapter = new VersionsAdapter(activity, activity.getInstalledModule()/*, activity.findViewById(R.id.snackbar)*/);
for (ModuleVersion version : module.versions) {
if (repoLoader.isVersionShown(version))
sAdapter.add(version);
@ -104,36 +91,6 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
setListAdapter(null);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != Activity.RESULT_OK) {
return;
}
if (requestCode == 42) {
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
try {
OutputStream os = activity.getContentResolver().openOutputStream(uri);
if (os != null) {
FileInputStream in = new FileInputStream(new File(DownloadView.lastInfo.localFilename));
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
os.close();
}
} catch (Exception e) {
e.printStackTrace();
//Snackbar.make(findViewById(R.id.snackbar), getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Snackbar.LENGTH_LONG).show();
}
}
}
}
}
static class ViewHolder {
TextView txtStatus;
TextView txtVersion;
@ -144,55 +101,6 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
TextView txtChanges;
}
public static class DownloadModuleCallback implements DownloadsUtil.DownloadFinishedCallback {
private final ModuleVersion moduleVersion;
private View snackbar;
DownloadModuleCallback(ModuleVersion moduleVersion, View snackbar) {
this.moduleVersion = moduleVersion;
this.snackbar = snackbar;
}
@Override
public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) {
File localFile = new File(info.localFilename);
if (!localFile.isFile())
return;
if (moduleVersion.md5sum != null && !moduleVersion.md5sum.isEmpty()) {
try {
String actualMd5Sum = HashUtil.md5(localFile);
if (!moduleVersion.md5sum.equals(actualMd5Sum)) {
Snackbar.make(snackbar, context.getString(R.string.download_md5sum_incorrect, actualMd5Sum, moduleVersion.md5sum), Snackbar.LENGTH_LONG).show();
DownloadsUtil.removeById(context, info.id);
return;
}
} catch (Exception e) {
Snackbar.make(snackbar, context.getString(R.string.download_could_not_read_file, e.getMessage()), Snackbar.LENGTH_LONG).show();
DownloadsUtil.removeById(context, info.id);
return;
}
}
PackageManager pm = context.getPackageManager();
PackageInfo packageInfo = pm.getPackageArchiveInfo(info.localFilename, 0);
if (packageInfo == null) {
Snackbar.make(snackbar, R.string.download_no_valid_apk, Snackbar.LENGTH_LONG).show();
DownloadsUtil.removeById(context, info.id);
return;
}
if (!packageInfo.packageName.equals(moduleVersion.module.packageName)) {
Snackbar.make(snackbar, context.getString(R.string.download_incorrect_package_name, packageInfo.packageName, moduleVersion.module.packageName), Snackbar.LENGTH_LONG).show();
DownloadsUtil.removeById(context, info.id);
return;
}
new InstallApkUtil(context, info).execute();
}
}
private class VersionsAdapter extends ArrayAdapter<ModuleVersion> {
private final DateFormat dateFormatter = DateFormat
.getDateInstance(DateFormat.SHORT);
@ -203,9 +111,9 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
private final String textInstalled;
private final String textUpdateAvailable;
private final long installedVersionCode;
private View snackbar;
//private View snackbar;
VersionsAdapter(Context context, InstalledModule installed, View snackbar) {
VersionsAdapter(Context context, InstalledModule installed/*, View snackbar*/) {
super(context, R.layout.item_version);
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
@ -218,7 +126,7 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
textInstalled = getString(R.string.download_section_installed) + ":";
textUpdateAvailable = getString(R.string.download_section_update_available) + ":";
installedVersionCode = (installed != null) ? installed.versionCode : -1;
this.snackbar = snackbar;
//this.snackbar = snackbar;
}
@SuppressLint("InflateParams")
@ -274,7 +182,7 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
holder.downloadView.setUrl(item.downloadLink);
holder.downloadView.setTitle(activity.getModule().name);
holder.downloadView.setDownloadFinishedCallback(new DownloadModuleCallback(item, snackbar));
//holder.downloadView.setDownloadFinishedCallback(new DownloadModuleCallback(item, snackbar));
if (item.changelog != null && !item.changelog.isEmpty()) {
holder.txtChangesTitle.setVisibility(View.VISIBLE);

View File

@ -32,7 +32,6 @@ import org.meowcat.edxposed.manager.repo.Module;
import org.meowcat.edxposed.manager.repo.ModuleVersion;
import org.meowcat.edxposed.manager.repo.ReleaseType;
import org.meowcat.edxposed.manager.repo.RepoDb;
import org.meowcat.edxposed.manager.util.DownloadsUtil;
import org.meowcat.edxposed.manager.util.InstallApkUtil;
import org.meowcat.edxposed.manager.util.ModuleUtil;
import org.meowcat.edxposed.manager.util.NavUtil;
@ -369,7 +368,7 @@ public class ModulesActivity extends BaseActivity implements ModuleUtil.ModuleLi
}
if (mv != null) {
DownloadsUtil.addModule(this, m.name, mv.downloadLink, (context, info) -> new InstallApkUtil(this, info).execute());
NavUtil.startURL(this, mv.downloadLink);
}
}

View File

@ -3,27 +3,28 @@ package org.meowcat.edxposed.manager.receivers;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONObject;
import org.meowcat.edxposed.manager.BuildConfig;
import org.meowcat.edxposed.manager.XposedApp;
import org.meowcat.edxposed.manager.util.NotificationUtil;
import org.meowcat.edxposed.manager.util.TaskRunner;
import org.meowcat.edxposed.manager.util.json.JSONUtils;
import java.util.concurrent.Callable;
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, Intent intent) {
new android.os.Handler().postDelayed(() -> new CheckUpdates().execute(), 60 * 60 * 1000 /*60 min*/);
new TaskRunner().executeAsync(new LongRunningTask());
}
private static class CheckUpdates extends AsyncTask<Void, Void, Void> {
private static class LongRunningTask implements Callable<Void> {
@Override
protected Void doInBackground(Void... params) {
public Void call() {
try {
Thread.sleep(60 * 60 * 1000);
String jsonString = JSONUtils.getFileContent(JSONUtils.JSON_LINK).replace("%XPOSED_ZIP%", "");
String newApkVersion = new JSONObject(jsonString).getJSONObject("apk").getString("version");
@ -35,11 +36,9 @@ public class BootReceiver extends BroadcastReceiver {
NotificationUtil.showInstallerUpdateNotification();
}
} catch (Exception e) {
//noinspection ConstantConditions
Log.d(XposedApp.TAG, e.getMessage());
e.printStackTrace();
}
return null;
}
}
}

View File

@ -1,26 +0,0 @@
package org.meowcat.edxposed.manager.receivers;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
import org.meowcat.edxposed.manager.util.DownloadsUtil;
public class DownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
try {
String action = intent.getAction();
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0);
DownloadsUtil.triggerDownloadFinishedCallback(context, downloadId);
}
} catch (Exception e) {//Flyme
e.printStackTrace();
Toast.makeText(context, "shit flyme boom", Toast.LENGTH_LONG).show();
}
}
}

View File

@ -1,17 +1,7 @@
package org.meowcat.edxposed.manager.util;
import android.app.DownloadManager;
import android.app.DownloadManager.Query;
import android.app.DownloadManager.Request;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import org.meowcat.edxposed.manager.R;
import org.meowcat.edxposed.manager.XposedApp;
@ -23,303 +13,10 @@ import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class DownloadsUtil {
public static final String MIME_TYPE_APK = "application/vnd.android.package-archive";
private static final Map<String, DownloadFinishedCallback> callbacks = new HashMap<>();
private static final SharedPreferences pref = XposedApp.getInstance().getSharedPreferences("download_cache", Context.MODE_PRIVATE);
private static DownloadInfo add(Builder b) {
Context context = b.context;
removeAllForUrl(context, b.url);
synchronized (callbacks) {
callbacks.put(b.url, b.callback);
}
Request request = new Request(Uri.parse(b.url));
request.setTitle(b.title);
request.setMimeType(b.mimeType.toString());
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE);
File path = new File(context.getExternalCacheDir(), "downloads");
try {
if (!path.mkdirs()) return null;
}catch (Exception e) {
e.printStackTrace();
return null;
}
File destination = new File(path, b.title + b.mimeType.getExtension());
removeAllForLocalFile(context, destination);
request.setDestinationUri(Uri.fromFile(destination));
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
long id = dm.enqueue(request);
return getById(context, id);
}
public static DownloadInfo addModule(Context context, String title, String url, DownloadFinishedCallback callback) {
return new Builder(context)
.setTitle(title)
.setUrl(url)
.setCallback(callback)
.setModule(true)
.setMimeType(MIME_TYPES.APK)
.download();
}
/*
public static ModuleVersion getStableVersion(Module m) {
for (int i = 0; i < m.versions.size(); i++) {
ModuleVersion mvTemp = m.versions.get(i);
if (mvTemp.relType == ReleaseType.STABLE) {
return mvTemp;
}
}
return null;
}
*/
public static DownloadInfo getById(Context context, long id) {
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = dm.query(new Query().setFilterById(id));
if (!c.moveToFirst()) {
c.close();
return null;
}
int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI);
int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE);
int columnLastMod = c.getColumnIndexOrThrow(
DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON);
int status = c.getInt(columnStatus);
String localFilename;
try {
localFilename = getFilenameFromUri(c.getString(columnLocalUri));
} catch (UnsupportedOperationException e) {
Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show();
return null;
}
if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) {
dm.remove(id);
c.close();
return null;
}
DownloadInfo info = new DownloadInfo(id, c.getString(columnUri),
c.getString(columnTitle), c.getLong(columnLastMod),
localFilename, status,
c.getInt(columnTotalSize), c.getInt(columnBytesDownloaded),
c.getInt(columnReason));
c.close();
return info;
}
public static DownloadInfo getLatestForUrl(Context context, String url) {
List<DownloadInfo> all;
try {
all = getAllForUrl(context, url);
} catch (Throwable throwable) {
return null;
}
return Objects.requireNonNull(all).isEmpty() ? null : all.get(0);
}
private static List<DownloadInfo> getAllForUrl(Context context, String url) {
DownloadManager dm = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = dm.query(new Query());
int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI);
int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE);
int columnLastMod = c.getColumnIndexOrThrow(
DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON);
List<DownloadInfo> downloads = new ArrayList<>();
while (c.moveToNext()) {
if (!url.equals(c.getString(columnUri)))
continue;
int status = c.getInt(columnStatus);
String localFilename;
try {
localFilename = getFilenameFromUri(c.getString(columnLocalUri));
} catch (UnsupportedOperationException e) {
Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show();
return null;
}
if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) {
dm.remove(c.getLong(columnId));
continue;
}
downloads.add(new DownloadInfo(c.getLong(columnId),
c.getString(columnUri), c.getString(columnTitle),
c.getLong(columnLastMod), localFilename,
status, c.getInt(columnTotalSize),
c.getInt(columnBytesDownloaded), c.getInt(columnReason)));
}
c.close();
Collections.sort(downloads);
return downloads;
}
public static void removeById(Context context, long id) {
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
dm.remove(id);
}
private static void removeAllForUrl(Context context, String url) {
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = dm.query(new Query());
int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI);
List<Long> idsList = new ArrayList<>(1);
while (c.moveToNext()) {
if (url.equals(c.getString(columnUri)))
idsList.add(c.getLong(columnId));
}
c.close();
if (idsList.isEmpty())
return;
long[] ids = new long[idsList.size()];
for (int i = 0; i < ids.length; i++)
ids[i] = idsList.get(i);
dm.remove(ids);
}
private static void removeAllForLocalFile(Context context, File file) {
//noinspection ResultOfMethodCallIgnored
file.delete();
String filename;
try {
filename = file.getCanonicalPath();
} catch (IOException e) {
Log.w(XposedApp.TAG, "Could not resolve path for " + file.getAbsolutePath(), e);
return;
}
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = dm.query(new Query());
int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
List<Long> idsList = new ArrayList<>(1);
while (c.moveToNext()) {
String itemFilename;
try {
itemFilename = getFilenameFromUri(c.getString(columnLocalUri));
} catch (UnsupportedOperationException e) {
Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show();
itemFilename = null;
}
if (itemFilename != null) {
if (filename.equals(itemFilename)) {
idsList.add(c.getLong(columnId));
} else {
try {
if (filename.equals(new File(itemFilename).getCanonicalPath())) {
idsList.add(c.getLong(columnId));
}
} catch (IOException ignored) {
}
}
}
}
c.close();
if (idsList.isEmpty())
return;
long[] ids = new long[idsList.size()];
for (int i = 0; i < ids.length; i++)
ids[i] = idsList.get(i);
dm.remove(ids);
}
// public static void removeOutdated(Context context, long cutoff) {
// DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
// Cursor c = dm.query(new Query());
// int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
// int columnLastMod = c.getColumnIndexOrThrow(
// DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
//
// List<Long> idsList = new ArrayList<>();
// while (c.moveToNext()) {
// if (c.getLong(columnLastMod) < cutoff)
// idsList.add(c.getLong(columnId));
// }
// c.close();
//
// if (idsList.isEmpty())
// return;
//
// long[] ids = new long[idsList.size()];
// for (int i = 0; i < ids.length; i++)
// ids[i] = idsList.get(0);
//
// dm.remove(ids);
// }
public static void triggerDownloadFinishedCallback(Context context, long id) {
DownloadInfo info = getById(context, id);
if (info == null || info.status != DownloadManager.STATUS_SUCCESSFUL)
return;
DownloadFinishedCallback callback;
synchronized (callbacks) {
callback = callbacks.get(info.url);
}
if (callback == null)
return;
// Hack to reset stat information.
//noinspection ResultOfMethodCallIgnored
new File(info.localFilename).setExecutable(false);
callback.onDownloadFinished(context, info);
}
private static String getFilenameFromUri(String uriString) {
if (uriString == null) {
return null;
}
Uri uri = Uri.parse(uriString);
if (Objects.requireNonNull(uri.getScheme()).equals("file")) {
return uri.getPath();
} else if (uri.getScheme().equals("content")) {
Context context = XposedApp.getInstance();
try (Cursor c = context.getContentResolver().query(uri, new String[]{MediaStore.Files.FileColumns.DATA}, null, null, null)) {
Objects.requireNonNull(c).moveToFirst();
return c.getString(c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA));
}
} else {
throw new UnsupportedOperationException("Unexpected URI: " + uriString);
}
}
static SyncDownloadInfo downloadSynchronously(String url, File target) {
final boolean useNotModifiedTags = target.exists();
@ -416,111 +113,6 @@ public class DownloadsUtil {
}
}
public enum MIME_TYPES {
APK {
@NonNull
public String toString() {
return MIME_TYPE_APK;
}
public String getExtension() {
return ".apk";
}
};
// ZIP {
// public String toString() {
// return MIME_TYPE_ZIP;
// }
//
// public String getExtension() {
// return ".zip";
// }
// };
public String getExtension() {
return null;
}
}
public interface DownloadFinishedCallback {
void onDownloadFinished(Context context, DownloadInfo info);
}
public static class Builder {
private final Context context;
boolean module = false;
private String title = null;
private String url = null;
private DownloadFinishedCallback callback = null;
private MIME_TYPES mimeType = MIME_TYPES.APK;
public Builder(Context context) {
this.context = context;
}
public Builder setTitle(String title) {
this.title = title;
return this;
}
public Builder setUrl(String url) {
this.url = url;
return this;
}
public Builder setCallback(DownloadFinishedCallback callback) {
this.callback = callback;
return this;
}
Builder setMimeType(MIME_TYPES mimeType) {
this.mimeType = mimeType;
return this;
}
public Builder setModule(boolean module) {
this.module = module;
return this;
}
public DownloadInfo download() {
return add(this);
}
}
public static class DownloadInfo implements Comparable<DownloadInfo> {
public final long id;
public final String url;
public final String title;
public final String localFilename;
public final int status;
public final int totalSize;
public final int bytesDownloaded;
public final int reason;
final long lastModification;
private DownloadInfo(long id, String url, String title, long lastModification, String localFilename, int status, int totalSize, int bytesDownloaded, int reason) {
this.id = id;
this.url = url;
this.title = title;
this.lastModification = lastModification;
this.localFilename = localFilename;
this.status = status;
this.totalSize = totalSize;
this.bytesDownloaded = bytesDownloaded;
this.reason = reason;
}
@Override
public int compareTo(@NonNull DownloadInfo another) {
int compare = (int) (another.lastModification
- this.lastModification);
if (compare != 0)
return compare;
return this.url.compareTo(another.url);
}
}
public static class SyncDownloadInfo {
static final int STATUS_SUCCESS = 0;
static final int STATUS_NOT_MODIFIED = 1;

View File

@ -1,41 +1,10 @@
package org.meowcat.edxposed.manager.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.AsyncTask;
import androidx.core.content.FileProvider;
import com.topjohnwu.superuser.Shell;
import org.meowcat.edxposed.manager.BuildConfig;
import org.meowcat.edxposed.manager.R;
import org.meowcat.edxposed.manager.XposedApp;
import java.io.File;
import java.util.LinkedList;
import java.util.List;
public class InstallApkUtil extends AsyncTask<Void, Void, Integer> {
private static final int ERROR_ROOT_NOT_GRANTED = -99;
private final DownloadsUtil.DownloadInfo info;
@SuppressLint("StaticFieldLeak")
private final Context context;
private boolean isApkRootInstallOn;
private List<String> output = new LinkedList<>();
public InstallApkUtil(Context context, DownloadsUtil.DownloadInfo info) {
this.context = context;
this.info = info;
}
public class InstallApkUtil {
public static String getAppLabel(ApplicationInfo info, PackageManager pm) {
try {
@ -47,74 +16,4 @@ public class InstallApkUtil extends AsyncTask<Void, Void, Integer> {
}
return info.loadLabel(pm).toString();
}
static void installApkNormally(Context context, String localFilename) {
Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", new File(localFilename));
installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
installIntent.setDataAndType(uri, DownloadsUtil.MIME_TYPE_APK);
installIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.getApplicationInfo().packageName);
context.startActivity(installIntent);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
SharedPreferences prefs = XposedApp.getPreferences();
isApkRootInstallOn = prefs.getBoolean("install_with_su", false);
if (isApkRootInstallOn) {
NotificationUtil.showModuleInstallingNotification(info.title);
}
}
@Override
protected Integer doInBackground(Void... params) {
int returnCode = 0;
if (isApkRootInstallOn) {
try {
String path = "/data/local/tmp/";
String fileName = new File(info.localFilename).getName();
Shell.su("cat \"" + info.localFilename + "\">" + path + fileName).exec();
Shell.Result result = Shell.su("pm install -r -f \"" + path + fileName + "\"").exec();
returnCode = result.getCode();
output = result.getOut();
Shell.su("rm -f " + path + fileName).exec();
} catch (IllegalStateException e) {
returnCode = ERROR_ROOT_NOT_GRANTED;
}
}
return returnCode;
}
@Override
protected void onPostExecute(Integer result) {
super.onPostExecute(result);
if (isApkRootInstallOn) {
NotificationUtil.cancel(NotificationUtil.NOTIFICATION_MODULE_INSTALLING);
if (result.equals(ERROR_ROOT_NOT_GRANTED)) {
NotificationUtil.showModuleInstallNotification(R.string.installation_error, R.string.root_failed, info.localFilename);
return;
}
StringBuilder out = new StringBuilder();
for (String o : output) {
out.append(o);
out.append("\n");
}
if (result.equals(0)) {
NotificationUtil.showModuleInstallNotification(R.string.installation_successful, R.string.installation_successful_message, info.localFilename, info.title);
} else {
NotificationUtil.showModuleInstallNotification(R.string.installation_error, R.string.installation_error_message, info.localFilename, info.title, out);
installApkNormally(context, info.localFilename);
}
} else {
installApkNormally(context, info.localFilename);
}
}
}

View File

@ -12,7 +12,6 @@ import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
@ -25,17 +24,14 @@ import org.meowcat.edxposed.manager.XposedApp;
public final class NotificationUtil {
public static final int NOTIFICATION_MODULE_NOT_ACTIVATED_YET = 0;
public static final int NOTIFICATION_MODULE_INSTALLING = 4;
private static final int NOTIFICATION_MODULES_UPDATED = 1;
private static final int NOTIFICATION_INSTALLER_UPDATE = 2;
private static final int NOTIFICATION_MODULE_INSTALLATION = 3;
private static final int PENDING_INTENT_OPEN_MODULES = 0;
private static final int PENDING_INTENT_OPEN_INSTALL = 1;
private static final int PENDING_INTENT_SOFT_REBOOT = 2;
private static final int PENDING_INTENT_REBOOT = 3;
private static final int PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT = 4;
private static final int PENDING_INTENT_ACTIVATE_MODULE = 5;
private static final int PENDING_INTENT_INSTALL_APK = 6;
private static final String HEADS_UP = "heads_up";
private static final String FRAGMENT_ID = "fragment";
@ -65,10 +61,6 @@ public final class NotificationUtil {
}
}
public static void cancel(int id) {
notificationManager.cancel(id);
}
public static void cancel(String tag, int id) {
notificationManager.cancel(tag, id);
}
@ -145,48 +137,6 @@ public final class NotificationUtil {
notificationManager.notify(null, NOTIFICATION_MODULES_UPDATED, builder.build());
}
static void showModuleInstallNotification(@StringRes int title, @StringRes int message, String path, Object... args) {
showModuleInstallNotification(context.getString(title), context.getString(message, args), path, title == R.string.installation_error);
}
private static void showModuleInstallNotification(String title, String message, String path, boolean error) {
NotificationCompat.Builder builder = getNotificationBuilder(title, message, NOTIFICATION_MODULES_CHANNEL);
if (error) {
Intent iInstallApk = new Intent(context, ApkReceiver.class);
iInstallApk.putExtra(ApkReceiver.EXTRA_APK_PATH, path);
PendingIntent pInstallApk = PendingIntent.getBroadcast(context, PENDING_INTENT_INSTALL_APK, iInstallApk, PendingIntent.FLAG_UPDATE_CURRENT);
builder.addAction(new NotificationCompat.Action.Builder(0, context.getString(R.string.installation_apk_normal), pInstallApk).build());
}
if (prefs.getBoolean(HEADS_UP, true)) {
builder.setPriority(2);
}
NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle();
notiStyle.setBigContentTitle(title);
notiStyle.bigText(message);
builder.setStyle(notiStyle);
notificationManager.notify(null, NOTIFICATION_MODULE_INSTALLATION, builder.build());
new android.os.Handler().postDelayed(() -> cancel(NOTIFICATION_MODULE_INSTALLATION), 10 * 1000);
}
public static void showModuleInstallingNotification(String appName) {
String title = context.getString(R.string.install_load);
String message = context.getString(R.string.install_load_apk, appName);
NotificationCompat.Builder builder = getNotificationBuilder(title, message, NOTIFICATION_MODULES_CHANNEL)
.setProgress(0, 0, true)
.setOngoing(true);
NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle();
notiStyle.setBigContentTitle(title);
notiStyle.bigText(message);
builder.setStyle(notiStyle);
notificationManager.notify(null, NOTIFICATION_MODULE_INSTALLING, builder.build());
}
public static void showInstallerUpdateNotification() {
Intent intent = new Intent(context, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@ -264,26 +214,4 @@ public final class NotificationUtil {
}
}
}
public static class ApkReceiver extends BroadcastReceiver {
public static final String EXTRA_APK_PATH = "path";
@Override
public void onReceive(Context context, Intent intent) {
/*
* Close the notification bar in order to see the toast that module
* was enabled successfully. Furthermore, if SU permissions haven't
* been granted yet, the SU dialog will be prompted behind the
* expanded notification panel and is therefore not visible to the
* user.
*/
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
if (intent.hasExtra(EXTRA_APK_PATH)) {
String path = intent.getStringExtra(EXTRA_APK_PATH);
InstallApkUtil.installApkNormally(context, path);
}
NotificationUtil.cancel(NotificationUtil.NOTIFICATION_MODULE_INSTALLATION);
}
}
}

View File

@ -0,0 +1,19 @@
package org.meowcat.edxposed.manager.util;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class TaskRunner {
private final Executor executor = Executors.newSingleThreadExecutor(); // change according to your requirements
public <R> void executeAsync(Callable<R> callable) {
executor.execute(() -> {
try {
callable.call();
} catch (Exception e) {
e.printStackTrace();
}
});
}
}

View File

@ -1,8 +1,6 @@
package org.meowcat.edxposed.manager.widget;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@ -10,86 +8,16 @@ import android.widget.LinearLayout;
import androidx.fragment.app.Fragment;
import org.meowcat.edxposed.manager.BaseActivity;
import org.meowcat.edxposed.manager.R;
import org.meowcat.edxposed.manager.databinding.DownloadViewBinding;
import org.meowcat.edxposed.manager.util.DownloadsUtil;
import org.meowcat.edxposed.manager.util.DownloadsUtil.DownloadFinishedCallback;
import org.meowcat.edxposed.manager.util.NavUtil;
public class DownloadView extends LinearLayout {
public static DownloadsUtil.DownloadInfo lastInfo = null;
public Fragment fragment;
private DownloadsUtil.DownloadInfo mInfo = null;
private String mUrl = null;
private String mTitle = null;
private DownloadFinishedCallback mCallback = null;
private DownloadViewBinding binding;
private final Runnable refreshViewRunnable = new Runnable() {
@Override
public void run() {
if (mUrl == null) {
binding.btnDownload.setVisibility(View.GONE);
binding.btnSave.setVisibility(View.GONE);
binding.btnDownloadCancel.setVisibility(View.GONE);
binding.btnInstall.setVisibility(View.GONE);
binding.progress.setVisibility(View.GONE);
binding.txtInfo.setVisibility(View.VISIBLE);
binding.txtInfo.setText(R.string.download_view_no_url);
} else if (mInfo == null) {
binding.btnDownload.setVisibility(View.VISIBLE);
binding.btnSave.setVisibility(View.VISIBLE);
binding.btnDownloadCancel.setVisibility(View.GONE);
binding.btnInstall.setVisibility(View.GONE);
binding.progress.setVisibility(View.GONE);
binding.txtInfo.setVisibility(View.GONE);
} else {
switch (mInfo.status) {
case DownloadManager.STATUS_PENDING:
case DownloadManager.STATUS_PAUSED:
case DownloadManager.STATUS_RUNNING:
binding.btnDownload.setVisibility(View.GONE);
binding.btnSave.setVisibility(View.GONE);
binding.btnDownloadCancel.setVisibility(View.VISIBLE);
binding.btnInstall.setVisibility(View.GONE);
binding.progress.setVisibility(View.VISIBLE);
binding.txtInfo.setVisibility(View.VISIBLE);
if (mInfo.totalSize <= 0 || mInfo.status != DownloadManager.STATUS_RUNNING) {
binding.progress.setIndeterminate(true);
binding.txtInfo.setText(R.string.download_view_waiting);
} else {
binding.progress.setIndeterminate(false);
binding.progress.setMax(mInfo.totalSize);
binding.progress.setProgress(mInfo.bytesDownloaded);
binding.txtInfo.setText(getContext().getString(
R.string.download_view_running,
mInfo.bytesDownloaded / 1024,
mInfo.totalSize / 1024));
}
break;
case DownloadManager.STATUS_FAILED:
binding.btnDownload.setVisibility(View.VISIBLE);
binding.btnSave.setVisibility(View.VISIBLE);
binding.btnDownloadCancel.setVisibility(View.GONE);
binding.btnInstall.setVisibility(View.GONE);
binding.progress.setVisibility(View.GONE);
binding.txtInfo.setVisibility(View.VISIBLE);
binding.txtInfo.setText(getContext().getString(
R.string.download_view_failed, mInfo.reason));
break;
case DownloadManager.STATUS_SUCCESSFUL:
binding.btnDownload.setVisibility(View.GONE);
binding.btnSave.setVisibility(View.VISIBLE);
binding.btnDownloadCancel.setVisibility(View.GONE);
binding.btnInstall.setVisibility(View.VISIBLE);
binding.progress.setVisibility(View.GONE);
binding.txtInfo.setVisibility(View.VISIBLE);
binding.txtInfo.setText(R.string.download_view_successful);
break;
}
}
}
};
public DownloadView(Context context, final AttributeSet attrs) {
super(context, attrs);
@ -98,51 +26,7 @@ public class DownloadView extends LinearLayout {
binding = DownloadViewBinding.inflate(LayoutInflater.from(context), this);
binding.btnDownload.setOnClickListener(v -> {
mInfo = DownloadsUtil.addModule(getContext(), mTitle, mUrl, mCallback);
refreshViewFromUiThread();
if (mInfo != null)
new DownloadMonitor().start();
});
binding.btnSave.setOnClickListener(v -> {
lastInfo = mInfo;
mInfo = DownloadsUtil.addModule(getContext(), mTitle, mUrl, (context1, info) -> {
Intent exportIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
exportIntent.addCategory(Intent.CATEGORY_OPENABLE);
exportIntent.setType(DownloadsUtil.MIME_TYPE_APK);
exportIntent.putExtra(Intent.EXTRA_TITLE, mTitle + ".apk");
fragment.startActivityForResult(exportIntent, 42);
});
refreshViewFromUiThread();
if (mInfo != null)
new DownloadMonitor().start();
});
binding.btnDownloadCancel.setOnClickListener(v -> {
if (mInfo == null)
return;
DownloadsUtil.removeById(getContext(), mInfo.id);
// UI update will happen automatically by the DownloadMonitor
});
binding.btnInstall.setOnClickListener(v -> {
if (mCallback == null)
return;
mCallback.onDownloadFinished(getContext(), mInfo);
});
refreshViewFromUiThread();
}
private void refreshViewFromUiThread() {
refreshViewRunnable.run();
}
private void refreshView() {
post(refreshViewRunnable);
binding.btnDownload.setOnClickListener(v -> NavUtil.startURL((BaseActivity) context, mUrl));
}
public String getUrl() {
@ -151,13 +35,14 @@ public class DownloadView extends LinearLayout {
public void setUrl(String url) {
mUrl = url;
if (mUrl != null)
mInfo = DownloadsUtil.getLatestForUrl(getContext(), mUrl);
else
mInfo = null;
refreshView();
if (mUrl != null) {
binding.btnDownload.setVisibility(View.VISIBLE);
binding.txtInfo.setVisibility(View.GONE);
}else {
binding.btnDownload.setVisibility(View.GONE);
binding.txtInfo.setVisibility(View.VISIBLE);
binding.txtInfo.setText(R.string.download_view_no_url);
}
}
public String getTitle() {
@ -167,44 +52,4 @@ public class DownloadView extends LinearLayout {
public void setTitle(String title) {
this.mTitle = title;
}
@SuppressWarnings("unused")
public DownloadFinishedCallback getDownloadFinishedCallback() {
return mCallback;
}
public void setDownloadFinishedCallback(DownloadFinishedCallback downloadFinishedCallback) {
this.mCallback = downloadFinishedCallback;
}
private class DownloadMonitor extends Thread {
DownloadMonitor() {
super("DownloadMonitor");
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
return;
}
try {
mInfo = DownloadsUtil.getById(getContext(), mInfo.id);
} catch (NullPointerException ignored) {
}
refreshView();
if (mInfo == null)
return;
if (mInfo.status != DownloadManager.STATUS_PENDING
&& mInfo.status != DownloadManager.STATUS_PAUSED
&& mInfo.status != DownloadManager.STATUS_RUNNING)
return;
}
}
}
}

View File

@ -1,5 +1,4 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
@ -21,43 +20,5 @@
android:layout_marginEnd="4dp"
android:text="@string/download_view_download"
android:theme="@style/Widget.AppCompat.Button.Colored" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSave"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/save"
android:theme="@style/Widget.AppCompat.Button.Colored" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDownloadCancel"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/download_view_cancel"
android:theme="@style/Widget.AppCompat.Button.Colored"
android:visibility="gone"
tools:ignore="ButtonOrder" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnInstall"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/download_view_install"
android:theme="@style/Widget.AppCompat.Button.Colored"
android:visibility="gone" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</merge>

View File

@ -139,7 +139,7 @@
<string name="about_libraries_label">使用的库</string>
<!-- DownloadView -->
<string name="download_view_download">下载并安装</string>
<string name="download_view_download">下载</string>
<string name="download_view_install">安装</string>
<string name="download_view_cancel">取消</string>
<string name="download_view_no_url">无可用的下载链接</string>

View File

@ -139,7 +139,7 @@
<string name="about_libraries_label">使用的庫</string>
<!-- DownloadView -->
<string name="download_view_download">下載並安裝</string>
<string name="download_view_download">下載</string>
<string name="download_view_install">安裝</string>
<string name="download_view_cancel">取消</string>
<string name="download_view_no_url">無可用的下載鏈接</string>

View File

@ -139,7 +139,7 @@
<string name="about_libraries_label">使用的庫</string>
<!-- DownloadView -->
<string name="download_view_download">下載並安裝</string>
<string name="download_view_download">下載</string>
<string name="download_view_install">安裝</string>
<string name="download_view_cancel">取消</string>
<string name="download_view_no_url">無可用的下載連結</string>

View File

@ -141,7 +141,7 @@
<string name="about_libraries_label">Used libraries</string>
<!-- DownloadView -->
<string name="download_view_download">Download and Install</string>
<string name="download_view_download">Download</string>
<string name="download_view_install">Install</string>
<string name="download_view_cancel">Cancel</string>
<string name="download_view_no_url">No download URL available</string>