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" /> <data android:scheme="package" />
</intent-filter> </intent-filter>
</receiver> </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 <receiver
android:name=".util.NotificationUtil$RebootReceiver" android:name=".util.NotificationUtil$RebootReceiver"
android:exported="false" /> android:exported="false" />

View File

@ -1,13 +1,8 @@
package org.meowcat.edxposed.manager; package org.meowcat.edxposed.manager;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context; 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.content.res.Resources;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.util.TypedValue; import android.util.TypedValue;
@ -24,23 +19,15 @@ import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.fragment.app.ListFragment; 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.Module;
import org.meowcat.edxposed.manager.repo.ModuleVersion; import org.meowcat.edxposed.manager.repo.ModuleVersion;
import org.meowcat.edxposed.manager.repo.ReleaseType; import org.meowcat.edxposed.manager.repo.ReleaseType;
import org.meowcat.edxposed.manager.repo.RepoParser; 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.ModuleUtil.InstalledModule;
import org.meowcat.edxposed.manager.util.RepoLoader; import org.meowcat.edxposed.manager.util.RepoLoader;
import org.meowcat.edxposed.manager.util.chrome.LinkTransformationMethod; import org.meowcat.edxposed.manager.util.chrome.LinkTransformationMethod;
import org.meowcat.edxposed.manager.widget.DownloadView; 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.text.DateFormat;
import java.util.Date; import java.util.Date;
@ -72,7 +59,7 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
getListView().addHeaderView(txtHeader); 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) { for (ModuleVersion version : module.versions) {
if (repoLoader.isVersionShown(version)) if (repoLoader.isVersionShown(version))
sAdapter.add(version); sAdapter.add(version);
@ -104,36 +91,6 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
setListAdapter(null); 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 { static class ViewHolder {
TextView txtStatus; TextView txtStatus;
TextView txtVersion; TextView txtVersion;
@ -144,55 +101,6 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
TextView txtChanges; 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 class VersionsAdapter extends ArrayAdapter<ModuleVersion> {
private final DateFormat dateFormatter = DateFormat private final DateFormat dateFormatter = DateFormat
.getDateInstance(DateFormat.SHORT); .getDateInstance(DateFormat.SHORT);
@ -203,9 +111,9 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
private final String textInstalled; private final String textInstalled;
private final String textUpdateAvailable; private final String textUpdateAvailable;
private final long installedVersionCode; 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); super(context, R.layout.item_version);
TypedValue typedValue = new TypedValue(); TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme(); Resources.Theme theme = context.getTheme();
@ -218,7 +126,7 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
textInstalled = getString(R.string.download_section_installed) + ":"; textInstalled = getString(R.string.download_section_installed) + ":";
textUpdateAvailable = getString(R.string.download_section_update_available) + ":"; textUpdateAvailable = getString(R.string.download_section_update_available) + ":";
installedVersionCode = (installed != null) ? installed.versionCode : -1; installedVersionCode = (installed != null) ? installed.versionCode : -1;
this.snackbar = snackbar; //this.snackbar = snackbar;
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@ -274,7 +182,7 @@ public class DownloadDetailsVersionsFragment extends ListFragment {
holder.downloadView.setUrl(item.downloadLink); holder.downloadView.setUrl(item.downloadLink);
holder.downloadView.setTitle(activity.getModule().name); 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()) { if (item.changelog != null && !item.changelog.isEmpty()) {
holder.txtChangesTitle.setVisibility(View.VISIBLE); 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.ModuleVersion;
import org.meowcat.edxposed.manager.repo.ReleaseType; import org.meowcat.edxposed.manager.repo.ReleaseType;
import org.meowcat.edxposed.manager.repo.RepoDb; 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.InstallApkUtil;
import org.meowcat.edxposed.manager.util.ModuleUtil; import org.meowcat.edxposed.manager.util.ModuleUtil;
import org.meowcat.edxposed.manager.util.NavUtil; import org.meowcat.edxposed.manager.util.NavUtil;
@ -369,7 +368,7 @@ public class ModulesActivity extends BaseActivity implements ModuleUtil.ModuleLi
} }
if (mv != null) { 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.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONObject; import org.json.JSONObject;
import org.meowcat.edxposed.manager.BuildConfig; 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.NotificationUtil;
import org.meowcat.edxposed.manager.util.TaskRunner;
import org.meowcat.edxposed.manager.util.json.JSONUtils; import org.meowcat.edxposed.manager.util.json.JSONUtils;
import java.util.concurrent.Callable;
public class BootReceiver extends BroadcastReceiver { public class BootReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(final Context context, Intent intent) { 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 @Override
protected Void doInBackground(Void... params) { public Void call() {
try { try {
Thread.sleep(60 * 60 * 1000);
String jsonString = JSONUtils.getFileContent(JSONUtils.JSON_LINK).replace("%XPOSED_ZIP%", ""); String jsonString = JSONUtils.getFileContent(JSONUtils.JSON_LINK).replace("%XPOSED_ZIP%", "");
String newApkVersion = new JSONObject(jsonString).getJSONObject("apk").getString("version"); String newApkVersion = new JSONObject(jsonString).getJSONObject("apk").getString("version");
@ -35,11 +36,9 @@ public class BootReceiver extends BroadcastReceiver {
NotificationUtil.showInstallerUpdateNotification(); NotificationUtil.showInstallerUpdateNotification();
} }
} catch (Exception e) { } catch (Exception e) {
//noinspection ConstantConditions e.printStackTrace();
Log.d(XposedApp.TAG, e.getMessage());
} }
return null; 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; 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.Context;
import android.content.SharedPreferences; 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.R;
import org.meowcat.edxposed.manager.XposedApp; import org.meowcat.edxposed.manager.XposedApp;
@ -23,303 +13,10 @@ import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; 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 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 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) { static SyncDownloadInfo downloadSynchronously(String url, File target) {
final boolean useNotModifiedTags = target.exists(); 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 { public static class SyncDownloadInfo {
static final int STATUS_SUCCESS = 0; static final int STATUS_SUCCESS = 0;
static final int STATUS_NOT_MODIFIED = 1; static final int STATUS_NOT_MODIFIED = 1;

View File

@ -1,41 +1,10 @@
package org.meowcat.edxposed.manager.util; 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.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Resources; import android.content.res.Resources;
import android.net.Uri;
import android.os.AsyncTask;
import androidx.core.content.FileProvider; public class InstallApkUtil {
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 static String getAppLabel(ApplicationInfo info, PackageManager pm) { public static String getAppLabel(ApplicationInfo info, PackageManager pm) {
try { try {
@ -47,74 +16,4 @@ public class InstallApkUtil extends AsyncTask<Void, Void, Integer> {
} }
return info.loadLabel(pm).toString(); 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.util.Log;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
@ -25,17 +24,14 @@ import org.meowcat.edxposed.manager.XposedApp;
public final class NotificationUtil { public final class NotificationUtil {
public static final int NOTIFICATION_MODULE_NOT_ACTIVATED_YET = 0; 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_MODULES_UPDATED = 1;
private static final int NOTIFICATION_INSTALLER_UPDATE = 2; 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_MODULES = 0;
private static final int PENDING_INTENT_OPEN_INSTALL = 1; private static final int PENDING_INTENT_OPEN_INSTALL = 1;
private static final int PENDING_INTENT_SOFT_REBOOT = 2; private static final int PENDING_INTENT_SOFT_REBOOT = 2;
private static final int PENDING_INTENT_REBOOT = 3; 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_AND_REBOOT = 4;
private static final int PENDING_INTENT_ACTIVATE_MODULE = 5; 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 HEADS_UP = "heads_up";
private static final String FRAGMENT_ID = "fragment"; 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) { public static void cancel(String tag, int id) {
notificationManager.cancel(tag, id); notificationManager.cancel(tag, id);
} }
@ -145,48 +137,6 @@ public final class NotificationUtil {
notificationManager.notify(null, NOTIFICATION_MODULES_UPDATED, builder.build()); 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() { public static void showInstallerUpdateNotification() {
Intent intent = new Intent(context, MainActivity.class); Intent intent = new Intent(context, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 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; package org.meowcat.edxposed.manager.widget;
import android.app.DownloadManager;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -10,86 +8,16 @@ import android.widget.LinearLayout;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import org.meowcat.edxposed.manager.BaseActivity;
import org.meowcat.edxposed.manager.R; import org.meowcat.edxposed.manager.R;
import org.meowcat.edxposed.manager.databinding.DownloadViewBinding; import org.meowcat.edxposed.manager.databinding.DownloadViewBinding;
import org.meowcat.edxposed.manager.util.DownloadsUtil; import org.meowcat.edxposed.manager.util.NavUtil;
import org.meowcat.edxposed.manager.util.DownloadsUtil.DownloadFinishedCallback;
public class DownloadView extends LinearLayout { public class DownloadView extends LinearLayout {
public static DownloadsUtil.DownloadInfo lastInfo = null;
public Fragment fragment; public Fragment fragment;
private DownloadsUtil.DownloadInfo mInfo = null;
private String mUrl = null; private String mUrl = null;
private String mTitle = null; private String mTitle = null;
private DownloadFinishedCallback mCallback = null;
private DownloadViewBinding binding; 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) { public DownloadView(Context context, final AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@ -98,51 +26,7 @@ public class DownloadView extends LinearLayout {
binding = DownloadViewBinding.inflate(LayoutInflater.from(context), this); binding = DownloadViewBinding.inflate(LayoutInflater.from(context), this);
binding.btnDownload.setOnClickListener(v -> { binding.btnDownload.setOnClickListener(v -> NavUtil.startURL((BaseActivity) context, mUrl));
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);
} }
public String getUrl() { public String getUrl() {
@ -151,13 +35,14 @@ public class DownloadView extends LinearLayout {
public void setUrl(String url) { public void setUrl(String url) {
mUrl = url; mUrl = url;
if (mUrl != null) {
if (mUrl != null) binding.btnDownload.setVisibility(View.VISIBLE);
mInfo = DownloadsUtil.getLatestForUrl(getContext(), mUrl); binding.txtInfo.setVisibility(View.GONE);
else }else {
mInfo = null; binding.btnDownload.setVisibility(View.GONE);
binding.txtInfo.setVisibility(View.VISIBLE);
refreshView(); binding.txtInfo.setText(R.string.download_view_no_url);
}
} }
public String getTitle() { public String getTitle() {
@ -167,44 +52,4 @@ public class DownloadView extends LinearLayout {
public void setTitle(String title) { public void setTitle(String title) {
this.mTitle = 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" <merge xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -21,43 +20,5 @@
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:text="@string/download_view_download" android:text="@string/download_view_download"
android:theme="@style/Widget.AppCompat.Button.Colored" /> 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> </LinearLayout>
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</merge> </merge>

View File

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

View File

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

View File

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

View File

@ -141,7 +141,7 @@
<string name="about_libraries_label">Used libraries</string> <string name="about_libraries_label">Used libraries</string>
<!-- DownloadView --> <!-- 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_install">Install</string>
<string name="download_view_cancel">Cancel</string> <string name="download_view_cancel">Cancel</string>
<string name="download_view_no_url">No download URL available</string> <string name="download_view_no_url">No download URL available</string>