hood-core/src/main/java/at/favre/lib/hood/util/PackageInfoAssembler.java
/*
* Copyright 2016 Patrick Favre-Bulle
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package at.favre.lib.hood.util;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.Signature;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import at.favre.lib.hood.Hood;
import at.favre.lib.hood.interfaces.PageEntry;
import at.favre.lib.hood.interfaces.Section;
import at.favre.lib.hood.util.defaults.DefaultProperties;
import timber.log.Timber;
/**
* Helper class to assemble packageManager packageInfo data for different configurations;
*/
public class PackageInfoAssembler {
private static final String TAG = PackageInfoAssembler.class.getSimpleName();
public enum Type {
/**
* Package name and versions
*/
APK_VERSION_INFO(null, "Versions", new PageEntryProvider() {
@Override
public List<PageEntry<?>> getEntries(@NonNull Context context, @NonNull PackageInfo packageInfo) {
return createPmApkVersionInfo(packageInfo);
}
}),
/**
* Declared services
*/
SERVICES(PackageManager.GET_SERVICES, "Services", new PageEntryProvider() {
@Override
public List<PageEntry<?>> getEntries(@NonNull Context context, @NonNull PackageInfo packageInfo) {
return createPmServiceInfo(packageInfo);
}
}),
/**
* Declared broadcast receivers
*/
RECEIVERS(PackageManager.GET_RECEIVERS, "Broadcast Receivers", new PageEntryProvider() {
@Override
public List<PageEntry<?>> getEntries(@NonNull Context context, @NonNull PackageInfo packageInfo) {
return createPmBroadcastReceiversInfo(packageInfo);
}
}),
/**
* Declared providers
*/
PROVIDER(PackageManager.GET_PROVIDERS, "Providers", new PageEntryProvider() {
@Override
public List<PageEntry<?>> getEntries(@NonNull Context context, @NonNull PackageInfo packageInfo) {
return createPmProviderInfo(packageInfo);
}
}),
/**
* Declared activities
*/
ACTIVITIES(PackageManager.GET_ACTIVITIES, "Activities", new PageEntryProvider() {
@Override
public List<PageEntry<?>> getEntries(@NonNull Context context, @NonNull PackageInfo packageInfo) {
return createPmActivitiesInfo(packageInfo);
}
}),
/**
* Apk signing signature
*/
SIGNATURE(PackageManager.GET_SIGNATURES, "Signatures", new PageEntryProvider() {
@Override
public List<PageEntry<?>> getEntries(@NonNull Context context, @NonNull PackageInfo packageInfo) {
return createPmSignatureHashInfo(packageInfo, Collections.<String, String>emptyMap());
}
}),
/**
* Declared permissions
*/
PERMISSIONS(PackageManager.GET_PERMISSIONS, "Permissions", new PageEntryProvider() {
@Override
public List<PageEntry<?>> getEntries(@NonNull Context context, @NonNull PackageInfo packageInfo) {
return createPmPermissionInfo(context, packageInfo, false);
}
}),
/**
* Declared uses-features
*/
USES_FEATURE(PackageManager.GET_CONFIGURATIONS, "System Features", new PageEntryProvider() {
@Override
public List<PageEntry<?>> getEntries(@NonNull Context context, @NonNull PackageInfo packageInfo) {
return createPmDeclaredSystemFeatureInfo(context, packageInfo);
}
}),
/**
* Install time and location
*/
APK_INSTALL_INFO(null, "Install Info", new PageEntryProvider() {
@Override
public List<PageEntry<?>> getEntries(@NonNull Context context, @NonNull PackageInfo packageInfo) {
return createApkStateInfo(packageInfo);
}
});
/**
* See PackageManager.GET_* flags
*/
private final Integer requestFlag;
/**
* Header for {@link at.favre.lib.hood.interfaces.Section.HeaderSection}
*/
private final String header;
private final PageEntryProvider pageEntryProvider;
Type(Integer requestFlag, String header, PageEntryProvider pageEntryProvider) {
this.requestFlag = requestFlag;
this.header = header;
this.pageEntryProvider = pageEntryProvider;
}
}
private final List<Type> typeList;
private final String packageName;
/**
* Same as {@link PackageInfoAssembler} constructor, but will use the current's packageName.
* Note that the order in which the type parameters are set will be honored, also it is possible
* to include a type multiple times.
*
* @param type must provide types - each type will request a certain packageManager info category (see {@link Type}
*/
public PackageInfoAssembler(@NonNull Type type, Type... types) {
this(null, type, types);
}
/**
* Creates and configures a new assembler which uses given types as request for the info to return
* in {@link #createSection(Context, boolean)}.
*
* @param packageName for which packageName (ie. app) you want the info for
* @param type must provide types - each type will request a certain packageManager info category (see {@link Type}
*/
public PackageInfoAssembler(String packageName, @NonNull Type type, Type... types) {
this.packageName = packageName;
this.typeList = new LinkedList<>();
typeList.add(type);
if (types != null) {
typeList.addAll(Arrays.asList(types));
}
}
/**
* Same as {@link #createSection(Context, boolean)} but will include all sub headers
*
* @param context must not be null, used for getting the {@link PackageManager}
* @return section containing the info
*/
public Section createSection(@Nullable Context context) {
return createSection(context, true);
}
/**
* Creates a section containing all requested types with or without sub headers for each type.
* If something went wrong (e.g. unknown package) an error message will be shown in ui
*
* @param context must not be null, used for getting the {@link PackageManager}
* @param addSectionHeaders if true includes a section header before very type
* @return section containing the info
*/
public Section createSection(@Nullable Context context, boolean addSectionHeaders) {
Section.ModifiableHeaderSection mainSection = Hood.ext().createSection("");
if (context != null) {
String targetPackageName = packageName == null ? context.getPackageName() : packageName;
try {
PackageInfo packageInfo = getPackageInfo(context, getRequiredRequestFlags(typeList));
if (packageInfo == null) {
throw new IllegalStateException("info was null");
}
for (Type type : typeList) {
try {
mainSection.add(Hood.ext().createSection(addSectionHeaders ? type.header : null, type.pageEntryProvider.getEntries(context, packageInfo)));
} catch (Exception e) {
mainSection.add(Hood.get().createMessageEntry("Could not get packageInfo for " + type + " and " + targetPackageName + ": " + e.getClass() + " (" + e.getMessage() + ")"));
Timber.w(e, "error while creating %s", type);
}
}
} catch (Exception e) {
mainSection.setErrorMessage("Could not get packageInfo for " + targetPackageName + ": " + e.getClass() + " (" + e.getMessage() + ")");
Timber.w(e, mainSection.getErrorMessage());
}
}
return mainSection.removeHeader();
}
private int getRequiredRequestFlags(List<Type> requestedTypes) {
int flags = 0;
EnumSet<Type> set = EnumSet.copyOf(requestedTypes);
for (Type requestedType : set) {
if (requestedType.requestFlag != null) {
flags |= requestedType.requestFlag;
}
}
return flags;
}
private interface PageEntryProvider {
List<PageEntry<?>> getEntries(@NonNull Context context, @NonNull PackageInfo packageInfo);
}
/* *************************************************************************** SUB-ASSEMBLERS */
public static PackageInfo getPackageInfo(Context ctx, int flags) {
try {
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), flags);
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException("could not get package info", e);
}
}
/**
* Includes info about when and where the apk was installed
*
* @param packageInfo from {@link PackageManager#getPackageInfo(String, int)}
* @return list of apk info
*/
public static List<PageEntry<?>> createPmApkVersionInfo(@NonNull PackageInfo packageInfo) {
List<PageEntry<?>> entries = new ArrayList<>();
entries.add(Hood.get().createPropertyEntry("package-name", packageInfo.packageName));
entries.add(Hood.get().createPropertyEntry("version-name", packageInfo.versionName));
entries.add(Hood.get().createPropertyEntry("version-code", String.valueOf(packageInfo.versionCode)));
return entries;
}
/**
* Lists all defined services with additional meta-data
*
* @param packageInfo from {@link PackageManager#getPackageInfo(String, int)} requiring {@link PackageManager#GET_SERVICES} flag
* @return list of services
*/
public static List<PageEntry<?>> createPmServiceInfo(@NonNull PackageInfo packageInfo) {
List<PageEntry<?>> entries = new ArrayList<>();
if (packageInfo.services != null) {
for (ServiceInfo service : packageInfo.services) {
if (service != null) {
entries.add(Hood.get().createPropertyEntry(service.name,
"exported: " + service.exported + "\n" +
"enabled: " + service.enabled + "\n" +
"flags: " + service.exported + "\n" +
"process: " + service.processName + "\n" +
"req-permission: " + service.permission + "\n", true));
}
}
}
return entries;
}
/**
* Lists all defined broadcast receivers with additional meta-data
*
* @param packageInfo from {@link PackageManager#getPackageInfo(String, int)} requiring {@link PackageManager#GET_RECEIVERS} flag
* @return entries
*/
public static List<PageEntry<?>> createPmBroadcastReceiversInfo(@NonNull PackageInfo packageInfo) {
List<PageEntry<?>> entries = new ArrayList<>();
if (packageInfo.receivers != null) {
for (ActivityInfo receiver : packageInfo.receivers) {
if (receiver != null) {
entries.add(Hood.get().createPropertyEntry(receiver.name,
"exported: " + receiver.exported + "\n" +
"enabled: " + receiver.enabled + "\n", true));
}
}
}
return entries;
}
/**
* Lists all defined providers with additional meta-data
*
* @param packageInfo from {@link PackageManager#getPackageInfo(String, int)} requiring {@link PackageManager#GET_PROVIDERS} flag
* @return entries
*/
public static List<PageEntry<?>> createPmProviderInfo(@NonNull PackageInfo packageInfo) {
List<PageEntry<?>> entries = new ArrayList<>();
if (packageInfo.providers != null) {
for (ProviderInfo provider : packageInfo.providers) {
if (provider != null) {
entries.add(Hood.get().createPropertyEntry(provider.name,
"exported: " + provider.exported + "\n" +
"enabled: " + provider.enabled + "\n" +
"authorities: " + provider.authority + "\n" +
"multi-process: " + provider.multiprocess + "\n" +
"read-perm: " + provider.readPermission + "\n" +
"write-perm: " + provider.writePermission + "\n", true));
}
}
}
return entries;
}
/**
* Lists all defined activities with additional meta-data
*
* @param packageInfo from {@link PackageManager#getPackageInfo(String, int)} requiring {@link PackageManager#GET_ACTIVITIES} flag
* @return entries
*/
public static List<PageEntry<?>> createPmActivitiesInfo(@NonNull PackageInfo packageInfo) {
List<PageEntry<?>> entries = new ArrayList<>();
if (packageInfo.activities != null) {
for (ActivityInfo receiver : packageInfo.activities) {
if (receiver != null) {
entries.add(Hood.get().createPropertyEntry(receiver.name,
"exported: " + receiver.exported + "\n" +
"enabled: " + receiver.enabled + "\n", true));
}
}
}
return entries;
}
/**
* Includes info about when and where the apk was installed
*
* @param packageInfo from {@link PackageManager#getPackageInfo(String, int)}
* @return entries
*/
public static List<PageEntry<?>> createApkStateInfo(@NonNull PackageInfo packageInfo) {
List<PageEntry<?>> entries = new ArrayList<>();
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
entries.add(Hood.get().createPropertyEntry("apk-location", TypeTranslators.translatePMInstallLocation(packageInfo.installLocation)));
}
entries.add(Hood.get().createPropertyEntry("apk-first-install", HoodUtil.toSimpleDateTimeFormat(packageInfo.firstInstallTime)));
entries.add(Hood.get().createPropertyEntry("apk-reinstall", HoodUtil.toSimpleDateTimeFormat(packageInfo.lastUpdateTime)));
return entries;
}
/**
* Creates page-entries for all the apks signatures and shows sh256 hash of it
*
* @param packageInfo from {@link PackageManager#getPackageInfo(String, int)} requiring {@link PackageManager#GET_SIGNATURES} flag
* @param refSha256Map a map of key: name of signature and value: first x digits of sha256 hash of signature; used for adding name to specific signature (i.e. "debug key")
* @return entries
*/
public static List<PageEntry<?>> createPmSignatureHashInfo(@NonNull PackageInfo packageInfo, @NonNull Map<String, String> refSha256Map) {
List<PageEntry<?>> entries = new ArrayList<>();
try {
for (Signature signature : packageInfo.signatures) {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(signature.toByteArray());
String sha256 = HoodUtil.byteToHex(md.digest());
String key = "apk-signature-sha256";
for (Map.Entry<String, String> refEntry : refSha256Map.entrySet()) {
if (sha256.toLowerCase().startsWith(refEntry.getValue().toLowerCase())) {
key += " (" + refEntry.getKey() + ")";
}
}
entries.add(Hood.get().createPropertyEntry(key, sha256, true));
}
} catch (Exception e) {
throw new IllegalStateException("could not create hash", e);
}
return entries;
}
/**
* Returns page-entry for each defined permission in the app (the passed activity belongs to).
* <p>
* See {@link DefaultProperties#createSectionRuntimePermissions(Activity, List)} for more details
*
* @param context must not be null and must be instance of activity (needed for getting the state)
* @param packageInfo from {@link PackageManager#getPackageInfo(String, int)} requiring {@link PackageManager#GET_PERMISSIONS} flag
* @param onlyDangerousPermissions only include permissions with flag PROTECTION_DANGEROUS (ie. have to be granted by the user)
* @return list of page-entries
*/
@SuppressLint("NewApi")
public static List<PageEntry<?>> createPmPermissionInfo(final @NonNull Context context, @NonNull PackageInfo packageInfo, boolean onlyDangerousPermissions) {
if (!(context instanceof Activity)) {
throw new IllegalArgumentException("context must be of type activity - needed for getting current permission state");
}
List<PageEntry<?>> entries = new ArrayList<>();
if (packageInfo.requestedPermissions != null && packageInfo.requestedPermissions.length > 0) {
List<String> permissionNames = new ArrayList<>();
for (int i = 0; i < packageInfo.requestedPermissions.length; i++) {
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1
|| !onlyDangerousPermissions
|| packageInfo.requestedPermissionsFlags[i] == PermissionInfo.PROTECTION_DANGEROUS) {
permissionNames.add(packageInfo.requestedPermissions[i]);
}
}
Collections.sort(permissionNames);
return DefaultProperties.createSectionRuntimePermissions(((Activity) context), permissionNames).removeHeader().asEntryList();
}
return entries;
}
/**
* Creates entries for all declared system features (see Manifest <uses-feature> tags)
* Uses {@link DefaultProperties#createSystemFeatureInfo(Context, Map)} call.
*
* @param context must not be null
* @param packageInfo from {@link PackageManager#getPackageInfo(String, int)} requiring {@link PackageManager#GET_CONFIGURATIONS} flag
* @return list of all declared uses-feature tags in AndroidManifest as page entries
*/
public static List<PageEntry<?>> createPmDeclaredSystemFeatureInfo(@NonNull Context context, @NonNull PackageInfo packageInfo) {
try {
Map<CharSequence, String> featureMap = new TreeMap<>();
if (packageInfo.reqFeatures != null && packageInfo.reqFeatures.length > 0) {
for (FeatureInfo reqFeature : packageInfo.reqFeatures) {
boolean required = reqFeature.flags == FeatureInfo.FLAG_REQUIRED;
String fullLabel;
String id;
if ((reqFeature.name == null || reqFeature.name.trim().isEmpty())
&& reqFeature.getGlEsVersion() != null && !reqFeature.getGlEsVersion().isEmpty()) {
fullLabel = "glEsVersion " + reqFeature.getGlEsVersion();
id = String.valueOf(reqFeature.reqGlEsVersion);
} else {
fullLabel = reqFeature.name;
id = reqFeature.name;
}
fullLabel += (required ? " (req)" : "");
featureMap.put(Hood.ext().createFullLabel(fullLabel.replace("android.hardware.", ""), fullLabel), id);
}
}
return DefaultProperties.createSystemFeatureInfo(context, featureMap);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}