/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.telephony.uicc; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.graphics.Bitmap; import android.graphics.Color; import android.telephony.Rlog; import com.android.internal.telephony.GsmAlphabet; import java.io.UnsupportedEncodingException; /** * Various methods, useful for dealing with SIM data. */ public class IccUtils { static final String LOG_TAG="IccUtils"; // A table mapping from a number to a hex character for fast encoding hex strings. private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; /** * Many fields in GSM SIM's are stored as nibble-swizzled BCD * * Assumes left-justified field that may be padded right with 0xf * values. * * Stops on invalid BCD value, returning string so far */ public static String bcdToString(byte[] data, int offset, int length) { StringBuilder ret = new StringBuilder(length*2); for (int i = offset ; i < offset + length ; i++) { int v; v = data[i] & 0xf; if (v > 9) break; ret.append((char)('0' + v)); v = (data[i] >> 4) & 0xf; // Some PLMNs have 'f' as high nibble, ignore it if (v == 0xf) continue; if (v > 9) break; ret.append((char)('0' + v)); } return ret.toString(); } /** * Converts a bcd byte array to String with offset 0 and byte array length. */ public static String bcdToString(byte[] data) { return bcdToString(data, 0, data.length); } /** * Converts BCD string to bytes. * * @param bcd This should have an even length. If not, an "0" will be appended to the string. */ public static byte[] bcdToBytes(String bcd) { byte[] output = new byte[(bcd.length() + 1) / 2]; bcdToBytes(bcd, output); return output; } /** * Converts BCD string to bytes and put it into the given byte array. * * @param bcd This should have an even length. If not, an "0" will be appended to the string. * @param bytes If the array size is less than needed, the rest of the BCD string isn't be * converted. If the array size is more than needed, the rest of array remains unchanged. */ public static void bcdToBytes(String bcd, byte[] bytes) { if (bcd.length() % 2 != 0) { bcd += "0"; } int size = Math.min(bytes.length * 2, bcd.length()); for (int i = 0, j = 0; i + 1 < size; i += 2, j++) { bytes[j] = (byte) (charToByte(bcd.charAt(i + 1)) << 4 | charToByte(bcd.charAt(i))); } } /** * PLMN (MCC/MNC) is encoded as per 24.008 10.5.1.3 * Returns a concatenated string of MCC+MNC, stripping * all invalid character 'f' */ public static String bcdPlmnToString(byte[] data, int offset) { if (offset + 3 > data.length) { return null; } byte[] trans = new byte[3]; trans[0] = (byte) ((data[0 + offset] << 4) | ((data[0 + offset] >> 4) & 0xF)); trans[1] = (byte) ((data[1 + offset] << 4) | (data[2 + offset] & 0xF)); trans[2] = (byte) ((data[2 + offset] & 0xF0) | ((data[1 + offset] >> 4) & 0xF)); String ret = bytesToHexString(trans); // For a valid plmn we trim all character 'f' if (ret.contains("f")) { ret = ret.replaceAll("f", ""); } return ret; } /** * Some fields (like ICC ID) in GSM SIMs are stored as nibble-swizzled BCH */ public static String bchToString(byte[] data, int offset, int length) { StringBuilder ret = new StringBuilder(length*2); for (int i = offset ; i < offset + length ; i++) { int v; v = data[i] & 0xf; ret.append(HEX_CHARS[v]); v = (data[i] >> 4) & 0xf; ret.append(HEX_CHARS[v]); } return ret.toString(); } /** * Decode cdma byte into String. */ public static String cdmaBcdToString(byte[] data, int offset, int length) { StringBuilder ret = new StringBuilder(length); int count = 0; for (int i = offset; count < length; i++) { int v; v = data[i] & 0xf; if (v > 9) v = 0; ret.append((char)('0' + v)); if (++count == length) break; v = (data[i] >> 4) & 0xf; if (v > 9) v = 0; ret.append((char)('0' + v)); ++count; } return ret.toString(); } /** * Decodes a GSM-style BCD byte, returning an int ranging from 0-99. * * In GSM land, the least significant BCD digit is stored in the most * significant nibble. * * Out-of-range digits are treated as 0 for the sake of the time stamp, * because of this: * * TS 23.040 section 9.2.3.11 * "if the MS receives a non-integer value in the SCTS, it shall * assume the digit is set to 0 but shall store the entire field * exactly as received" */ public static int gsmBcdByteToInt(byte b) { int ret = 0; // treat out-of-range BCD values as 0 if ((b & 0xf0) <= 0x90) { ret = (b >> 4) & 0xf; } if ((b & 0x0f) <= 0x09) { ret += (b & 0xf) * 10; } return ret; } /** * Decodes a CDMA style BCD byte like {@link #gsmBcdByteToInt}, but * opposite nibble format. The least significant BCD digit * is in the least significant nibble and the most significant * is in the most significant nibble. */ public static int cdmaBcdByteToInt(byte b) { int ret = 0; // treat out-of-range BCD values as 0 if ((b & 0xf0) <= 0x90) { ret = ((b >> 4) & 0xf) * 10; } if ((b & 0x0f) <= 0x09) { ret += (b & 0xf); } return ret; } /** * Decodes a string field that's formatted like the EF[ADN] alpha * identifier * * From TS 51.011 10.5.1: * Coding: * this alpha tagging shall use either * - the SMS default 7 bit coded alphabet as defined in * TS 23.038 [12] with bit 8 set to 0. The alpha identifier * shall be left justified. Unused bytes shall be set to 'FF'; or * - one of the UCS2 coded options as defined in annex B. * * Annex B from TS 11.11 V8.13.0: * 1) If the first octet in the alpha string is '80', then the * remaining octets are 16 bit UCS2 characters ... * 2) if the first octet in the alpha string is '81', then the * second octet contains a value indicating the number of * characters in the string, and the third octet contains an * 8 bit number which defines bits 15 to 8 of a 16 bit * base pointer, where bit 16 is set to zero and bits 7 to 1 * are also set to zero. These sixteen bits constitute a * base pointer to a "half page" in the UCS2 code space, to be * used with some or all of the remaining octets in the string. * The fourth and subsequent octets contain codings as follows: * If bit 8 of the octet is set to zero, the remaining 7 bits * of the octet contain a GSM Default Alphabet character, * whereas if bit 8 of the octet is set to one, then the * remaining seven bits are an offset value added to the * 16 bit base pointer defined earlier... * 3) If the first octet of the alpha string is set to '82', then * the second octet contains a value indicating the number of * characters in the string, and the third and fourth octets * contain a 16 bit number which defines the complete 16 bit * base pointer to a "half page" in the UCS2 code space... */ public static String adnStringFieldToString(byte[] data, int offset, int length) { if (length == 0) { return ""; } if (length >= 1) { if (data[offset] == (byte) 0x80) { int ucslen = (length - 1) / 2; String ret = null; try { ret = new String(data, offset + 1, ucslen * 2, "utf-16be"); } catch (UnsupportedEncodingException ex) { Rlog.e(LOG_TAG, "implausible UnsupportedEncodingException", ex); } if (ret != null) { // trim off trailing FFFF characters ucslen = ret.length(); while (ucslen > 0 && ret.charAt(ucslen - 1) == '\uFFFF') ucslen--; return ret.substring(0, ucslen); } } } boolean isucs2 = false; char base = '\0'; int len = 0; if (length >= 3 && data[offset] == (byte) 0x81) { len = data[offset + 1] & 0xFF; if (len > length - 3) len = length - 3; base = (char) ((data[offset + 2] & 0xFF) << 7); offset += 3; isucs2 = true; } else if (length >= 4 && data[offset] == (byte) 0x82) { len = data[offset + 1] & 0xFF; if (len > length - 4) len = length - 4; base = (char) (((data[offset + 2] & 0xFF) << 8) | (data[offset + 3] & 0xFF)); offset += 4; isucs2 = true; } if (isucs2) { StringBuilder ret = new StringBuilder(); while (len > 0) { // UCS2 subset case if (data[offset] < 0) { ret.append((char) (base + (data[offset] & 0x7F))); offset++; len--; } // GSM character set case int count = 0; while (count < len && data[offset + count] >= 0) count++; ret.append(GsmAlphabet.gsm8BitUnpackedToString(data, offset, count)); offset += count; len -= count; } return ret.toString(); } Resources resource = Resources.getSystem(); String defaultCharset = ""; try { defaultCharset = resource.getString(com.android.internal.R.string.gsm_alphabet_default_charset); } catch (NotFoundException e) { // Ignore Exception and defaultCharset is set to a empty string. } return GsmAlphabet.gsm8BitUnpackedToString(data, offset, length, defaultCharset.trim()); } public static int hexCharToInt(char c) { if (c >= '0' && c <= '9') return (c - '0'); if (c >= 'A' && c <= 'F') return (c - 'A' + 10); if (c >= 'a' && c <= 'f') return (c - 'a' + 10); throw new RuntimeException ("invalid hex char '" + c + "'"); } /** * Converts a hex String to a byte array. * * @param s A string of hexadecimal characters, must be an even number of * chars long * * @return byte array representation * * @throws RuntimeException on invalid format */ public static byte[] hexStringToBytes(String s) { byte[] ret; if (s == null) return null; int sz = s.length(); ret = new byte[sz/2]; for (int i=0 ; i > 4); ret.append(HEX_CHARS[b]); b = 0x0f & bytes[i]; ret.append(HEX_CHARS[b]); } return ret.toString(); } /** * Convert a TS 24.008 Section 10.5.3.5a Network Name field to a string * "offset" points to "octet 3", the coding scheme byte * empty string returned on decode error */ public static String networkNameToString(byte[] data, int offset, int length) { String ret; if ((data[offset] & 0x80) != 0x80 || length < 1) { return ""; } switch ((data[offset] >>> 4) & 0x7) { case 0: // SMS character set int countSeptets; int unusedBits = data[offset] & 7; countSeptets = (((length - 1) * 8) - unusedBits) / 7 ; ret = GsmAlphabet.gsm7BitPackedToString(data, offset + 1, countSeptets); break; case 1: // UCS2 try { ret = new String(data, offset + 1, length - 1, "utf-16"); } catch (UnsupportedEncodingException ex) { ret = ""; Rlog.e(LOG_TAG,"implausible UnsupportedEncodingException", ex); } break; // unsupported encoding default: ret = ""; break; } // "Add CI" // "The MS should add the letters for the Country's Initials and // a separator (e.g. a space) to the text string" if ((data[offset] & 0x40) != 0) { // FIXME(mkf) add country initials here } return ret; } /** * Convert a TS 131.102 image instance of code scheme '11' into Bitmap * @param data The raw data * @param length The length of image body * @return The bitmap */ public static Bitmap parseToBnW(byte[] data, int length){ int valueIndex = 0; int width = data[valueIndex++] & 0xFF; int height = data[valueIndex++] & 0xFF; int numOfPixels = width*height; int[] pixels = new int[numOfPixels]; int pixelIndex = 0; int bitIndex = 7; byte currentByte = 0x00; while (pixelIndex < numOfPixels) { // reassign data and index for every byte (8 bits). if (pixelIndex % 8 == 0) { currentByte = data[valueIndex++]; bitIndex = 7; } pixels[pixelIndex++] = bitToRGB((currentByte >> bitIndex-- ) & 0x01); } if (pixelIndex != numOfPixels) { Rlog.e(LOG_TAG, "parse end and size error"); } return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888); } private static int bitToRGB(int bit){ if(bit == 1){ return Color.WHITE; } else { return Color.BLACK; } } /** * a TS 131.102 image instance of code scheme '11' into color Bitmap * * @param data The raw data * @param length the length of image body * @param transparency with or without transparency * @return The color bitmap */ public static Bitmap parseToRGB(byte[] data, int length, boolean transparency) { int valueIndex = 0; int width = data[valueIndex++] & 0xFF; int height = data[valueIndex++] & 0xFF; int bits = data[valueIndex++] & 0xFF; int colorNumber = data[valueIndex++] & 0xFF; int clutOffset = ((data[valueIndex++] & 0xFF) << 8) | (data[valueIndex++] & 0xFF); int[] colorIndexArray = getCLUT(data, clutOffset, colorNumber); if (true == transparency) { colorIndexArray[colorNumber - 1] = Color.TRANSPARENT; } int[] resultArray = null; if (0 == (8 % bits)) { resultArray = mapTo2OrderBitColor(data, valueIndex, (width * height), colorIndexArray, bits); } else { resultArray = mapToNon2OrderBitColor(data, valueIndex, (width * height), colorIndexArray, bits); } return Bitmap.createBitmap(resultArray, width, height, Bitmap.Config.RGB_565); } private static int[] mapTo2OrderBitColor(byte[] data, int valueIndex, int length, int[] colorArray, int bits) { if (0 != (8 % bits)) { Rlog.e(LOG_TAG, "not event number of color"); return mapToNon2OrderBitColor(data, valueIndex, length, colorArray, bits); } int mask = 0x01; switch (bits) { case 1: mask = 0x01; break; case 2: mask = 0x03; break; case 4: mask = 0x0F; break; case 8: mask = 0xFF; break; } int[] resultArray = new int[length]; int resultIndex = 0; int run = 8 / bits; while (resultIndex < length) { byte tempByte = data[valueIndex++]; for (int runIndex = 0; runIndex < run; ++runIndex) { int offset = run - runIndex - 1; resultArray[resultIndex++] = colorArray[(tempByte >> (offset * bits)) & mask]; } } return resultArray; } private static int[] mapToNon2OrderBitColor(byte[] data, int valueIndex, int length, int[] colorArray, int bits) { if (0 == (8 % bits)) { Rlog.e(LOG_TAG, "not odd number of color"); return mapTo2OrderBitColor(data, valueIndex, length, colorArray, bits); } int[] resultArray = new int[length]; // TODO fix me: return resultArray; } private static int[] getCLUT(byte[] rawData, int offset, int number) { if (null == rawData) { return null; } int[] result = new int[number]; int endIndex = offset + (number * 3); // 1 color use 3 bytes int valueIndex = offset; int colorIndex = 0; int alpha = 0xff << 24; do { result[colorIndex++] = alpha | ((rawData[valueIndex++] & 0xFF) << 16) | ((rawData[valueIndex++] & 0xFF) << 8) | ((rawData[valueIndex++] & 0xFF)); } while (valueIndex < endIndex); return result; } public static String getDecimalSubstring(String iccId) { int position; for (position = 0; position < iccId.length(); position ++) { if (!Character.isDigit(iccId.charAt(position))) break; } return iccId.substring( 0, position ); } /** * Converts a series of bytes to an integer. This method currently only supports positive 32-bit * integers. * * @param src The source bytes. * @param offset The position of the first byte of the data to be converted. The data is base * 256 with the most significant digit first. * @param length The length of the data to be converted. It must be <= 4. * @throws IllegalArgumentException If {@code length} is bigger than 4 or {@code src} cannot be * parsed as a positive integer. * @throws IndexOutOfBoundsException If the range defined by {@code offset} and {@code length} * exceeds the bounds of {@code src}. */ public static int bytesToInt(byte[] src, int offset, int length) { if (length > 4) { throw new IllegalArgumentException( "length must be <= 4 (only 32-bit integer supported): " + length); } if (offset < 0 || length < 0 || offset + length > src.length) { throw new IndexOutOfBoundsException( "Out of the bounds: src=[" + src.length + "], offset=" + offset + ", length=" + length); } int result = 0; for (int i = 0; i < length; i++) { result = (result << 8) | (src[offset + i] & 0xFF); } if (result < 0) { throw new IllegalArgumentException( "src cannot be parsed as a positive integer: " + result); } return result; } /** * Converts a series of bytes to a raw long variable which can be both positive and negative. * This method currently only supports 64-bit long variable. * * @param src The source bytes. * @param offset The position of the first byte of the data to be converted. The data is base * 256 with the most significant digit first. * @param length The length of the data to be converted. It must be <= 8. * @throws IllegalArgumentException If {@code length} is bigger than 8. * @throws IndexOutOfBoundsException If the range defined by {@code offset} and {@code length} * exceeds the bounds of {@code src}. */ public static long bytesToRawLong(byte[] src, int offset, int length) { if (length > 8) { throw new IllegalArgumentException( "length must be <= 8 (only 64-bit long supported): " + length); } if (offset < 0 || length < 0 || offset + length > src.length) { throw new IndexOutOfBoundsException( "Out of the bounds: src=[" + src.length + "], offset=" + offset + ", length=" + length); } long result = 0; for (int i = 0; i < length; i++) { result = (result << 8) | (src[offset + i] & 0xFF); } return result; } /** * Converts an integer to a new byte array with base 256 and the most significant digit first. * * @throws IllegalArgumentException If {@code value} is negative. */ public static byte[] unsignedIntToBytes(int value) { if (value < 0) { throw new IllegalArgumentException("value must be 0 or positive: " + value); } byte[] bytes = new byte[byteNumForUnsignedInt(value)]; unsignedIntToBytes(value, bytes, 0); return bytes; } /** * Converts an integer to a new byte array with base 256 and the most significant digit first. * The first byte's highest bit is used for sign. If the most significant digit is larger than * 127, an extra byte (0) will be prepended before it. This method currently doesn't support * negative values. * * @throws IllegalArgumentException If {@code value} is negative. */ public static byte[] signedIntToBytes(int value) { if (value < 0) { throw new IllegalArgumentException("value must be 0 or positive: " + value); } byte[] bytes = new byte[byteNumForSignedInt(value)]; signedIntToBytes(value, bytes, 0); return bytes; } /** * Converts an integer to a series of bytes with base 256 and the most significant digit first. * * @param value The integer to be converted. * @param dest The destination byte array. * @param offset The start offset of the byte array. * @return The number of byte needeed. * @throws IllegalArgumentException If {@code value} is negative. * @throws IndexOutOfBoundsException If {@code offset} exceeds the bounds of {@code dest}. */ public static int unsignedIntToBytes(int value, byte[] dest, int offset) { return intToBytes(value, dest, offset, false); } /** * Converts an integer to a series of bytes with base 256 and the most significant digit first. * The first byte's highest bit is used for sign. If the most significant digit is larger than * 127, an extra byte (0) will be prepended before it. This method currently doesn't support * negative values. * * @throws IllegalArgumentException If {@code value} is negative. * @throws IndexOutOfBoundsException If {@code offset} exceeds the bounds of {@code dest}. */ public static int signedIntToBytes(int value, byte[] dest, int offset) { return intToBytes(value, dest, offset, true); } /** * Calculates the number of required bytes to represent {@code value}. The bytes will be base * 256 with the most significant digit first. * * @throws IllegalArgumentException If {@code value} is negative. */ public static int byteNumForUnsignedInt(int value) { return byteNumForInt(value, false); } /** * Calculates the number of required bytes to represent {@code value}. The bytes will be base * 256 with the most significant digit first. If the most significant digit is larger than 127, * an extra byte (0) will be prepended before it. This method currently only supports positive * integers. * * @throws IllegalArgumentException If {@code value} is negative. */ public static int byteNumForSignedInt(int value) { return byteNumForInt(value, true); } private static int intToBytes(int value, byte[] dest, int offset, boolean signed) { int l = byteNumForInt(value, signed); if (offset < 0 || offset + l > dest.length) { throw new IndexOutOfBoundsException("Not enough space to write. Required bytes: " + l); } for (int i = l - 1, v = value; i >= 0; i--, v >>>= 8) { byte b = (byte) (v & 0xFF); dest[offset + i] = b; } return l; } private static int byteNumForInt(int value, boolean signed) { if (value < 0) { throw new IllegalArgumentException("value must be 0 or positive: " + value); } if (signed) { if (value <= 0x7F) { return 1; } if (value <= 0x7FFF) { return 2; } if (value <= 0x7FFFFF) { return 3; } } else { if (value <= 0xFF) { return 1; } if (value <= 0xFFFF) { return 2; } if (value <= 0xFFFFFF) { return 3; } } return 4; } /** * Counts the number of trailing zero bits of a byte. */ public static byte countTrailingZeros(byte b) { if (b == 0) { return 8; } int v = b & 0xFF; byte c = 7; if ((v & 0x0F) != 0) { c -= 4; } if ((v & 0x33) != 0) { c -= 2; } if ((v & 0x55) != 0) { c -= 1; } return c; } /** * Converts a byte to a hex string. */ public static String byteToHex(byte b) { return new String(new char[] {HEX_CHARS[(b & 0xFF) >>> 4], HEX_CHARS[b & 0xF]}); } /** * Strip all the trailing 'F' characters of a string, e.g., an ICCID. */ public static String stripTrailingFs(String s) { return s == null ? null : s.replaceAll("(?i)f*$", ""); } /** * Converts a character of [0-9a-aA-F] to its hex value in a byte. If the character is not a * hex number, 0 will be returned. */ private static byte charToByte(char c) { if (c >= 0x30 && c <= 0x39) { return (byte) (c - 0x30); } else if (c >= 0x41 && c <= 0x46) { return (byte) (c - 0x37); } else if (c >= 0x61 && c <= 0x66) { return (byte) (c - 0x57); } return 0; } }