511 lines
17 KiB
Java
511 lines
17 KiB
Java
/*
|
|
* Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
package org.fdroid.fdroid;
|
|
|
|
import android.content.Context;
|
|
import android.content.pm.PackageManager.NameNotFoundException;
|
|
import android.content.res.AssetManager;
|
|
import android.content.res.XmlResourceParser;
|
|
import android.net.Uri;
|
|
import android.text.Editable;
|
|
import android.text.Html;
|
|
import android.text.TextUtils;
|
|
import android.util.DisplayMetrics;
|
|
import android.util.Log;
|
|
import com.nostra13.universalimageloader.utils.StorageUtils;
|
|
import org.fdroid.fdroid.data.Repo;
|
|
import org.xml.sax.XMLReader;
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.Closeable;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.FileReader;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStream;
|
|
import java.math.BigInteger;
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.cert.Certificate;
|
|
import java.security.cert.CertificateEncodingException;
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.Formatter;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
|
|
public final class Utils {
|
|
|
|
public static final int BUFFER_SIZE = 4096;
|
|
|
|
// The date format used for storing dates (e.g. lastupdated, added) in the
|
|
// database.
|
|
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
|
|
|
private static final String[] FRIENDLY_SIZE_FORMAT = {
|
|
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" };
|
|
|
|
public static final SimpleDateFormat LOG_DATE_FORMAT =
|
|
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
|
|
|
|
public static String getIconsDir(Context context) {
|
|
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
|
String iconsDir;
|
|
if (metrics.densityDpi >= 640) {
|
|
iconsDir = "/icons-640/";
|
|
} else if (metrics.densityDpi >= 480) {
|
|
iconsDir = "/icons-480/";
|
|
} else if (metrics.densityDpi >= 320) {
|
|
iconsDir = "/icons-320/";
|
|
} else if (metrics.densityDpi >= 240) {
|
|
iconsDir = "/icons-240/";
|
|
} else if (metrics.densityDpi >= 160) {
|
|
iconsDir = "/icons-160/";
|
|
} else {
|
|
iconsDir = "/icons-120/";
|
|
}
|
|
return iconsDir;
|
|
}
|
|
|
|
public static void copy(InputStream input, OutputStream output)
|
|
throws IOException {
|
|
copy(input, output, null, null);
|
|
}
|
|
|
|
public static void copy(InputStream input, OutputStream output,
|
|
ProgressListener progressListener,
|
|
ProgressListener.Event templateProgressEvent)
|
|
throws IOException {
|
|
byte[] buffer = new byte[BUFFER_SIZE];
|
|
int bytesRead = 0;
|
|
while (true) {
|
|
int count = input.read(buffer);
|
|
if (count == -1) {
|
|
break;
|
|
}
|
|
if (progressListener != null) {
|
|
bytesRead += count;
|
|
templateProgressEvent.progress = bytesRead;
|
|
progressListener.onProgress(templateProgressEvent);
|
|
}
|
|
output.write(buffer, 0, count);
|
|
}
|
|
output.flush();
|
|
}
|
|
|
|
/**
|
|
* use symlinks if they are available, otherwise fall back to copying
|
|
*/
|
|
public static boolean symlinkOrCopyFile(File inFile, File outFile) {
|
|
if (new File("/system/bin/ln").exists()) {
|
|
return symlink(inFile, outFile);
|
|
} else {
|
|
return copy(inFile, outFile);
|
|
}
|
|
}
|
|
|
|
public static boolean symlink(File inFile, File outFile) {
|
|
int exitCode = -1;
|
|
try {
|
|
Process sh = Runtime.getRuntime().exec("sh");
|
|
OutputStream out = sh.getOutputStream();
|
|
String command = "/system/bin/ln -s " + inFile + " " + outFile
|
|
+ "\nexit\n";
|
|
out.write(command.getBytes("ASCII"));
|
|
|
|
final char buf[] = new char[40];
|
|
InputStreamReader reader = new InputStreamReader(sh.getInputStream());
|
|
while (reader.read(buf) != -1)
|
|
throw new IOException("stdout: " + new String(buf));
|
|
reader = new InputStreamReader(sh.getErrorStream());
|
|
while (reader.read(buf) != -1)
|
|
throw new IOException("stderr: " + new String(buf));
|
|
|
|
exitCode = sh.waitFor();
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
return false;
|
|
} catch (InterruptedException e) {
|
|
e.printStackTrace();
|
|
return false;
|
|
}
|
|
return exitCode == 0;
|
|
}
|
|
|
|
public static boolean copy(File inFile, File outFile) {
|
|
InputStream input = null;
|
|
OutputStream output = null;
|
|
try {
|
|
input = new FileInputStream(inFile);
|
|
output = new FileOutputStream(outFile);
|
|
Utils.copy(input, output);
|
|
output.close();
|
|
input.close();
|
|
return true;
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static void closeQuietly(Closeable closeable) {
|
|
if (closeable == null) {
|
|
return;
|
|
}
|
|
try {
|
|
closeable.close();
|
|
} catch (IOException ioe) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
public static String getFriendlySize(int size) {
|
|
double s = size;
|
|
int i = 0;
|
|
while (i < FRIENDLY_SIZE_FORMAT.length - 1 && s >= 1024) {
|
|
s = (100 * s / 1024) / 100.0;
|
|
i++;
|
|
}
|
|
return String.format(FRIENDLY_SIZE_FORMAT[i], s);
|
|
}
|
|
|
|
private static final String[] androidVersionNames = {
|
|
"?", // 0, undefined
|
|
"1.0", // 1
|
|
"1.1", // 2
|
|
"1.5", // 3
|
|
"1.6", // 4
|
|
"2.0", // 5
|
|
"2.0.1", // 6
|
|
"2.1", // 7
|
|
"2.2", // 8
|
|
"2.3", // 9
|
|
"2.3.3", // 10
|
|
"3.0", // 11
|
|
"3.1", // 12
|
|
"3.2", // 13
|
|
"4.0", // 14
|
|
"4.0.3", // 15
|
|
"4.1", // 16
|
|
"4.2", // 17
|
|
"4.3", // 18
|
|
"4.4", // 19
|
|
"4.4W", // 20
|
|
"5.0" // 21
|
|
};
|
|
|
|
public static String getAndroidVersionName(int sdkLevel) {
|
|
if (sdkLevel < 0) {
|
|
return androidVersionNames[0];
|
|
}
|
|
if (sdkLevel >= androidVersionNames.length) {
|
|
return String.format("v%d", sdkLevel);
|
|
}
|
|
return androidVersionNames[sdkLevel];
|
|
}
|
|
|
|
/* PackageManager doesn't give us minSdkVersion, so we have to parse it */
|
|
public static int getMinSdkVersion(Context context, String packageName) {
|
|
try {
|
|
AssetManager am = context.createPackageContext(packageName, 0).getAssets();
|
|
XmlResourceParser xml = am.openXmlResourceParser("AndroidManifest.xml");
|
|
int eventType = xml.getEventType();
|
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
|
if (eventType == XmlPullParser.START_TAG) {
|
|
if (xml.getName().equals("uses-sdk")) {
|
|
for (int j = 0; j < xml.getAttributeCount(); j++) {
|
|
if (xml.getAttributeName(j).equals("minSdkVersion")) {
|
|
return Integer.parseInt(xml.getAttributeValue(j));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
eventType = xml.nextToken();
|
|
}
|
|
} catch (NameNotFoundException e) {
|
|
e.printStackTrace();
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
} catch (XmlPullParserException e) {
|
|
e.printStackTrace();
|
|
}
|
|
return 8; // some kind of hopeful default
|
|
}
|
|
|
|
public static int countSubstringOccurrence(File file, String substring) throws IOException {
|
|
int count = 0;
|
|
FileReader input = null;
|
|
try {
|
|
int currentSubstringIndex = 0;
|
|
char[] buffer = new char[4096];
|
|
|
|
input = new FileReader(file);
|
|
int numRead = input.read(buffer);
|
|
while(numRead != -1) {
|
|
|
|
for (char c : buffer) {
|
|
if (c == substring.charAt(currentSubstringIndex)) {
|
|
currentSubstringIndex ++;
|
|
if (currentSubstringIndex == substring.length()) {
|
|
count ++;
|
|
currentSubstringIndex = 0;
|
|
}
|
|
} else {
|
|
currentSubstringIndex = 0;
|
|
}
|
|
}
|
|
numRead = input.read(buffer);
|
|
}
|
|
} finally {
|
|
closeQuietly(input);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// return a fingerprint formatted for display
|
|
public static String formatFingerprint(String fingerprint) {
|
|
if (TextUtils.isEmpty(fingerprint)
|
|
|| fingerprint.length() != 64 // SHA-256 is 64 hex chars
|
|
|| fingerprint.matches(".*[^0-9a-fA-F].*")) // its a hex string
|
|
return "BAD FINGERPRINT";
|
|
String displayFP = fingerprint.substring(0, 2);
|
|
for (int i = 2; i < fingerprint.length(); i = i + 2)
|
|
displayFP += " " + fingerprint.substring(i, i + 2);
|
|
return displayFP;
|
|
}
|
|
|
|
public static Uri getSharingUri(Context context, Repo repo) {
|
|
if (TextUtils.isEmpty(repo.address))
|
|
return Uri.parse("http://wifi-not-enabled");
|
|
Uri uri = Uri.parse(repo.address.replaceFirst("http", "fdroidrepo"));
|
|
Uri.Builder b = uri.buildUpon();
|
|
b.appendQueryParameter("swap", "1");
|
|
if (!TextUtils.isEmpty(repo.fingerprint))
|
|
b.appendQueryParameter("fingerprint", repo.fingerprint);
|
|
if (!TextUtils.isEmpty(FDroidApp.bssid)) {
|
|
b.appendQueryParameter("bssid", Uri.encode(FDroidApp.bssid));
|
|
if (!TextUtils.isEmpty(FDroidApp.ssid))
|
|
b.appendQueryParameter("ssid", Uri.encode(FDroidApp.ssid));
|
|
}
|
|
return b.build();
|
|
}
|
|
|
|
public static File getApkCacheDir(Context context) {
|
|
File apkCacheDir = new File(
|
|
StorageUtils.getCacheDirectory(context, true), "apks");
|
|
if (!apkCacheDir.exists()) {
|
|
apkCacheDir.mkdir();
|
|
}
|
|
return apkCacheDir;
|
|
}
|
|
|
|
public static String calcFingerprint(String keyHexString) {
|
|
if (TextUtils.isEmpty(keyHexString)
|
|
|| keyHexString.matches(".*[^a-fA-F0-9].*")) {
|
|
Log.e("FDroid", "Signing key certificate was blank or contained a non-hex-digit!");
|
|
return null;
|
|
} else
|
|
return calcFingerprint(Hasher.unhex(keyHexString));
|
|
}
|
|
|
|
public static String calcFingerprint(Certificate cert) {
|
|
try {
|
|
return calcFingerprint(cert.getEncoded());
|
|
} catch (CertificateEncodingException e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static String calcFingerprint(byte[] key) {
|
|
String ret = null;
|
|
if (key.length < 256) {
|
|
Log.e("FDroid", "key was shorter than 256 bytes (" + key.length + "), cannot be valid!");
|
|
return null;
|
|
}
|
|
try {
|
|
// keytool -list -v gives you the SHA-256 fingerprint
|
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
digest.update(key);
|
|
byte[] fingerprint = digest.digest();
|
|
Formatter formatter = new Formatter(new StringBuilder());
|
|
for (int i = 0; i < fingerprint.length; i++) {
|
|
formatter.format("%02X", fingerprint[i]);
|
|
}
|
|
ret = formatter.toString();
|
|
formatter.close();
|
|
} catch (Exception e) {
|
|
Log.w("FDroid", "Unable to get certificate fingerprint.\n"
|
|
+ Log.getStackTraceString(e));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
public static class CommaSeparatedList implements Iterable<String> {
|
|
private String value;
|
|
|
|
private CommaSeparatedList(String list) {
|
|
value = list;
|
|
}
|
|
|
|
public static CommaSeparatedList make(List<String> list) {
|
|
if (list == null || list.size() == 0)
|
|
return null;
|
|
else {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int i = 0; i < list.size(); i++) {
|
|
if (i > 0) {
|
|
sb.append(',');
|
|
}
|
|
sb.append(list.get(i));
|
|
}
|
|
return new CommaSeparatedList(sb.toString());
|
|
}
|
|
}
|
|
|
|
public static CommaSeparatedList make(String list) {
|
|
if (list == null || list.length() == 0)
|
|
return null;
|
|
else
|
|
return new CommaSeparatedList(list);
|
|
}
|
|
|
|
public static String str(CommaSeparatedList instance) {
|
|
return (instance == null ? null : instance.toString());
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return value;
|
|
}
|
|
|
|
public String toPrettyString() {
|
|
return value.replaceAll(",", ", ");
|
|
}
|
|
|
|
@Override
|
|
public Iterator<String> iterator() {
|
|
TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(',');
|
|
splitter.setString(value);
|
|
return splitter.iterator();
|
|
}
|
|
|
|
public boolean contains(String v) {
|
|
for (String s : this) {
|
|
if (s.equals(v))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// this is all new stuff being added
|
|
public static String hashBytes(byte[] input, String algo) {
|
|
try {
|
|
MessageDigest md = MessageDigest.getInstance(algo);
|
|
byte[] hashBytes = md.digest(input);
|
|
String hash = toHexString(hashBytes);
|
|
|
|
md.reset();
|
|
return hash;
|
|
} catch (NoSuchAlgorithmException e) {
|
|
Log.e("FDroid", "Device does not support " + algo + " MessageDisgest algorithm");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static String getBinaryHash(File apk, String algo) {
|
|
FileInputStream fis = null;
|
|
BufferedInputStream bis = null;
|
|
try {
|
|
MessageDigest md = MessageDigest.getInstance(algo);
|
|
fis = new FileInputStream(apk);
|
|
bis = new BufferedInputStream(fis);
|
|
|
|
byte[] dataBytes = new byte[524288];
|
|
int nread = 0;
|
|
|
|
while ((nread = bis.read(dataBytes)) != -1)
|
|
md.update(dataBytes, 0, nread);
|
|
|
|
byte[] mdbytes = md.digest();
|
|
return toHexString(mdbytes);
|
|
} catch (IOException e) {
|
|
Log.e("FDroid", "Error reading \"" + apk.getAbsolutePath()
|
|
+ "\" to compute " + algo + " hash.");
|
|
return null;
|
|
} catch (NoSuchAlgorithmException e) {
|
|
Log.e("FDroid", "Device does not support " + algo + " MessageDisgest algorithm");
|
|
return null;
|
|
} finally {
|
|
closeQuietly(fis);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computes the base 16 representation of the byte array argument.
|
|
*
|
|
* @param bytes an array of bytes.
|
|
* @return the bytes represented as a string of hexadecimal digits.
|
|
*/
|
|
public static String toHexString(byte[] bytes) {
|
|
BigInteger bi = new BigInteger(1, bytes);
|
|
return String.format("%0" + (bytes.length << 1) + "X", bi);
|
|
}
|
|
|
|
|
|
// Need this to add the unimplemented support for ordered and unordered
|
|
// lists to Html.fromHtml().
|
|
public static class HtmlTagHandler implements Html.TagHandler {
|
|
int listNum;
|
|
|
|
@Override
|
|
public void handleTag(boolean opening, String tag, Editable output,
|
|
XMLReader reader) {
|
|
if (tag.equals("ul")) {
|
|
if (opening)
|
|
listNum = -1;
|
|
else
|
|
output.append('\n');
|
|
} else if (opening && tag.equals("ol")) {
|
|
if (opening)
|
|
listNum = 1;
|
|
else
|
|
output.append('\n');
|
|
} else if (tag.equals("li")) {
|
|
if (opening) {
|
|
if (listNum == -1) {
|
|
output.append("\t• ");
|
|
} else {
|
|
output.append("\t").append(Integer.toString(listNum)).append(". ");
|
|
listNum++;
|
|
}
|
|
} else {
|
|
output.append('\n');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|