Skip to content

Commit

Permalink
[BatchOps] Add option to export app list as CSV and JSON
Browse files Browse the repository at this point in the history
Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
  • Loading branch information
MuntashirAkon committed Jul 11, 2024
1 parent f4f1512 commit 941d4ba
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,78 +15,66 @@
import androidx.annotation.NonNull;
import androidx.core.content.pm.PackageInfoCompat;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

import io.github.muntashirakon.AppManager.compat.PackageManagerCompat;
import io.github.muntashirakon.AppManager.utils.DateUtils;
import io.github.muntashirakon.AppManager.utils.ExUtils;
import io.github.muntashirakon.AppManager.utils.PackageUtils;
import io.github.muntashirakon.AppManager.utils.UIUtils;
import io.github.muntashirakon.csv.CsvWriter;

public final class ListExporter {
public static final int EXPORT_TYPE_XML = 1;
public static final int EXPORT_TYPE_MARKDOWN = 2;
public static final int EXPORT_TYPE_CSV = 0;
public static final int EXPORT_TYPE_JSON = 1;
public static final int EXPORT_TYPE_XML = 2;
public static final int EXPORT_TYPE_MARKDOWN = 3;

@IntDef({EXPORT_TYPE_XML, EXPORT_TYPE_MARKDOWN})
@IntDef({EXPORT_TYPE_CSV, EXPORT_TYPE_JSON, EXPORT_TYPE_XML, EXPORT_TYPE_MARKDOWN})
@Retention(RetentionPolicy.SOURCE)
public @interface ExportType {
}

@NonNull
public static String export(@NonNull Context context, @ExportType int exportType,
@NonNull List<PackageInfo> packageInfoList) throws IOException {
List<AppListItem> appListItems = new ArrayList<>(packageInfoList.size());
PackageManager pm = context.getPackageManager();
for (PackageInfo packageInfo : packageInfoList) {
ApplicationInfo applicationInfo = packageInfo.applicationInfo;
AppListItem item = new AppListItem(packageInfo.packageName);
item.setIcon(UIUtils.getBitmapFromDrawable(applicationInfo.loadIcon(pm)));
item.setPackageLabel(applicationInfo.loadLabel(pm).toString());
item.setVersionCode(PackageInfoCompat.getLongVersionCode(packageInfo));
item.setVersionName(packageInfo.versionName);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
item.setMinSdk(applicationInfo.minSdkVersion);
}
item.setTargetSdk(applicationInfo.targetSdkVersion);
String[] signatureSha256 = PackageUtils.getSigningCertSha256Checksum(packageInfo, false);
item.setSignatureSha256(TextUtils.join(",", signatureSha256));
item.setFirstInstallTime(packageInfo.firstInstallTime);
item.setLastUpdateTime(packageInfo.lastUpdateTime);
String installerPackageName = PackageManagerCompat.getInstallerPackageName(packageInfo.packageName,
UserHandleHidden.getUserId(applicationInfo.uid));
if (installerPackageName != null) {
item.setInstallerPackageName(installerPackageName);
String installerPackageLabel;
public static void export(@NonNull Context context,
@NonNull Writer writer,
@ExportType int exportType,
@NonNull List<PackageInfo> packageInfoList) throws IOException {
List<AppListItem> appListItems = getAppListItems(context, packageInfoList);
switch (exportType) {
case EXPORT_TYPE_CSV:
exportCsv(writer, appListItems);
return;
case EXPORT_TYPE_JSON:
try {
installerPackageLabel = pm.getApplicationInfo(installerPackageName, 0).loadLabel(pm).toString();
if (!installerPackageLabel.equals(installerPackageName)) {
item.setInstallerPackageLabel(installerPackageLabel);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
exportJson(writer, appListItems);
} catch (JSONException e) {
ExUtils.rethrowAsIOException(e);
}
}
appListItems.add(item);
}
if (exportType == EXPORT_TYPE_XML) {
return exportXml(appListItems);
} else if (exportType == EXPORT_TYPE_MARKDOWN) {
return exportMarkdown(context, appListItems);
return;
case EXPORT_TYPE_XML:
exportXml(writer, appListItems);
return;
case EXPORT_TYPE_MARKDOWN:
exportMarkdown(context, writer, appListItems);
return;
}
throw new IllegalArgumentException("Invalid export type: " + exportType);
}

@NonNull
private static String exportXml(@NonNull List<AppListItem> appListItems) throws IOException {
private static void exportXml(@NonNull Writer writer,
@NonNull List<AppListItem> appListItems) throws IOException {
XmlSerializer xmlSerializer = Xml.newSerializer();
StringWriter stringWriter = new StringWriter();
xmlSerializer.setOutput(stringWriter);
xmlSerializer.setOutput(writer);
xmlSerializer.startDocument("UTF-8", true);
xmlSerializer.docdecl("packages SYSTEM \"https://raw.githubusercontent.com/MuntashirAkon/AppManager/master/schema/packages.dtd\"");
xmlSerializer.startTag("", "packages");
Expand Down Expand Up @@ -115,36 +103,140 @@ private static String exportXml(@NonNull List<AppListItem> appListItems) throws
xmlSerializer.endTag("", "packages");
xmlSerializer.endDocument();
xmlSerializer.flush();
return stringWriter.toString();
}

@NonNull
private static String exportMarkdown(@NonNull Context context, @NonNull List<AppListItem> appListItems) {
StringBuilder sb = new StringBuilder("# Package Info\n\n");
private static void exportCsv(@NonNull Writer writer,
@NonNull List<AppListItem> appListItems) throws IOException {
CsvWriter csvWriter = new CsvWriter(writer);
// Add header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
csvWriter.addLine(new String[]{"name", "label", "versionCode", "versionName", "minSdk",
"targetSdk", "signature", "firstInstallTime", "lastUpdateTime",
"installerPackageName", "installerPackageLabel"});
} else {
csvWriter.addLine(new String[]{"name", "label", "versionCode", "versionName",
"targetSdk", "signature", "firstInstallTime", "lastUpdateTime",
"installerPackageName", "installerPackageLabel"});
}
for (AppListItem item : appListItems) {
String installerPackage = item.getInstallerPackageName() != null ? item.getInstallerPackageName() : "";
String installerLabel = item.getInstallerPackageLabel() != null ? item.getInstallerPackageLabel() : "";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
csvWriter.addLine(new String[]{item.packageName, item.getPackageLabel(),
String.valueOf(item.getVersionCode()), item.getVersionName(),
String.valueOf(item.getMinSdk()), String.valueOf(item.getTargetSdk()),
item.getSignatureSha256(), String.valueOf(item.getFirstInstallTime()),
String.valueOf(item.getLastUpdateTime()),
installerPackage, installerLabel});
} else {
csvWriter.addLine(new String[]{item.packageName, item.getPackageLabel(),
String.valueOf(item.getVersionCode()), item.getVersionName(),
String.valueOf(item.getTargetSdk()), item.getSignatureSha256(),
String.valueOf(item.getFirstInstallTime()),
String.valueOf(item.getLastUpdateTime()),
installerPackage, installerLabel});
}
}
}

private static void exportJson(@NonNull Writer writer,
@NonNull List<AppListItem> appListItems)
throws JSONException, IOException {
// Should reflect packages.dtd
JSONArray array = new JSONArray();
for (AppListItem item : appListItems) {
JSONObject object = new JSONObject();
object.put("name", item.packageName);
object.put("label", item.getPackageLabel());
object.put("versionCode", item.getVersionCode());
object.put("versionName", item.getVersionName());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
object.put("minSdk", item.getMinSdk());
}
object.put("targetSdk", item.getTargetSdk());
object.put("signature", item.getSignatureSha256());
object.put("firstInstallTime", item.getFirstInstallTime());
object.put("lastUpdateTime", item.getLastUpdateTime());
if (item.getInstallerPackageName() != null) {
object.put("installerPackageName", item.getInstallerPackageName());
if (item.getInstallerPackageLabel() != null) {
object.put("installerPackageLabel", item.getInstallerPackageLabel());
}
}
array.put(object);
}
writer.write(array.toString(4));
}

private static void exportMarkdown(@NonNull Context context, @NonNull Writer writer,
@NonNull List<AppListItem> appListItems) throws IOException {
writer.write("# Package Info\n\n");
for (AppListItem appListItem : appListItems) {
sb.append("## ").append(appListItem.getPackageLabel()).append("\n\n")
writer.append("## ").append(appListItem.getPackageLabel()).append("\n\n")
.append("**Package name:** ").append(appListItem.packageName).append("\n")
.append("**Version:** ").append(appListItem.getVersionName()).append(" (")
.append(appListItem.getVersionCode()).append(")\n");
.append(String.valueOf(appListItem.getVersionCode())).append(")\n");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
sb.append("**Min SDK:** ").append(appListItem.getMinSdk()).append(", ");
writer.append("**Min SDK:** ").append(String.valueOf(appListItem.getMinSdk()))
.append(", ");
}
sb.append("**Target SDK:** ").append(appListItem.getTargetSdk()).append("\n")
.append("**Date installed:** ").append(DateUtils.formatDateTime(context, appListItem.getFirstInstallTime()))
.append(", **Date updated:** ").append(DateUtils.formatDateTime(context, appListItem.getLastUpdateTime()))
writer.append("**Target SDK:** ").append(String.valueOf(appListItem.getTargetSdk()))
.append("\n")
.append("**Date installed:** ")
.append(DateUtils.formatDateTime(context, appListItem.getFirstInstallTime()))
.append(", **Date updated:** ")
.append(DateUtils.formatDateTime(context, appListItem.getLastUpdateTime()))
.append("\n");
if (appListItem.getInstallerPackageName() != null) {
sb.append("**Installer:** ");
writer.append("**Installer:** ");
if (appListItem.getInstallerPackageLabel() != null) {
sb.append(appListItem.getInstallerPackageLabel()).append(" (");
writer.append(appListItem.getInstallerPackageLabel()).append(" (");
}
sb.append(appListItem.getInstallerPackageName());
writer.append(appListItem.getInstallerPackageName());
if (appListItem.getInstallerPackageLabel() != null) {
sb.append(")");
writer.append(")");
}
}
writer.append("\n\n");
}
}

@NonNull
private static List<AppListItem> getAppListItems(@NonNull Context context,
@NonNull List<PackageInfo> packageInfoList) {
List<AppListItem> appListItems = new ArrayList<>(packageInfoList.size());
PackageManager pm = context.getPackageManager();
for (PackageInfo packageInfo : packageInfoList) {
ApplicationInfo applicationInfo = packageInfo.applicationInfo;
AppListItem item = new AppListItem(packageInfo.packageName);
appListItems.add(item);
item.setIcon(UIUtils.getBitmapFromDrawable(applicationInfo.loadIcon(pm)));
item.setPackageLabel(applicationInfo.loadLabel(pm).toString());
item.setVersionCode(PackageInfoCompat.getLongVersionCode(packageInfo));
item.setVersionName(packageInfo.versionName);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
item.setMinSdk(applicationInfo.minSdkVersion);
}
item.setTargetSdk(applicationInfo.targetSdkVersion);
String[] signatureSha256 = PackageUtils.getSigningCertSha256Checksum(packageInfo, false);
item.setSignatureSha256(TextUtils.join(",", signatureSha256));
item.setFirstInstallTime(packageInfo.firstInstallTime);
item.setLastUpdateTime(packageInfo.lastUpdateTime);
String installerPackageName = PackageManagerCompat.getInstallerPackageName(
packageInfo.packageName, UserHandleHidden.getUserId(applicationInfo.uid));
if (installerPackageName != null) {
item.setInstallerPackageName(installerPackageName);
String installerPackageLabel;
try {
installerPackageLabel = pm.getApplicationInfo(installerPackageName, 0)
.loadLabel(pm).toString();
if (!installerPackageLabel.equals(installerPackageName)) {
item.setInstallerPackageLabel(installerPackageLabel);
}
} catch (PackageManager.NameNotFoundException ignore) {
}
}
sb.append("\n\n");
}
return sb.toString();
return appListItems;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,26 @@ public class MainActivity extends BaseActivity implements AdvancedSearchView.OnQ
dialogFragment.show(getSupportFragmentManager(), RulesTypeSelectionDialogFragment.TAG);
});

private final ActivityResultLauncher<String> mExportAppListCsv = registerForActivityResult(
new ActivityResultContracts.CreateDocument("text/csv"),
uri -> {
if (uri == null) {
// Back button pressed.
return;
}
mProgressIndicator.show();
viewModel.saveExportedAppList(ListExporter.EXPORT_TYPE_CSV, Paths.get(uri));
});
private final ActivityResultLauncher<String> mExportAppListJson = registerForActivityResult(
new ActivityResultContracts.CreateDocument("application/json"),
uri -> {
if (uri == null) {
// Back button pressed.
return;
}
mProgressIndicator.show();
viewModel.saveExportedAppList(ListExporter.EXPORT_TYPE_JSON, Paths.get(uri));
});
private final ActivityResultLauncher<String> mExportAppListXml = registerForActivityResult(
new ActivityResultContracts.CreateDocument("text/xml"),
uri -> {
Expand All @@ -132,7 +152,6 @@ public class MainActivity extends BaseActivity implements AdvancedSearchView.OnQ
mProgressIndicator.show();
viewModel.saveExportedAppList(ListExporter.EXPORT_TYPE_XML, Paths.get(uri));
});

private final ActivityResultLauncher<String> mExportAppListMarkdown = registerForActivityResult(
new ActivityResultContracts.CreateDocument("text/markdown"),
uri -> {
Expand Down Expand Up @@ -407,24 +426,30 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
final String fileName = "app_manager_rules_export-" + DateUtils.formatDateTime(this, System.currentTimeMillis()) + ".am.tsv";
mBatchExportRules.launch(fileName);
} else if (id == R.id.action_export_app_list) {
List<Integer> exportTypes = Arrays.asList(ListExporter.EXPORT_TYPE_XML, ListExporter.EXPORT_TYPE_MARKDOWN);
List<Integer> exportTypes = Arrays.asList(ListExporter.EXPORT_TYPE_CSV,
ListExporter.EXPORT_TYPE_JSON,
ListExporter.EXPORT_TYPE_XML,
ListExporter.EXPORT_TYPE_MARKDOWN);
new SearchableSingleChoiceDialogBuilder<>(this, exportTypes, R.array.export_app_list_options)
.setTitle(R.string.export_app_list_select_format)
.setOnSingleChoiceClickListener((dialog, which, item1, isChecked) -> {
if (!isChecked) {
return;
}
String filename = "app_manager_app_list-" + DateUtils.formatLongDateTime(this, System.currentTimeMillis()) + ".am";
switch (item1) {
case ListExporter.EXPORT_TYPE_XML: {
final String fileName = "app_manager_app_list-" + DateUtils.formatDateTime(this, System.currentTimeMillis()) + ".am.xml";
mExportAppListXml.launch(fileName);
case ListExporter.EXPORT_TYPE_CSV:
mExportAppListCsv.launch(filename + ".csv");
break;
case ListExporter.EXPORT_TYPE_JSON:
mExportAppListJson.launch(filename + ".json");
break;
case ListExporter.EXPORT_TYPE_XML:
mExportAppListXml.launch(filename + ".xml");
break;
}
case ListExporter.EXPORT_TYPE_MARKDOWN: {
final String fileName = "app_manager_app_list-" + DateUtils.formatDateTime(this, System.currentTimeMillis()) + ".am.md";
mExportAppListMarkdown.launch(fileName);
case ListExporter.EXPORT_TYPE_MARKDOWN:
mExportAppListMarkdown.launch(filename + ".md");
break;
}
}
})
.setNegativeButton(R.string.close, null)
Expand Down
Loading

0 comments on commit 941d4ba

Please sign in to comment.