package org.fdroid.fdroid.data;
import android.annotation.TargetApi;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import org.fdroid.fdroid.RepoXMLHandler;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
import java.io.File;
import java.util.Date;
import java.util.HashSet;
public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
// Using only byte-range keeps it only 8-bits in the SQLite database
public static final int SDK_VERSION_MAX_VALUE = Byte.MAX_VALUE;
public static final int SDK_VERSION_MIN_VALUE = 0;
public String packageName;
public String versionName;
public int versionCode;
public int size; // Size in bytes - 0 means we don't know!
public long repo; // ID of the repo it comes from
public String hash; // checksum of the APK, in lowercase hex
public String hashType;
public int minSdkVersion = SDK_VERSION_MIN_VALUE; // 0 if unknown
public int targetSdkVersion = SDK_VERSION_MIN_VALUE; // 0 if unknown
public int maxSdkVersion = SDK_VERSION_MAX_VALUE; // "infinity" if not set
public String obbMainFile;
public String obbMainFileSha256;
public String obbPatchFile;
public String obbPatchFileSha256;
public Date added;
/**
* The array of the names of the permissions that this APK requests. This is the
* same data as {@link android.content.pm.PackageInfo#requestedPermissions}. Note this
* does not mean that all these permissions have been granted, only requested. For
* example, a regular app can request a system permission, but it won't be granted it.
*/
public String[] requestedPermissions;
public String[] features; // null if empty or unknown
public String[] nativecode; // null if empty or unknown
/**
* ID (md5 sum of public key) of signature. Might be null, in the
* transition to this field existing.
*/
public String sig;
/**
* True if compatible with the device.
*/
public boolean compatible;
public String apkName; // F-Droid style APK name
public SanitizedFile installedFile; // the .apk file on this device's filesystem
/**
* If not null, this is the name of the source tarball for the
* application. Null indicates that it's a developer's binary
* build - otherwise it's built from source.
*/
public String srcname;
public int repoVersion;
public String repoAddress;
public String[] incompatibleReasons;
/**
* The numeric primary key of the Metadata table, which is used to join apks.
*/
public long appId;
public Apk() {
}
/**
* If you need an {@link Apk} but it is no longer in the database any more (e.g. because the
* version you have installed is no longer in the repository metadata) then you can instantiate
* an {@link Apk} via an {@link InstalledApp} instance.
*
* Note: Many of the fields on this instance will not be known in this circumstance. Currently
* the only things that are known are:
*
* + {@link Apk#packageName}
* + {@link Apk#versionName}
* + {@link Apk#versionCode}
* + {@link Apk#hash}
* + {@link Apk#hashType}
*
* This could instead be implemented by accepting a {@link PackageInfo} and it would get much
* the same information, but it wouldn't have the hash of the package. Seeing as we've already
* done the hard work to calculate that hash and stored it in the database, we may as well use
* that.
*/
public Apk(@NonNull InstalledApp app) {
packageName = app.getPackageName();
versionName = app.getVersionName();
versionCode = app.getVersionCode();
hash = app.getHash(); // checksum of the APK, in lowercase hex
hashType = app.getHashType();
// zero for "we don't know". If we require this in the future, then we could look up the
// file on disk if required.
size = 0;
// Same as size. We could look this up if required but not needed at time of writing.
installedFile = null;
// If we are being created from an InstalledApp, it is because we couldn't load it from the
// apk table in the database, indicating it is not available in any of our repos.
repo = 0;
}
public Apk(Cursor cursor) {
checkCursorPosition(cursor);
for (int i = 0; i < cursor.getColumnCount(); i++) {
switch (cursor.getColumnName(i)) {
case Cols.APP_ID:
appId = cursor.getLong(i);
break;
case Cols.HASH:
hash = cursor.getString(i);
break;
case Cols.HASH_TYPE:
hashType = cursor.getString(i);
break;
case Cols.ADDED_DATE:
added = Utils.parseDate(cursor.getString(i), null);
break;
case Cols.FEATURES:
features = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.Package.PACKAGE_NAME:
packageName = cursor.getString(i);
break;
case Cols.IS_COMPATIBLE:
compatible = cursor.getInt(i) == 1;
break;
case Cols.MIN_SDK_VERSION:
minSdkVersion = cursor.getInt(i);
break;
case Cols.TARGET_SDK_VERSION:
targetSdkVersion = cursor.getInt(i);
break;
case Cols.MAX_SDK_VERSION:
maxSdkVersion = cursor.getInt(i);
break;
case Cols.OBB_MAIN_FILE:
obbMainFile = cursor.getString(i);
break;
case Cols.OBB_MAIN_FILE_SHA256:
obbMainFileSha256 = cursor.getString(i);
break;
case Cols.OBB_PATCH_FILE:
obbPatchFile = cursor.getString(i);
break;
case Cols.OBB_PATCH_FILE_SHA256:
obbPatchFileSha256 = cursor.getString(i);
break;
case Cols.NAME:
apkName = cursor.getString(i);
break;
case Cols.REQUESTED_PERMISSIONS:
requestedPermissions = convertToRequestedPermissions(cursor.getString(i));
break;
case Cols.NATIVE_CODE:
nativecode = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.INCOMPATIBLE_REASONS:
incompatibleReasons = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.REPO_ID:
repo = cursor.getInt(i);
break;
case Cols.SIGNATURE:
sig = cursor.getString(i);
break;
case Cols.SIZE:
size = cursor.getInt(i);
break;
case Cols.SOURCE_NAME:
srcname = cursor.getString(i);
break;
case Cols.VERSION_NAME:
versionName = cursor.getString(i);
break;
case Cols.VERSION_CODE:
versionCode = cursor.getInt(i);
break;
case Cols.Repo.VERSION:
repoVersion = cursor.getInt(i);
break;
case Cols.Repo.ADDRESS:
repoAddress = cursor.getString(i);
break;
}
}
}
private void checkRepoAddress() {
if (repoAddress == null || apkName == null) {
throw new IllegalStateException("Apk needs to have both Schema.ApkTable.Cols.REPO_ADDRESS and Schema.ApkTable.Cols.NAME set in order to calculate URL.");
}
}
public String getUrl() {
checkRepoAddress();
return repoAddress + "/" + apkName.replace(" ", "%20");
}
/**
* Get the URL to download the <i>main</i> expansion file, the primary
* expansion file for additional resources required by your application.
* The filename will always have the format:
* "main.<i>versionCode</i>.<i>packageName</i>.obb"
*
* @return a URL to download the OBB file that matches this APK
* @see #getPatchObbUrl()
* @see <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a>
*/
public String getMainObbUrl() {
if (repoAddress == null || obbMainFile == null) {
return null;
}
checkRepoAddress();
return repoAddress + "/" + obbMainFile;
}
/**
* Get the URL to download the optional <i>patch</i> expansion file, which
* is intended for small updates to the <i>main</i> expansion file.
* The filename will always have the format:
* "patch.<i>versionCode</i>.<i>packageName</i>.obb"
*
* @return a URL to download the OBB file that matches this APK
* @see #getMainObbUrl()
* @see <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a>
*/
public String getPatchObbUrl() {
if (repoAddress == null || obbPatchFile == null) {
return null;
}
checkRepoAddress();
return repoAddress + "/" + obbPatchFile;
}
/**
* Get the local {@link File} to the "main" OBB file.
*/
public File getMainObbFile() {
if (obbMainFile == null) {
return null;
}
return new File(App.getObbDir(packageName), obbMainFile);
}
/**
* Get the local {@link File} to the "patch" OBB file.
*/
public File getPatchObbFile() {
if (obbPatchFile == null) {
return null;
}
return new File(App.getObbDir(packageName), obbPatchFile);
}
@Override
public String toString() {
return toContentValues().toString();
}
public ContentValues toContentValues() {
ContentValues values = new ContentValues();
values.put(Cols.APP_ID, appId);
values.put(Cols.VERSION_NAME, versionName);
values.put(Cols.VERSION_CODE, versionCode);
values.put(Cols.REPO_ID, repo);
values.put(Cols.HASH, hash);
values.put(Cols.HASH_TYPE, hashType);
values.put(Cols.SIGNATURE, sig);
values.put(Cols.SOURCE_NAME, srcname);
values.put(Cols.SIZE, size);
values.put(Cols.NAME, apkName);
values.put(Cols.MIN_SDK_VERSION, minSdkVersion);
values.put(Cols.TARGET_SDK_VERSION, targetSdkVersion);
values.put(Cols.MAX_SDK_VERSION, maxSdkVersion);
values.put(Cols.OBB_MAIN_FILE, obbMainFile);
values.put(Cols.OBB_MAIN_FILE_SHA256, obbMainFileSha256);
values.put(Cols.OBB_PATCH_FILE, obbPatchFile);
values.put(Cols.OBB_PATCH_FILE_SHA256, obbPatchFileSha256);
values.put(Cols.ADDED_DATE, Utils.formatDate(added, ""));
values.put(Cols.REQUESTED_PERMISSIONS, Utils.serializeCommaSeparatedString(requestedPermissions));
values.put(Cols.FEATURES, Utils.serializeCommaSeparatedString(features));
values.put(Cols.NATIVE_CODE, Utils.serializeCommaSeparatedString(nativecode));
values.put(Cols.INCOMPATIBLE_REASONS, Utils.serializeCommaSeparatedString(incompatibleReasons));
values.put(Cols.IS_COMPATIBLE, compatible ? 1 : 0);
return values;
}
@Override
@TargetApi(19)
public int compareTo(Apk apk) {
if (Build.VERSION.SDK_INT < 19) {
return Integer.valueOf(versionCode).compareTo(apk.versionCode);
}
return Integer.compare(versionCode, apk.versionCode);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.packageName);
dest.writeString(this.versionName);
dest.writeInt(this.versionCode);
dest.writeInt(this.size);
dest.writeLong(this.repo);
dest.writeString(this.hash);
dest.writeString(this.hashType);
dest.writeInt(this.minSdkVersion);
dest.writeInt(this.targetSdkVersion);
dest.writeInt(this.maxSdkVersion);
dest.writeString(this.obbMainFile);
dest.writeString(this.obbMainFileSha256);
dest.writeString(this.obbPatchFile);
dest.writeString(this.obbPatchFileSha256);
dest.writeLong(this.added != null ? this.added.getTime() : -1);
dest.writeStringArray(this.requestedPermissions);
dest.writeStringArray(this.features);
dest.writeStringArray(this.nativecode);
dest.writeString(this.sig);
dest.writeByte(this.compatible ? (byte) 1 : (byte) 0);
dest.writeString(this.apkName);
dest.writeSerializable(this.installedFile);
dest.writeString(this.srcname);
dest.writeInt(this.repoVersion);
dest.writeString(this.repoAddress);
dest.writeStringArray(this.incompatibleReasons);
dest.writeLong(this.appId);
}
protected Apk(Parcel in) {
this.packageName = in.readString();
this.versionName = in.readString();
this.versionCode = in.readInt();
this.size = in.readInt();
this.repo = in.readLong();
this.hash = in.readString();
this.hashType = in.readString();
this.minSdkVersion = in.readInt();
this.targetSdkVersion = in.readInt();
this.maxSdkVersion = in.readInt();
this.obbMainFile = in.readString();
this.obbMainFileSha256 = in.readString();
this.obbPatchFile = in.readString();
this.obbPatchFileSha256 = in.readString();
long tmpAdded = in.readLong();
this.added = tmpAdded == -1 ? null : new Date(tmpAdded);
this.requestedPermissions = in.createStringArray();
this.features = in.createStringArray();
this.nativecode = in.createStringArray();
this.sig = in.readString();
this.compatible = in.readByte() != 0;
this.apkName = in.readString();
this.installedFile = (SanitizedFile) in.readSerializable();
this.srcname = in.readString();
this.repoVersion = in.readInt();
this.repoAddress = in.readString();
this.incompatibleReasons = in.createStringArray();
this.appId = in.readLong();
}
public static final Parcelable.Creator<Apk> CREATOR = new Parcelable.Creator<Apk>() {
@Override
public Apk createFromParcel(Parcel source) {
return new Apk(source);
}
@Override
public Apk[] newArray(int size) {
return new Apk[size];
}
};
private String[] convertToRequestedPermissions(String permissionsFromDb) {
String[] array = Utils.parseCommaSeparatedString(permissionsFromDb);
if (array != null) {
HashSet<String> requestedPermissionsSet = new HashSet<>();
for (String permission : array) {
requestedPermissionsSet.add(RepoXMLHandler.fdroidToAndroidPermission(permission));
}
return requestedPermissionsSet.toArray(new String[requestedPermissionsSet.size()]);
}
return null;
}
}