fdroidclient / F-Droid / src / org / fdroid / fdroid / Utils.java @ 645f9fc5
History | View | Annotate | Download (23.2 KB)
1 |
/*
|
---|---|
2 |
* Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com
|
3 |
*
|
4 |
* This program is free software; you can redistribute it and/or
|
5 |
* modify it under the terms of the GNU General Public License
|
6 |
* as published by the Free Software Foundation; either version 3
|
7 |
* of the License, or (at your option) any later version.
|
8 |
*
|
9 |
* This program is distributed in the hope that it will be useful,
|
10 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12 |
* GNU General Public License for more details.
|
13 |
*
|
14 |
* You should have received a copy of the GNU General Public License
|
15 |
* along with this program; if not, write to the Free Software
|
16 |
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17 |
*/
|
18 |
|
19 |
package org.fdroid.fdroid; |
20 |
|
21 |
import android.content.Context; |
22 |
import android.content.pm.PackageManager; |
23 |
import android.content.res.AssetManager; |
24 |
import android.content.res.XmlResourceParser; |
25 |
import android.graphics.Bitmap; |
26 |
import android.net.Uri; |
27 |
import android.support.annotation.NonNull; |
28 |
import android.support.annotation.Nullable; |
29 |
import android.text.Editable; |
30 |
import android.text.Html; |
31 |
import android.text.TextUtils; |
32 |
import android.util.DisplayMetrics; |
33 |
import android.util.Log; |
34 |
|
35 |
import com.nostra13.universalimageloader.core.DisplayImageOptions; |
36 |
import com.nostra13.universalimageloader.core.assist.ImageScaleType; |
37 |
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; |
38 |
import com.nostra13.universalimageloader.utils.StorageUtils; |
39 |
|
40 |
import org.fdroid.fdroid.compat.FileCompat; |
41 |
import org.fdroid.fdroid.data.Apk; |
42 |
import org.fdroid.fdroid.data.Repo; |
43 |
import org.fdroid.fdroid.data.SanitizedFile; |
44 |
import org.xml.sax.XMLReader; |
45 |
import org.xmlpull.v1.XmlPullParser; |
46 |
import org.xmlpull.v1.XmlPullParserException; |
47 |
|
48 |
import java.io.BufferedInputStream; |
49 |
import java.io.Closeable; |
50 |
import java.io.File; |
51 |
import java.io.FileInputStream; |
52 |
import java.io.FileOutputStream; |
53 |
import java.io.IOException; |
54 |
import java.io.InputStream; |
55 |
import java.io.OutputStream; |
56 |
import java.math.BigInteger; |
57 |
import java.security.MessageDigest; |
58 |
import java.security.NoSuchAlgorithmException; |
59 |
import java.security.cert.Certificate; |
60 |
import java.security.cert.CertificateEncodingException; |
61 |
import java.text.ParseException; |
62 |
import java.text.SimpleDateFormat; |
63 |
import java.util.Date; |
64 |
import java.util.Formatter; |
65 |
import java.util.Iterator; |
66 |
import java.util.List; |
67 |
import java.util.Locale; |
68 |
|
69 |
public final class Utils { |
70 |
|
71 |
@SuppressWarnings("UnusedDeclaration") |
72 |
private static final String TAG = "Utils"; |
73 |
|
74 |
public static final int BUFFER_SIZE = 4096; |
75 |
|
76 |
// The date format used for storing dates (e.g. lastupdated, added) in the
|
77 |
// database.
|
78 |
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); |
79 |
|
80 |
private static final String[] FRIENDLY_SIZE_FORMAT = { |
81 |
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" }; |
82 |
|
83 |
private static final SimpleDateFormat LOG_DATE_FORMAT = |
84 |
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); |
85 |
|
86 |
public static final String FALLBACK_ICONS_DIR = "/icons/"; |
87 |
|
88 |
/*
|
89 |
* @param dpiMultiplier Lets you grab icons for densities larger or
|
90 |
* smaller than that of your device by some fraction. Useful, for example,
|
91 |
* if you want to display a 48dp image at twice the size, 96dp, in which
|
92 |
* case you'd use a dpiMultiplier of 2.0 to get an image twice as big.
|
93 |
*/
|
94 |
public static String getIconsDir(final Context context, final double dpiMultiplier) { |
95 |
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
96 |
final double dpi = metrics.densityDpi * dpiMultiplier; |
97 |
if (dpi >= 640) { |
98 |
return "/icons-640/"; |
99 |
} |
100 |
if (dpi >= 480) { |
101 |
return "/icons-480/"; |
102 |
} |
103 |
if (dpi >= 320) { |
104 |
return "/icons-320/"; |
105 |
} |
106 |
if (dpi >= 240) { |
107 |
return "/icons-240/"; |
108 |
} |
109 |
if (dpi >= 160) { |
110 |
return "/icons-160/"; |
111 |
} |
112 |
|
113 |
return "/icons-120/"; |
114 |
} |
115 |
|
116 |
public static void copy(InputStream input, OutputStream output) |
117 |
throws IOException { |
118 |
copy(input, output, null, null); |
119 |
} |
120 |
|
121 |
public static void copy(InputStream input, OutputStream output, |
122 |
ProgressListener progressListener, |
123 |
ProgressListener.Event templateProgressEvent) |
124 |
throws IOException { |
125 |
byte[] buffer = new byte[BUFFER_SIZE]; |
126 |
int bytesRead = 0; |
127 |
while (true) { |
128 |
int count = input.read(buffer);
|
129 |
if (count == -1) { |
130 |
break;
|
131 |
} |
132 |
if (progressListener != null) { |
133 |
bytesRead += count; |
134 |
templateProgressEvent.progress = bytesRead; |
135 |
progressListener.onProgress(templateProgressEvent); |
136 |
} |
137 |
output.write(buffer, 0, count);
|
138 |
} |
139 |
output.flush(); |
140 |
} |
141 |
|
142 |
/**
|
143 |
* Attempt to symlink, but if that fails, it will make a copy of the file.
|
144 |
*/
|
145 |
public static boolean symlinkOrCopyFileQuietly(SanitizedFile inFile, SanitizedFile outFile) { |
146 |
return FileCompat.symlink(inFile, outFile) || copyQuietly(inFile, outFile);
|
147 |
} |
148 |
|
149 |
/**
|
150 |
* Read the input stream until it reaches the end, ignoring any exceptions.
|
151 |
*/
|
152 |
public static void consumeStream(InputStream stream) { |
153 |
final byte[] buffer = new byte[256]; |
154 |
try {
|
155 |
int read;
|
156 |
do {
|
157 |
read = stream.read(buffer); |
158 |
} while (read != -1); |
159 |
} catch (IOException e) { |
160 |
// Ignore...
|
161 |
} |
162 |
} |
163 |
|
164 |
public static boolean copyQuietly(File inFile, File outFile) { |
165 |
InputStream input = null; |
166 |
OutputStream output = null; |
167 |
try {
|
168 |
input = new FileInputStream(inFile); |
169 |
output = new FileOutputStream(outFile); |
170 |
Utils.copy(input, output); |
171 |
return true; |
172 |
} catch (IOException e) { |
173 |
Log.e(TAG, "I/O error when copying a file", e);
|
174 |
return false; |
175 |
} finally {
|
176 |
closeQuietly(output); |
177 |
closeQuietly(input); |
178 |
} |
179 |
} |
180 |
|
181 |
public static void closeQuietly(Closeable closeable) { |
182 |
if (closeable == null) { |
183 |
return;
|
184 |
} |
185 |
try {
|
186 |
closeable.close(); |
187 |
} catch (IOException ioe) { |
188 |
// ignore
|
189 |
} |
190 |
} |
191 |
|
192 |
public static String getFriendlySize(int size) { |
193 |
double s = size;
|
194 |
int i = 0; |
195 |
while (i < FRIENDLY_SIZE_FORMAT.length - 1 && s >= 1024) { |
196 |
s = (100 * s / 1024) / 100.0; |
197 |
i++; |
198 |
} |
199 |
return String.format(FRIENDLY_SIZE_FORMAT[i], s); |
200 |
} |
201 |
|
202 |
private static final String[] androidVersionNames = { |
203 |
"?", // 0, undefined |
204 |
"1.0", // 1 |
205 |
"1.1", // 2 |
206 |
"1.5", // 3 |
207 |
"1.6", // 4 |
208 |
"2.0", // 5 |
209 |
"2.0.1", // 6 |
210 |
"2.1", // 7 |
211 |
"2.2", // 8 |
212 |
"2.3", // 9 |
213 |
"2.3.3", // 10 |
214 |
"3.0", // 11 |
215 |
"3.1", // 12 |
216 |
"3.2", // 13 |
217 |
"4.0", // 14 |
218 |
"4.0.3", // 15 |
219 |
"4.1", // 16 |
220 |
"4.2", // 17 |
221 |
"4.3", // 18 |
222 |
"4.4", // 19 |
223 |
"4.4W", // 20 |
224 |
"5.0", // 21 |
225 |
"5.1", // 22 |
226 |
"6.0" // 23 |
227 |
}; |
228 |
|
229 |
public static String getAndroidVersionName(int sdkLevel) { |
230 |
if (sdkLevel < 0) { |
231 |
return androidVersionNames[0]; |
232 |
} |
233 |
if (sdkLevel >= androidVersionNames.length) {
|
234 |
return String.format(Locale.ENGLISH, "v%d", sdkLevel); |
235 |
} |
236 |
return androidVersionNames[sdkLevel];
|
237 |
} |
238 |
|
239 |
/* PackageManager doesn't give us the min and max sdk versions, so we have
|
240 |
* to parse it */
|
241 |
private static int getMinMaxSdkVersion(Context context, String packageName, |
242 |
String attrName) {
|
243 |
try {
|
244 |
AssetManager am = context.createPackageContext(packageName, 0).getAssets();
|
245 |
XmlResourceParser xml = am.openXmlResourceParser("AndroidManifest.xml");
|
246 |
int eventType = xml.getEventType();
|
247 |
while (eventType != XmlPullParser.END_DOCUMENT) {
|
248 |
if (eventType == XmlPullParser.START_TAG && xml.getName().equals("uses-sdk")) { |
249 |
for (int j = 0; j < xml.getAttributeCount(); j++) { |
250 |
if (xml.getAttributeName(j).equals(attrName)) {
|
251 |
return Integer.parseInt(xml.getAttributeValue(j)); |
252 |
} |
253 |
} |
254 |
} |
255 |
eventType = xml.nextToken(); |
256 |
} |
257 |
} catch (PackageManager.NameNotFoundException | IOException | XmlPullParserException e) { |
258 |
Log.e(TAG, "Could not get min/max sdk version", e);
|
259 |
} |
260 |
return 0; |
261 |
} |
262 |
|
263 |
public static int getMinSdkVersion(Context context, String packageName) { |
264 |
return getMinMaxSdkVersion(context, packageName, "minSdkVersion"); |
265 |
} |
266 |
|
267 |
public static int getMaxSdkVersion(Context context, String packageName) { |
268 |
return getMinMaxSdkVersion(context, packageName, "maxSdkVersion"); |
269 |
} |
270 |
|
271 |
// return a fingerprint formatted for display
|
272 |
public static String formatFingerprint(Context context, String fingerprint) { |
273 |
if (TextUtils.isEmpty(fingerprint)
|
274 |
|| fingerprint.length() != 64 // SHA-256 is 64 hex chars |
275 |
|| fingerprint.matches(".*[^0-9a-fA-F].*")) // its a hex string |
276 |
return context.getString(R.string.bad_fingerprint);
|
277 |
String displayFP = fingerprint.substring(0, 2); |
278 |
for (int i = 2; i < fingerprint.length(); i = i + 2) |
279 |
displayFP += " " + fingerprint.substring(i, i + 2); |
280 |
return displayFP;
|
281 |
} |
282 |
|
283 |
@NonNull
|
284 |
public static Uri getLocalRepoUri(Repo repo) { |
285 |
if (TextUtils.isEmpty(repo.address))
|
286 |
return Uri.parse("http://wifi-not-enabled"); |
287 |
Uri uri = Uri.parse(repo.address); |
288 |
Uri.Builder b = uri.buildUpon(); |
289 |
if (!TextUtils.isEmpty(repo.fingerprint))
|
290 |
b.appendQueryParameter("fingerprint", repo.fingerprint);
|
291 |
String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https" : "http"; |
292 |
b.scheme(scheme); |
293 |
return b.build();
|
294 |
} |
295 |
|
296 |
public static Uri getSharingUri(Repo repo) { |
297 |
if (TextUtils.isEmpty(repo.address))
|
298 |
return Uri.parse("http://wifi-not-enabled"); |
299 |
Uri localRepoUri = getLocalRepoUri(repo); |
300 |
Uri.Builder b = localRepoUri.buildUpon(); |
301 |
b.scheme(localRepoUri.getScheme().replaceFirst("http", "fdroidrepo")); |
302 |
b.appendQueryParameter("swap", "1"); |
303 |
if (!TextUtils.isEmpty(FDroidApp.bssid)) {
|
304 |
b.appendQueryParameter("bssid", Uri.encode(FDroidApp.bssid));
|
305 |
if (!TextUtils.isEmpty(FDroidApp.ssid))
|
306 |
b.appendQueryParameter("ssid", Uri.encode(FDroidApp.ssid));
|
307 |
} |
308 |
return b.build();
|
309 |
} |
310 |
|
311 |
/**
|
312 |
* See {@link Utils#getApkDownloadDir(android.content.Context)} for why this is "unsafe".
|
313 |
*/
|
314 |
public static SanitizedFile getApkCacheDir(Context context) { |
315 |
final SanitizedFile apkCacheDir = new SanitizedFile(StorageUtils.getCacheDirectory(context, true), "apks"); |
316 |
if (!apkCacheDir.exists()) {
|
317 |
apkCacheDir.mkdir(); |
318 |
} |
319 |
return apkCacheDir;
|
320 |
} |
321 |
|
322 |
/**
|
323 |
* The directory where .apk files are downloaded (and stored - if the relevant property is enabled).
|
324 |
* This must be on internal storage, to prevent other apps with "write external storage" from being
|
325 |
* able to change the .apk file between F-Droid requesting the Package Manger to install, and the
|
326 |
* Package Manager receiving that request.
|
327 |
*/
|
328 |
public static File getApkDownloadDir(Context context) { |
329 |
final SanitizedFile apkCacheDir = new SanitizedFile(StorageUtils.getCacheDirectory(context, false), "temp"); |
330 |
if (!apkCacheDir.exists()) {
|
331 |
apkCacheDir.mkdir(); |
332 |
} |
333 |
|
334 |
// All parent directories of the .apk file need to be executable for the package installer
|
335 |
// to be able to have permission to read our world-readable .apk files.
|
336 |
FileCompat.setExecutable(apkCacheDir, true, false); |
337 |
return apkCacheDir;
|
338 |
} |
339 |
|
340 |
public static String calcFingerprint(String keyHexString) { |
341 |
if (TextUtils.isEmpty(keyHexString)
|
342 |
|| keyHexString.matches(".*[^a-fA-F0-9].*")) {
|
343 |
Log.e(TAG, "Signing key certificate was blank or contained a non-hex-digit!");
|
344 |
return null; |
345 |
} |
346 |
return calcFingerprint(Hasher.unhex(keyHexString));
|
347 |
} |
348 |
|
349 |
public static String calcFingerprint(Certificate cert) { |
350 |
if (cert == null) |
351 |
return null; |
352 |
try {
|
353 |
return calcFingerprint(cert.getEncoded());
|
354 |
} catch (CertificateEncodingException e) { |
355 |
return null; |
356 |
} |
357 |
} |
358 |
|
359 |
public static String calcFingerprint(byte[] key) { |
360 |
if (key == null) |
361 |
return null; |
362 |
String ret = null; |
363 |
if (key.length < 256) { |
364 |
Log.e(TAG, "key was shorter than 256 bytes (" + key.length + "), cannot be valid!"); |
365 |
return null; |
366 |
} |
367 |
try {
|
368 |
// keytool -list -v gives you the SHA-256 fingerprint
|
369 |
MessageDigest digest = MessageDigest.getInstance("SHA-256"); |
370 |
digest.update(key); |
371 |
byte[] fingerprint = digest.digest(); |
372 |
Formatter formatter = new Formatter(new StringBuilder()); |
373 |
for (byte aFingerprint : fingerprint) { |
374 |
formatter.format("%02X", aFingerprint);
|
375 |
} |
376 |
ret = formatter.toString(); |
377 |
formatter.close(); |
378 |
} catch (Exception e) { |
379 |
Log.w(TAG, "Unable to get certificate fingerprint", e);
|
380 |
} |
381 |
return ret;
|
382 |
} |
383 |
|
384 |
/**
|
385 |
* There is a method {@link java.util.Locale#forLanguageTag(String)} which would be useful
|
386 |
* for this, however it doesn't deal with android-specific language tags, which are a little
|
387 |
* different. For example, android language tags may have an "r" before the country code,
|
388 |
* such as "zh-rHK", however {@link java.util.Locale} expects them to be "zr-HK".
|
389 |
*/
|
390 |
public static Locale getLocaleFromAndroidLangTag(String languageTag) { |
391 |
if (TextUtils.isEmpty(languageTag)) {
|
392 |
return null; |
393 |
} |
394 |
|
395 |
final String[] parts = languageTag.split("-"); |
396 |
if (parts.length == 1) { |
397 |
return new Locale(parts[0]); |
398 |
} |
399 |
if (parts.length == 2) { |
400 |
String country = parts[1]; |
401 |
// Some languages have an "r" before the country as per the values folders, such
|
402 |
// as "zh-rCN". As far as the Locale class is concerned, the "r" is
|
403 |
// not helpful, and this should be "zh-CN". Thus, we will
|
404 |
// strip the "r" when found.
|
405 |
if (country.charAt(0) == 'r' && country.length() == 3) { |
406 |
country = country.substring(1);
|
407 |
} |
408 |
return new Locale(parts[0], country); |
409 |
} |
410 |
Log.e(TAG, "Locale could not be parsed from language tag: " + languageTag);
|
411 |
return new Locale(languageTag); |
412 |
} |
413 |
|
414 |
public static String getApkUrl(Apk apk) { |
415 |
return getApkUrl(apk.repoAddress, apk);
|
416 |
} |
417 |
|
418 |
public static String getApkUrl(String repoAddress, Apk apk) { |
419 |
return repoAddress + "/" + apk.apkName.replace(" ", "%20"); |
420 |
} |
421 |
|
422 |
public static class CommaSeparatedList implements Iterable<String> { |
423 |
private final String value; |
424 |
|
425 |
private CommaSeparatedList(String list) { |
426 |
value = list; |
427 |
} |
428 |
|
429 |
public static CommaSeparatedList make(List<String> list) { |
430 |
if (list == null || list.size() == 0) |
431 |
return null; |
432 |
StringBuilder sb = new StringBuilder(); |
433 |
for (int i = 0; i < list.size(); i++) { |
434 |
if (i > 0) { |
435 |
sb.append(',');
|
436 |
} |
437 |
sb.append(list.get(i)); |
438 |
} |
439 |
return new CommaSeparatedList(sb.toString()); |
440 |
} |
441 |
|
442 |
public static CommaSeparatedList make(String[] list) { |
443 |
if (list == null || list.length == 0) |
444 |
return null; |
445 |
StringBuilder sb = new StringBuilder(); |
446 |
for (int i = 0; i < list.length; i++) { |
447 |
if (i > 0) { |
448 |
sb.append(',');
|
449 |
} |
450 |
sb.append(list[i]); |
451 |
} |
452 |
return new CommaSeparatedList(sb.toString()); |
453 |
} |
454 |
|
455 |
@Nullable
|
456 |
public static CommaSeparatedList make(@Nullable String list) { |
457 |
if (TextUtils.isEmpty(list))
|
458 |
return null; |
459 |
return new CommaSeparatedList(list); |
460 |
} |
461 |
|
462 |
public static String str(CommaSeparatedList instance) { |
463 |
return (instance == null ? null : instance.toString()); |
464 |
} |
465 |
|
466 |
@Override
|
467 |
public String toString() { |
468 |
return value;
|
469 |
} |
470 |
|
471 |
public String toPrettyString() { |
472 |
return value.replaceAll(",", ", "); |
473 |
} |
474 |
|
475 |
@Override
|
476 |
public Iterator<String> iterator() { |
477 |
TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(','); |
478 |
splitter.setString(value); |
479 |
return splitter.iterator();
|
480 |
} |
481 |
|
482 |
public boolean contains(String v) { |
483 |
for (final String s : this) { |
484 |
if (s.equals(v))
|
485 |
return true; |
486 |
} |
487 |
return false; |
488 |
} |
489 |
} |
490 |
|
491 |
public static DisplayImageOptions.Builder getImageLoadingOptions() { |
492 |
return new DisplayImageOptions.Builder() |
493 |
.cacheInMemory(true)
|
494 |
.cacheOnDisk(true)
|
495 |
.imageScaleType(ImageScaleType.NONE) |
496 |
.showImageOnLoading(R.drawable.ic_repo_app_default) |
497 |
.showImageForEmptyUri(R.drawable.ic_repo_app_default) |
498 |
.displayer(new FadeInBitmapDisplayer(200, true, true, false)) |
499 |
.bitmapConfig(Bitmap.Config.RGB_565); |
500 |
} |
501 |
|
502 |
// this is all new stuff being added
|
503 |
public static String hashBytes(byte[] input, String algo) { |
504 |
try {
|
505 |
MessageDigest md = MessageDigest.getInstance(algo); |
506 |
byte[] hashBytes = md.digest(input); |
507 |
String hash = toHexString(hashBytes);
|
508 |
|
509 |
md.reset(); |
510 |
return hash;
|
511 |
} catch (NoSuchAlgorithmException e) { |
512 |
Log.e(TAG, "Device does not support " + algo + " MessageDisgest algorithm"); |
513 |
return null; |
514 |
} |
515 |
} |
516 |
|
517 |
public static String getBinaryHash(File apk, String algo) { |
518 |
FileInputStream fis = null; |
519 |
try {
|
520 |
MessageDigest md = MessageDigest.getInstance(algo); |
521 |
fis = new FileInputStream(apk); |
522 |
BufferedInputStream bis = new BufferedInputStream(fis); |
523 |
|
524 |
byte[] dataBytes = new byte[524288]; |
525 |
int nread;
|
526 |
while ((nread = bis.read(dataBytes)) != -1) |
527 |
md.update(dataBytes, 0, nread);
|
528 |
|
529 |
byte[] mdbytes = md.digest(); |
530 |
return toHexString(mdbytes);
|
531 |
} catch (IOException e) { |
532 |
Log.e(TAG, "Error reading \"" + apk.getAbsolutePath()
|
533 |
+ "\" to compute " + algo + " hash."); |
534 |
return null; |
535 |
} catch (NoSuchAlgorithmException e) { |
536 |
Log.e(TAG, "Device does not support " + algo + " MessageDisgest algorithm"); |
537 |
return null; |
538 |
} finally {
|
539 |
closeQuietly(fis); |
540 |
} |
541 |
} |
542 |
|
543 |
/**
|
544 |
* Computes the base 16 representation of the byte array argument.
|
545 |
*
|
546 |
* @param bytes an array of bytes.
|
547 |
* @return the bytes represented as a string of hexadecimal digits.
|
548 |
*/
|
549 |
public static String toHexString(byte[] bytes) { |
550 |
BigInteger bi = new BigInteger(1, bytes); |
551 |
return String.format("%0" + (bytes.length << 1) + "X", bi); |
552 |
} |
553 |
|
554 |
public static int parseInt(String str, int fallback) { |
555 |
if (str == null || str.length() == 0) { |
556 |
return fallback;
|
557 |
} |
558 |
int result;
|
559 |
try {
|
560 |
result = Integer.parseInt(str);
|
561 |
} catch (NumberFormatException e) { |
562 |
result = fallback; |
563 |
} |
564 |
return result;
|
565 |
} |
566 |
|
567 |
public static Date parseDate(String str, Date fallback) { |
568 |
if (str == null || str.length() == 0) { |
569 |
return fallback;
|
570 |
} |
571 |
Date result;
|
572 |
try {
|
573 |
result = DATE_FORMAT.parse(str); |
574 |
} catch (ParseException e) { |
575 |
result = fallback; |
576 |
} |
577 |
return result;
|
578 |
} |
579 |
|
580 |
public static String formatDate(Date date, String fallback) { |
581 |
if (date == null) { |
582 |
return fallback;
|
583 |
} |
584 |
return DATE_FORMAT.format(date);
|
585 |
} |
586 |
|
587 |
public static String formatLogDate(Date date) { |
588 |
if (date == null) { |
589 |
return "(unknown)"; |
590 |
} |
591 |
return LOG_DATE_FORMAT.format(date);
|
592 |
} |
593 |
|
594 |
// Need this to add the unimplemented support for ordered and unordered
|
595 |
// lists to Html.fromHtml().
|
596 |
public static class HtmlTagHandler implements Html.TagHandler { |
597 |
int listNum;
|
598 |
|
599 |
@Override
|
600 |
public void handleTag(boolean opening, String tag, Editable output, |
601 |
XMLReader reader) { |
602 |
switch (tag) {
|
603 |
case "ul": |
604 |
if (opening)
|
605 |
listNum = -1;
|
606 |
else
|
607 |
output.append('\n');
|
608 |
break;
|
609 |
case "ol": |
610 |
if (opening)
|
611 |
listNum = 1;
|
612 |
else
|
613 |
output.append('\n');
|
614 |
break;
|
615 |
case "li": |
616 |
if (opening) {
|
617 |
if (listNum == -1) { |
618 |
output.append("\t• ");
|
619 |
} else {
|
620 |
output.append("\t").append(Integer.toString(listNum)).append(". "); |
621 |
listNum++; |
622 |
} |
623 |
} else {
|
624 |
output.append('\n');
|
625 |
} |
626 |
break;
|
627 |
} |
628 |
} |
629 |
} |
630 |
|
631 |
/**
|
632 |
* Remove all files from the {@parm directory} either beginning with {@param startsWith}
|
633 |
* or ending with {@param endsWith}. Note that if the SD card is not ready, then the
|
634 |
* cache directory will probably not be available. In this situation no files will be
|
635 |
* deleted (and thus they may still exist after the SD card becomes available).
|
636 |
*/
|
637 |
public static void deleteFiles(@Nullable File directory, @Nullable String startsWith, @Nullable String endsWith) { |
638 |
|
639 |
if (directory == null) { |
640 |
return;
|
641 |
} |
642 |
|
643 |
final File[] files = directory.listFiles(); |
644 |
if (files == null) { |
645 |
return;
|
646 |
} |
647 |
|
648 |
if (startsWith != null) { |
649 |
DebugLog(TAG, "Cleaning up files in " + directory + " that start with \"" + startsWith + "\""); |
650 |
} |
651 |
|
652 |
if (endsWith != null) { |
653 |
DebugLog(TAG, "Cleaning up files in " + directory + " that end with \"" + endsWith + "\""); |
654 |
} |
655 |
|
656 |
for (File f : files) { |
657 |
if ((startsWith != null && f.getName().startsWith(startsWith)) |
658 |
|| (endsWith != null && f.getName().endsWith(endsWith))) {
|
659 |
if (!f.delete()) {
|
660 |
Log.w(TAG, "Couldn't delete cache file " + f);
|
661 |
} |
662 |
} |
663 |
} |
664 |
} |
665 |
|
666 |
public static void DebugLog(String tag, String msg) { |
667 |
if (BuildConfig.DEBUG) {
|
668 |
Log.d(tag, msg); |
669 |
} |
670 |
} |
671 |
|
672 |
public static void DebugLog(String tag, String msg, Throwable tr) { |
673 |
if (BuildConfig.DEBUG) {
|
674 |
Log.d(tag, msg, tr); |
675 |
} |
676 |
} |
677 |
|
678 |
} |