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