1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.gallery3d.common; 18 19 import android.content.ContentResolver; 20 import android.net.Uri; 21 22 import java.io.FileInputStream; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.security.DigestInputStream; 26 import java.security.MessageDigest; 27 import java.security.NoSuchAlgorithmException; 28 import java.util.Arrays; 29 import java.util.List; 30 31 /** 32 * MD5-based digest Wrapper. 33 */ 34 public class Fingerprint { 35 // Instance of the MessageDigest using our specified digest algorithm. 36 private static final MessageDigest DIGESTER; 37 38 /** 39 * Name of the digest algorithm we use in {@link java.security.MessageDigest} 40 */ 41 private static final String DIGEST_MD5 = "md5"; 42 43 // Version 1 streamId prefix. 44 // Hard coded stream id length limit is 40-chars. Don't ask! 45 private static final String STREAM_ID_CS_PREFIX = "cs_01_"; 46 47 // 16 bytes for 128-bit fingerprint 48 private static final int FINGERPRINT_BYTE_LENGTH; 49 50 // length of prefix + 32 hex chars for 128-bit fingerprint 51 private static final int STREAM_ID_CS_01_LENGTH; 52 53 static { 54 try { 55 DIGESTER = MessageDigest.getInstance(DIGEST_MD5); 56 FINGERPRINT_BYTE_LENGTH = DIGESTER.getDigestLength(); 57 STREAM_ID_CS_01_LENGTH = STREAM_ID_CS_PREFIX.length() 58 + (FINGERPRINT_BYTE_LENGTH * 2); 59 } catch (NoSuchAlgorithmException e) { 60 // can't continue, but really shouldn't happen 61 throw new IllegalStateException(e); 62 } 63 } 64 65 // md5 digest bytes. 66 private final byte[] mMd5Digest; 67 68 /** 69 * Creates a new Fingerprint. 70 */ 71 public Fingerprint(byte[] bytes) { 72 if ((bytes == null) || (bytes.length != FINGERPRINT_BYTE_LENGTH)) { 73 throw new IllegalArgumentException(); 74 } 75 mMd5Digest = bytes; 76 } 77 78 /** 79 * Creates a Fingerprint based on the contents of a file. 80 * 81 * Note that this will close() stream after calculating the digest. 82 * @param byteCount length of original data will be stored at byteCount[0] as a side product 83 * of the fingerprint calculation 84 */ 85 public static Fingerprint fromInputStream(InputStream stream, long[] byteCount) 86 throws IOException { 87 DigestInputStream in = null; 88 long count = 0; 89 try { 90 in = new DigestInputStream(stream, DIGESTER); 91 byte[] bytes = new byte[8192]; 92 while (true) { 93 // scan through file to compute a fingerprint. 94 int n = in.read(bytes); 95 if (n < 0) break; 96 count += n; 97 } 98 } finally { 99 if (in != null) in.close(); 100 } 101 if ((byteCount != null) && (byteCount.length > 0)) byteCount[0] = count; 102 return new Fingerprint(in.getMessageDigest().digest()); 103 } 104 105 /** 106 * Decodes a string stream id to a 128-bit fingerprint. 107 */ 108 public static Fingerprint fromStreamId(String streamId) { 109 if ((streamId == null) 110 || !streamId.startsWith(STREAM_ID_CS_PREFIX) 111 || (streamId.length() != STREAM_ID_CS_01_LENGTH)) { 112 throw new IllegalArgumentException("bad streamId: " + streamId); 113 } 114 115 // decode the hex bytes of the fingerprint portion 116 byte[] bytes = new byte[FINGERPRINT_BYTE_LENGTH]; 117 int byteIdx = 0; 118 for (int idx = STREAM_ID_CS_PREFIX.length(); idx < STREAM_ID_CS_01_LENGTH; 119 idx += 2) { 120 int value = (toDigit(streamId, idx) << 4) | toDigit(streamId, idx + 1); 121 bytes[byteIdx++] = (byte) (value & 0xff); 122 } 123 return new Fingerprint(bytes); 124 } 125 126 /** 127 * Scans a list of strings for a valid streamId. 128 * 129 * @param streamIdList list of stream id's to be scanned 130 * @return valid fingerprint or null if it can't be found 131 */ 132 public static Fingerprint extractFingerprint(List<String> streamIdList) { 133 for (String streamId : streamIdList) { 134 if (streamId.startsWith(STREAM_ID_CS_PREFIX)) { 135 return fromStreamId(streamId); 136 } 137 } 138 return null; 139 } 140 141 /** 142 * Encodes a 128-bit fingerprint as a string stream id. 143 * 144 * Stream id string is limited to 40 characters, which could be digits, lower case ASCII and 145 * underscores. 146 */ 147 public String toStreamId() { 148 StringBuilder streamId = new StringBuilder(STREAM_ID_CS_PREFIX); 149 appendHexFingerprint(streamId, mMd5Digest); 150 return streamId.toString(); 151 } 152 153 public byte[] getBytes() { 154 return mMd5Digest; 155 } 156 157 @Override 158 public boolean equals(Object obj) { 159 if (this == obj) return true; 160 if (!(obj instanceof Fingerprint)) return false; 161 Fingerprint other = (Fingerprint) obj; 162 return Arrays.equals(mMd5Digest, other.mMd5Digest); 163 } 164 165 public boolean equals(byte[] md5Digest) { 166 return Arrays.equals(mMd5Digest, md5Digest); 167 } 168 169 @Override 170 public int hashCode() { 171 return Arrays.hashCode(mMd5Digest); 172 } 173 174 // Utility methods. 175 176 private static int toDigit(String streamId, int index) { 177 int digit = Character.digit(streamId.charAt(index), 16); 178 if (digit < 0) { 179 throw new IllegalArgumentException("illegal hex digit in " + streamId); 180 } 181 return digit; 182 } 183 184 private static void appendHexFingerprint(StringBuilder sb, byte[] bytes) { 185 for (int idx = 0; idx < FINGERPRINT_BYTE_LENGTH; idx++) { 186 int value = bytes[idx]; 187 sb.append(Integer.toHexString((value >> 4) & 0x0f)); 188 sb.append(Integer.toHexString(value& 0x0f)); 189 } 190 } 191 } 192