// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.download;
import android.text.TextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import java.util.UUID;
/**
* Class representing the download information stored in SharedPreferences to construct a
* download notification.
*/
public class DownloadSharedPreferenceEntry {
private static final String TAG = "DownloadEntry";
// Current version of the DownloadSharedPreferenceEntry. When changing the SharedPreference,
// we need to change the version number too.
@VisibleForTesting
static final int VERSION = 3;
public static final int ITEM_TYPE_DOWNLOAD = 1;
public static final int ITEM_TYPE_OFFLINE_PAGE = 2;
public final int notificationId;
public final boolean isOffTheRecord; // Whether the download is public (non incognito).
public boolean canDownloadWhileMetered;
public final String fileName;
public final String downloadGuid;
public final int itemType;
static final DownloadSharedPreferenceEntry INVALID_ENTRY =
new DownloadSharedPreferenceEntry(-1, false, false, null, "", ITEM_TYPE_DOWNLOAD);
DownloadSharedPreferenceEntry(int notificationId, boolean isOffTheRecord,
boolean canDownloadWhileMetered, String guid, String fileName, int itemType) {
this.notificationId = notificationId;
this.isOffTheRecord = isOffTheRecord;
this.canDownloadWhileMetered = canDownloadWhileMetered;
this.downloadGuid = guid;
this.fileName = fileName;
this.itemType = itemType;
}
/**
* Parse the pending notification from a String object in SharedPrefs.
*
* @param sharedPrefString String from SharedPreference, containing the notification ID, GUID,
* file name, whether it is resumable and whether download started on a metered network.
* @return a DownloadSharedPreferenceEntry object.
*/
static DownloadSharedPreferenceEntry parseFromString(String sharedPrefString) {
String versionString = sharedPrefString.substring(0, sharedPrefString.indexOf(","));
// Ignore all SharedPreference entries that has an invalid version for now.
int version = 0;
try {
version = Integer.parseInt(versionString);
} catch (NumberFormatException nfe) {
Log.w(TAG, "Exception while parsing pending download:" + sharedPrefString);
}
if (version <= 0 || version > 3) return INVALID_ENTRY;
// Expected number of items for version 1 and 2 is 6, version 3 is 7.
int expectedItemsNumber = (version == 3 ? 7 : 6);
String[] values = sharedPrefString.split(",", expectedItemsNumber);
if (values.length != expectedItemsNumber) return INVALID_ENTRY;
// Index == 0 is used for version, therefor we start from 1.
int currentIndex = 1;
int id = 0;
int itemType = ITEM_TYPE_DOWNLOAD;
try {
id = Integer.parseInt(values[currentIndex++]);
if (version > 2) {
itemType = Integer.parseInt(values[currentIndex++]);
}
} catch (NumberFormatException nfe) {
Log.w(TAG, "Exception while parsing pending download:" + sharedPrefString);
return INVALID_ENTRY;
}
if (itemType != ITEM_TYPE_DOWNLOAD && itemType != ITEM_TYPE_OFFLINE_PAGE) {
return INVALID_ENTRY;
}
boolean isOffTheRecord = (version >= 2) ? "1".equals(values[currentIndex])
: "0".equals(values[currentIndex]);
++currentIndex;
boolean canDownloadWhileMetered = "1".equals(values[currentIndex++]);
String guid = values[currentIndex++];
if (!isValidGUID(guid)) return INVALID_ENTRY;
String fileName = values[currentIndex++];
return new DownloadSharedPreferenceEntry(
id, isOffTheRecord, canDownloadWhileMetered, guid, fileName, itemType);
}
/**
* @return a string for the DownloadSharedPreferenceEntry instance to be inserted into
* SharedPrefs.
*/
String getSharedPreferenceString() {
return VERSION + "," + notificationId + "," + itemType + "," + (isOffTheRecord ? "1" : "0")
+ "," + (canDownloadWhileMetered ? "1" : "0") + "," + downloadGuid + "," + fileName;
}
/**
* Check if a string is a valid GUID. GUID is RFC 4122 compliant, it should have format
* xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
* TODO(qinmin): move this to base/.
* @return true if the string is a valid GUID, or false otherwise.
*/
static boolean isValidGUID(String guid) {
if (guid == null) return false;
try {
// Java UUID class doesn't check the length of the string. Need to convert it back to
// string so that we can validate the length of the original string.
UUID uuid = UUID.fromString(guid);
String uuidString = uuid.toString();
return guid.equalsIgnoreCase(uuidString);
} catch (IllegalArgumentException e) {
return false;
}
}
public boolean isOfflinePage() {
return itemType == ITEM_TYPE_OFFLINE_PAGE;
}
/**
* Build a download item from this object.
*/
DownloadItem buildDownloadItem() {
DownloadInfo info = new DownloadInfo.Builder()
.setDownloadGuid(downloadGuid)
.setFileName(fileName)
.setIsOffTheRecord(isOffTheRecord)
.build();
return new DownloadItem(false, info);
}
@Override
public boolean equals(Object object) {
if (!(object instanceof DownloadSharedPreferenceEntry)) {
return false;
}
final DownloadSharedPreferenceEntry other = (DownloadSharedPreferenceEntry) object;
return TextUtils.equals(downloadGuid, other.downloadGuid)
&& TextUtils.equals(fileName, other.fileName)
&& notificationId == other.notificationId
&& itemType == other.itemType
&& isOffTheRecord == other.isOffTheRecord
&& canDownloadWhileMetered == other.canDownloadWhileMetered;
}
@Override
public int hashCode() {
int hash = 31;
hash = 37 * hash + (isOffTheRecord ? 1 : 0);
hash = 37 * hash + (canDownloadWhileMetered ? 1 : 0);
hash = 37 * hash + notificationId;
hash = 37 * hash + itemType;
hash = 37 * hash + downloadGuid.hashCode();
hash = 37 * hash + fileName.hashCode();
return hash;
}
}