diff options
-rw-r--r-- | media/java/android/media/ExifInterface.java | 398 | ||||
-rw-r--r-- | media/java/android/media/MediaScanner.java | 240 |
2 files changed, 522 insertions, 116 deletions
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java new file mode 100644 index 0000000..645f3f6 --- /dev/null +++ b/media/java/android/media/ExifInterface.java @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2007 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 android.media; + +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; + +/** + * Wrapper for native Exif library + * {@hide} + */ +public class ExifInterface { + private static final String TAG = "ExifInterface"; + private String mFilename; + + // Constants used for the Orientation Exif tag. + public static final int ORIENTATION_UNDEFINED = 0; + public static final int ORIENTATION_NORMAL = 1; + + // Constants used for white balance + public static final int WHITEBALANCE_AUTO = 0; + public static final int WHITEBALANCE_MANUAL = 1; + + // left right reversed mirror + public static final int ORIENTATION_FLIP_HORIZONTAL = 2; + public static final int ORIENTATION_ROTATE_180 = 3; + + // upside down mirror + public static final int ORIENTATION_FLIP_VERTICAL = 4; + + // flipped about top-left <--> bottom-right axis + public static final int ORIENTATION_TRANSPOSE = 5; + + // rotate 90 cw to right it + public static final int ORIENTATION_ROTATE_90 = 6; + + // flipped about top-right <--> bottom-left axis + public static final int ORIENTATION_TRANSVERSE = 7; + + // rotate 270 to right it + public static final int ORIENTATION_ROTATE_270 = 8; + + // The Exif tag names + public static final String TAG_ORIENTATION = "Orientation"; + + public static final String TAG_DATE_TIME_ORIGINAL = "DateTimeOriginal"; + public static final String TAG_MAKE = "Make"; + public static final String TAG_MODEL = "Model"; + public static final String TAG_FLASH = "Flash"; + public static final String TAG_IMAGE_WIDTH = "ImageWidth"; + public static final String TAG_IMAGE_LENGTH = "ImageLength"; + + public static final String TAG_GPS_LATITUDE = "GPSLatitude"; + public static final String TAG_GPS_LONGITUDE = "GPSLongitude"; + + public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef"; + public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef"; + public static final String TAG_WHITE_BALANCE = "WhiteBalance"; + + private boolean mSavedAttributes = false; + private boolean mHasThumbnail = false; + private HashMap<String, String> mCachedAttributes = null; + + static { + System.loadLibrary("exif"); + } + + private static ExifInterface sExifObj = null; + /** + * Since the underlying jhead native code is not thread-safe, + * ExifInterface should use singleton interface instead of public + * constructor. + */ + private static synchronized ExifInterface instance() { + if (sExifObj == null) { + sExifObj = new ExifInterface(); + } + + return sExifObj; + } + + /** + * The following 3 static methods are handy routines for atomic operation + * of underlying jhead library. It retrieves EXIF data and then release + * ExifInterface immediately. + */ + public static synchronized HashMap<String, String> loadExifData(String filename) { + ExifInterface exif = instance(); + HashMap<String, String> exifData = null; + if (exif != null) { + exif.setFilename(filename); + exifData = exif.getAttributes(); + } + return exifData; + } + + public static synchronized void saveExifData(String filename, HashMap<String, String> exifData) { + ExifInterface exif = instance(); + if (exif != null) { + exif.setFilename(filename); + exif.saveAttributes(exifData); + } + } + + public static synchronized byte[] getExifThumbnail(String filename) { + ExifInterface exif = instance(); + if (exif != null) { + exif.setFilename(filename); + return exif.getThumbnail(); + } + return null; + } + + public void setFilename(String filename) { + mFilename = filename; + } + + /** + * Given a HashMap of Exif tags and associated values, an Exif section in + * the JPG file is created and loaded with the tag data. saveAttributes() + * is expensive because it involves copying all the JPG data from one file + * to another and deleting the old file and renaming the other. It's best + * to collect all the attributes to write and make a single call rather + * than multiple calls for each attribute. You must call "commitChanges()" + * at some point to commit the changes. + */ + public void saveAttributes(HashMap<String, String> attributes) { + // format of string passed to native C code: + // "attrCnt attr1=valueLen value1attr2=value2Len value2..." + // example: + // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO" + StringBuilder sb = new StringBuilder(); + int size = attributes.size(); + if (attributes.containsKey("hasThumbnail")) { + --size; + } + sb.append(size + " "); + for (Map.Entry<String, String> iter : attributes.entrySet()) { + String key = iter.getKey(); + if (key.equals("hasThumbnail")) { + // this is a fake attribute not saved as an exif tag + continue; + } + String val = iter.getValue(); + sb.append(key + "="); + sb.append(val.length() + " "); + sb.append(val); + } + String s = sb.toString(); + saveAttributesNative(mFilename, s); + commitChangesNative(mFilename); + mSavedAttributes = true; + } + + /** + * Returns a HashMap loaded with the Exif attributes of the file. The key + * is the standard tag name and the value is the tag's value: e.g. + * Model -> Nikon. Numeric values are returned as strings. + */ + public HashMap<String, String> getAttributes() { + if (mCachedAttributes != null) { + return mCachedAttributes; + } + // format of string passed from native C code: + // "attrCnt attr1=valueLen value1attr2=value2Len value2..." + // example: + // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO" + mCachedAttributes = new HashMap<String, String>(); + + String attrStr = getAttributesNative(mFilename); + + // get count + int ptr = attrStr.indexOf(' '); + int count = Integer.parseInt(attrStr.substring(0, ptr)); + // skip past the space between item count and the rest of the attributes + ++ptr; + + for (int i = 0; i < count; i++) { + // extract the attribute name + int equalPos = attrStr.indexOf('=', ptr); + String attrName = attrStr.substring(ptr, equalPos); + ptr = equalPos + 1; // skip past = + + // extract the attribute value length + int lenPos = attrStr.indexOf(' ', ptr); + int attrLen = Integer.parseInt(attrStr.substring(ptr, lenPos)); + ptr = lenPos + 1; // skip pas the space + + // extract the attribute value + String attrValue = attrStr.substring(ptr, ptr + attrLen); + ptr += attrLen; + + if (attrName.equals("hasThumbnail")) { + mHasThumbnail = attrValue.equalsIgnoreCase("true"); + } else { + mCachedAttributes.put(attrName, attrValue); + } + } + return mCachedAttributes; + } + + /** + * Given a numerical white balance value, return a + * human-readable string describing it. + */ + public static String whiteBalanceToString(int whitebalance) { + switch (whitebalance) { + case WHITEBALANCE_AUTO: + return "Auto"; + case WHITEBALANCE_MANUAL: + return "Manual"; + default: + return ""; + } + } + + /** + * Given a numerical orientation, return a human-readable string describing + * the orientation. + */ + public static String orientationToString(int orientation) { + // TODO: this function needs to be localized and use string resource ids + // rather than strings + String orientationString; + switch (orientation) { + case ORIENTATION_NORMAL: + orientationString = "Normal"; + break; + case ORIENTATION_FLIP_HORIZONTAL: + orientationString = "Flipped horizontal"; + break; + case ORIENTATION_ROTATE_180: + orientationString = "Rotated 180 degrees"; + break; + case ORIENTATION_FLIP_VERTICAL: + orientationString = "Upside down mirror"; + break; + case ORIENTATION_TRANSPOSE: + orientationString = "Transposed"; + break; + case ORIENTATION_ROTATE_90: + orientationString = "Rotated 90 degrees"; + break; + case ORIENTATION_TRANSVERSE: + orientationString = "Transversed"; + break; + case ORIENTATION_ROTATE_270: + orientationString = "Rotated 270 degrees"; + break; + default: + orientationString = "Undefined"; + break; + } + return orientationString; + } + + /** + * Copies the thumbnail data out of the filename and puts it in the Exif + * data associated with the file used to create this object. You must call + * "commitChanges()" at some point to commit the changes. + */ + public boolean appendThumbnail(String thumbnailFileName) { + if (!mSavedAttributes) { + throw new RuntimeException("Must call saveAttributes " + + "before calling appendThumbnail"); + } + mHasThumbnail = appendThumbnailNative(mFilename, thumbnailFileName); + return mHasThumbnail; + } + + public boolean hasThumbnail() { + if (!mSavedAttributes) { + getAttributes(); + } + return mHasThumbnail; + } + + public byte[] getThumbnail() { + return getThumbnailNative(mFilename); + } + + public static float[] getLatLng(HashMap<String, String> exifData) { + if (exifData == null) { + return null; + } + + String latValue = exifData.get(ExifInterface.TAG_GPS_LATITUDE); + String latRef = exifData.get(ExifInterface.TAG_GPS_LATITUDE_REF); + String lngValue = exifData.get(ExifInterface.TAG_GPS_LONGITUDE); + String lngRef = exifData.get(ExifInterface.TAG_GPS_LONGITUDE_REF); + float[] latlng = null; + + if (latValue != null && latRef != null + && lngValue != null && lngRef != null) { + latlng = new float[2]; + latlng[0] = ExifInterface.convertRationalLatLonToFloat( + latValue, latRef); + latlng[1] = ExifInterface.convertRationalLatLonToFloat( + lngValue, lngRef); + } + + return latlng; + } + + public static float convertRationalLatLonToFloat( + String rationalString, String ref) { + try { + String [] parts = rationalString.split(","); + + String [] pair; + pair = parts[0].split("/"); + int degrees = (int) (Float.parseFloat(pair[0].trim()) + / Float.parseFloat(pair[1].trim())); + + pair = parts[1].split("/"); + int minutes = (int) ((Float.parseFloat(pair[0].trim()) + / Float.parseFloat(pair[1].trim()))); + + pair = parts[2].split("/"); + float seconds = Float.parseFloat(pair[0].trim()) + / Float.parseFloat(pair[1].trim()); + + float result = degrees + (minutes / 60F) + (seconds / (60F * 60F)); + if ((ref.equals("S") || ref.equals("W"))) { + return -result; + } + return result; + } catch (RuntimeException ex) { + // if for whatever reason we can't parse the lat long then return + // null + return 0f; + } + } + + public static String convertRationalLatLonToDecimalString( + String rationalString, String ref, boolean usePositiveNegative) { + float result = convertRationalLatLonToFloat(rationalString, ref); + + String preliminaryResult = String.valueOf(result); + if (usePositiveNegative) { + String neg = (ref.equals("S") || ref.equals("E")) ? "-" : ""; + return neg + preliminaryResult; + } else { + return preliminaryResult + String.valueOf((char) 186) + " " + + ref; + } + } + + public static String makeLatLongString(double d) { + d = Math.abs(d); + + int degrees = (int) d; + + double remainder = d - degrees; + int minutes = (int) (remainder * 60D); + // really seconds * 1000 + int seconds = (int) (((remainder * 60D) - minutes) * 60D * 1000D); + + String retVal = degrees + "/1," + minutes + "/1," + seconds + "/1000"; + return retVal; + } + + public static String makeLatStringRef(double lat) { + return lat >= 0D ? "N" : "S"; + } + + public static String makeLonStringRef(double lon) { + return lon >= 0D ? "W" : "E"; + } + + private native boolean appendThumbnailNative(String fileName, + String thumbnailFileName); + + private native void saveAttributesNative(String fileName, + String compressedAttributes); + + private native String getAttributesNative(String fileName); + + private native void commitChangesNative(String fileName); + + private native byte[] getThumbnailNative(String fileName); +} diff --git a/media/java/android/media/MediaScanner.java b/media/java/android/media/MediaScanner.java index cccc0fc..6de7bc1 100644 --- a/media/java/android/media/MediaScanner.java +++ b/media/java/android/media/MediaScanner.java @@ -54,7 +54,7 @@ import java.util.Iterator; /** * Internal service helper that no-one should use directly. - * + * * The way the scan currently works is: * - The Java MediaScannerService creates a MediaScanner (this class), and calls * MediaScanner.scanDirectories on it. @@ -96,7 +96,7 @@ import java.util.Iterator; * {@hide} */ public class MediaScanner -{ +{ static { System.loadLibrary("media_jni"); } @@ -108,17 +108,17 @@ public class MediaScanner Audio.Media.DATA, // 1 Audio.Media.DATE_MODIFIED, // 2 }; - + private static final int ID_AUDIO_COLUMN_INDEX = 0; private static final int PATH_AUDIO_COLUMN_INDEX = 1; private static final int DATE_MODIFIED_AUDIO_COLUMN_INDEX = 2; - + private static final String[] VIDEO_PROJECTION = new String[] { Video.Media._ID, // 0 Video.Media.DATA, // 1 Video.Media.DATE_MODIFIED, // 2 }; - + private static final int ID_VIDEO_COLUMN_INDEX = 0; private static final int PATH_VIDEO_COLUMN_INDEX = 1; private static final int DATE_MODIFIED_VIDEO_COLUMN_INDEX = 2; @@ -128,11 +128,11 @@ public class MediaScanner Images.Media.DATA, // 1 Images.Media.DATE_MODIFIED, // 2 }; - + private static final int ID_IMAGES_COLUMN_INDEX = 0; private static final int PATH_IMAGES_COLUMN_INDEX = 1; private static final int DATE_MODIFIED_IMAGES_COLUMN_INDEX = 2; - + private static final String[] PLAYLISTS_PROJECTION = new String[] { Audio.Playlists._ID, // 0 Audio.Playlists.DATA, // 1 @@ -157,7 +157,7 @@ public class MediaScanner private static final String ALARMS_DIR = "/alarms/"; private static final String MUSIC_DIR = "/music/"; private static final String PODCAST_DIR = "/podcasts/"; - + private static final String[] ID3_GENRES = { // ID3v1 Genres "Blues", @@ -317,11 +317,11 @@ public class MediaScanner * to get the full system property. */ private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config."; - + // set to true if file path comparisons should be case insensitive. // this should be set when scanning files on a case insensitive file system. private boolean mCaseInsensitivePaths; - + private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options(); private static class FileCacheEntry { @@ -331,7 +331,7 @@ public class MediaScanner long mLastModified; boolean mSeenInFileSystem; boolean mLastModifiedChanged; - + FileCacheEntry(Uri tableUri, long rowId, String path, long lastModified) { mTableUri = tableUri; mRowId = rowId; @@ -346,10 +346,10 @@ public class MediaScanner return mPath; } } - - // hashes file path to FileCacheEntry. + + // hashes file path to FileCacheEntry. // path should be lower case if mCaseInsensitivePaths is true - private HashMap<String, FileCacheEntry> mFileCache; + private HashMap<String, FileCacheEntry> mFileCache; private ArrayList<FileCacheEntry> mPlayLists; private HashMap<String, Uri> mGenreCache; @@ -360,7 +360,7 @@ public class MediaScanner mContext = c; mBitmapOptions.inSampleSize = 1; mBitmapOptions.inJustDecodeBounds = true; - + setDefaultRingtoneFileNames(); } @@ -370,11 +370,11 @@ public class MediaScanner mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX + Settings.System.NOTIFICATION_SOUND); } - + private MyMediaScannerClient mClient = new MyMediaScannerClient(); - + private class MyMediaScannerClient implements MediaScannerClient { - + private String mArtist; private String mAlbumArtist; // use this if mArtist is missing private String mAlbum; @@ -389,11 +389,11 @@ public class MediaScanner private String mPath; private long mLastModified; private long mFileSize; - + public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) { - + // special case certain file names - // I use regionMatches() instead of substring() below + // I use regionMatches() instead of substring() below // to avoid memory allocation int lastSlash = path.lastIndexOf('/'); if (lastSlash >= 0 && lastSlash + 2 < path.length()) { @@ -401,7 +401,7 @@ public class MediaScanner if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { return null; } - + // ignore album art files created by Windows Media Player: // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg and AlbumArt_{...}_Small.jpg if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { @@ -416,7 +416,7 @@ public class MediaScanner } } } - + mMimeType = null; // try mimeType first, if it is specified if (mimeType != null) { @@ -435,7 +435,7 @@ public class MediaScanner mMimeType = mediaFileType.mimeType; } } - + String key = path; if (mCaseInsensitivePaths) { key = path.toLowerCase(); @@ -446,20 +446,20 @@ public class MediaScanner mFileCache.put(key, entry); } entry.mSeenInFileSystem = true; - + // add some slack to avoid a rounding error long delta = lastModified - entry.mLastModified; if (delta > 1 || delta < -1) { entry.mLastModified = lastModified; entry.mLastModifiedChanged = true; } - + if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) { mPlayLists.add(entry); // we don't process playlists in the main scan, so return null return null; } - + // clear all the metadata mArtist = null; mAlbumArtist = null; @@ -472,10 +472,10 @@ public class MediaScanner mDuration = 0; mPath = path; mLastModified = lastModified; - + return entry; } - + public void scanFile(String path, long lastModified, long fileSize) { doScanFile(path, null, lastModified, fileSize, false); } @@ -513,7 +513,7 @@ public class MediaScanner } else if (MediaFile.isImageFileType(mFileType)) { // we used to compute the width and height but it's not worth it } - + result = endFile(entry, ringtones, notifications, alarms, music, podcasts); } } catch (RemoteException e) { @@ -531,17 +531,17 @@ public class MediaScanner char ch = s.charAt(start++); // return defaultValue if we have no integer at all if (ch < '0' || ch > '9') return defaultValue; - + int result = ch - '0'; while (start < length) { ch = s.charAt(start++); if (ch < '0' || ch > '9') return result; result = result * 10 + (ch - '0'); } - + return result; - } - + } + public void handleStringTag(String name, String value) { if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { // Don't trim() here, to preserve the special \001 character @@ -577,7 +577,7 @@ public class MediaScanner // track number might be of the form "2/12" // we just read the number before the slash int num = parseSubstring(value, 0, 0); - mTrack = (mTrack / 1000) * 1000 + num; + mTrack = (mTrack / 1000) * 1000 + num; } else if (name.equalsIgnoreCase("discnumber") || name.equals("set") || name.startsWith("set;")) { // set number might be of the form "1/3" @@ -588,16 +588,16 @@ public class MediaScanner mDuration = parseSubstring(value, 0, 0); } } - + public void setMimeType(String mimeType) { mMimeType = mimeType; mFileType = MediaFile.getFileTypeForMimeType(mimeType); } - + /** * Formats the data into a values array suitable for use with the Media * Content Provider. - * + * * @return a map of values */ private ContentValues toValues() { @@ -608,7 +608,7 @@ public class MediaScanner map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); map.put(MediaStore.MediaColumns.SIZE, mFileSize); map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); - + if (MediaFile.isVideoFileType(mFileType)) { map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); @@ -629,9 +629,9 @@ public class MediaScanner } return map; } - + private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications, - boolean alarms, boolean music, boolean podcasts) + boolean alarms, boolean music, boolean podcasts) throws RemoteException { // update database Uri tableUri; @@ -649,7 +649,7 @@ public class MediaScanner return null; } entry.mTableUri = tableUri; - + // use album artist if artist is missing if (mArtist == null || mArtist.length() == 0) { mArtist = mAlbumArtist; @@ -680,10 +680,18 @@ public class MediaScanner values.put(Audio.Media.IS_ALARM, alarms); values.put(Audio.Media.IS_MUSIC, music); values.put(Audio.Media.IS_PODCAST, podcasts); - } else if (isImage) { - // nothing right now + } else if (mFileType == MediaFile.FILE_TYPE_JPEG) { + HashMap<String, String> exifData = + ExifInterface.loadExifData(entry.mPath); + if (exifData != null) { + float[] latlng = ExifInterface.getLatLng(exifData); + if (latlng != null) { + values.put(Images.Media.LATITUDE, latlng[0]); + values.put(Images.Media.LONGITUDE, latlng[1]); + } + } } - + Uri result = null; long rowId = entry.mRowId; if (rowId == 0) { @@ -730,15 +738,15 @@ public class MediaScanner } } } - + if (uri != null) { - // add entry to audio_genre_map + // add entry to audio_genre_map values.clear(); values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); mMediaProvider.insert(uri, values); } } - + if (notifications && !mDefaultNotificationSet) { if (TextUtils.isEmpty(mDefaultNotificationFilename) || doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { @@ -752,36 +760,36 @@ public class MediaScanner mDefaultRingtoneSet = true; } } - + return result; } - + private boolean doesPathHaveFilename(String path, String filename) { int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; int filenameLength = filename.length(); return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && pathFilenameStart + filenameLength == path.length(); } - + private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { - + String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), settingName); - + if (TextUtils.isEmpty(existingSettingValue)) { // Set the setting to the given URI Settings.System.putString(mContext.getContentResolver(), settingName, ContentUris.withAppendedId(uri, rowId).toString()); } } - + }; // end of anonymous MediaScannerClient instance - + private void prescan(String filePath) throws RemoteException { Cursor c = null; String where = null; String[] selectionArgs = null; - + if (mFileCache == null) { mFileCache = new HashMap<String, FileCacheEntry>(); } else { @@ -792,7 +800,7 @@ public class MediaScanner } else { mPlayLists.clear(); } - + // Build the list of files from the content provider try { // Read existing files from the audio table @@ -801,14 +809,14 @@ public class MediaScanner selectionArgs = new String[] { filePath }; } c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null); - + if (c != null) { try { while (c.moveToNext()) { long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX); String path = c.getString(PATH_AUDIO_COLUMN_INDEX); long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX); - + String key = path; if (mCaseInsensitivePaths) { key = path.toLowerCase(); @@ -829,14 +837,14 @@ public class MediaScanner where = null; } c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null); - + if (c != null) { try { while (c.moveToNext()) { long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX); String path = c.getString(PATH_VIDEO_COLUMN_INDEX); long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX); - + String key = path; if (mCaseInsensitivePaths) { key = path.toLowerCase(); @@ -858,7 +866,7 @@ public class MediaScanner } mOriginalCount = 0; c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null); - + if (c != null) { try { mOriginalCount = c.getCount(); @@ -866,7 +874,7 @@ public class MediaScanner long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX); String path = c.getString(PATH_IMAGES_COLUMN_INDEX); long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX); - + String key = path; if (mCaseInsensitivePaths) { key = path.toLowerCase(); @@ -879,7 +887,7 @@ public class MediaScanner c = null; } } - + if (mProcessPlaylists) { // Read existing files from the playlists table if (filePath != null) { @@ -888,16 +896,16 @@ public class MediaScanner where = null; } c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null); - + if (c != null) { try { while (c.moveToNext()) { String path = c.getString(PATH_IMAGES_COLUMN_INDEX); - + if (path != null && path.length() > 0) { long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX); long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX); - + String key = path; if (mCaseInsensitivePaths) { key = path.toLowerCase(); @@ -919,7 +927,7 @@ public class MediaScanner } } } - + private boolean inScanDirectory(String path, String[] directories) { for (int i = 0; i < directories.length; i++) { if (path.startsWith(directories[i])) { @@ -928,25 +936,25 @@ public class MediaScanner } return false; } - + private void pruneDeadThumbnailFiles() { HashSet<String> existingFiles = new HashSet<String>(); String directory = "/sdcard/DCIM/.thumbnails"; String [] files = (new File(directory)).list(); if (files == null) files = new String[0]; - + for (int i = 0; i < files.length; i++) { String fullPathString = directory + "/" + files[i]; existingFiles.add(fullPathString); } - + try { Cursor c = mMediaProvider.query( - mThumbsUri, - new String [] { "_data" }, - null, - null, + mThumbsUri, + new String [] { "_data" }, + null, + null, null); Log.v(TAG, "pruneDeadThumbnailFiles... " + c); if (c != null && c.moveToFirst()) { @@ -955,7 +963,7 @@ public class MediaScanner existingFiles.remove(fullPathString); } while (c.moveToNext()); } - + for (String fileToDelete : existingFiles) { if (Config.LOGV) Log.v(TAG, "fileToDelete is " + fileToDelete); @@ -964,7 +972,7 @@ public class MediaScanner } catch (SecurityException ex) { } } - + Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); if (c != null) { c.close(); @@ -980,10 +988,10 @@ public class MediaScanner while (iterator.hasNext()) { FileCacheEntry entry = iterator.next(); String path = entry.mPath; - + // remove database entries for files that no longer exist. boolean fileMissing = false; - + if (!entry.mSeenInFileSystem) { if (inScanDirectory(path, directories)) { // we didn't see this file in the scan directory. @@ -997,7 +1005,7 @@ public class MediaScanner } } } - + if (fileMissing) { // do not delete missing playlists, since they may have been modified by the user. // the user can delete them in the media player instead. @@ -1016,25 +1024,25 @@ public class MediaScanner } } } - + // handle playlists last, after we know what media files are on the storage. if (mProcessPlaylists) { processPlayLists(); } - + if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) pruneDeadThumbnailFiles(); - + // allow GC to clean up mGenreCache = null; mPlayLists = null; mFileCache = null; mMediaProvider = null; } - + private void initialize(String volumeName) { mMediaProvider = mContext.getContentResolver().acquireProvider("media"); - + mAudioUri = Audio.Media.getContentUri(volumeName); mVideoUri = Video.Media.getContentUri(volumeName); mImagesUri = Images.Media.getContentUri(volumeName); @@ -1051,23 +1059,23 @@ public class MediaScanner if ( Process.supportsProcesses()) { mCaseInsensitivePaths = true; } - } + } } public void scanDirectories(String[] directories, String volumeName) { try { long start = System.currentTimeMillis(); - initialize(volumeName); + initialize(volumeName); prescan(null); long prescan = System.currentTimeMillis(); - + for (int i = 0; i < directories.length; i++) { processDirectory(directories[i], MediaFile.sFileExtensions, mClient); } long scan = System.currentTimeMillis(); postscan(directories); long end = System.currentTimeMillis(); - + if (Config.LOGD) { Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); @@ -1088,9 +1096,9 @@ public class MediaScanner // this function is used to scan a single file public Uri scanSingleFile(String path, String volumeName, String mimeType) { try { - initialize(volumeName); + initialize(volumeName); prescan(path); - + File file = new File(path); // always scan the file, so we can return the content://media Uri for existing files return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true); @@ -1105,7 +1113,7 @@ public class MediaScanner int result = 0; int end1 = path1.length(); int end2 = path2.length(); - + while (end1 > 0 && end2 > 0) { int slash1 = path1.lastIndexOf('/', end1 - 1); int slash2 = path2.lastIndexOf('/', end2 - 1); @@ -1123,13 +1131,13 @@ public class MediaScanner end2 = start2 - 1; } else break; } - + return result; } - private boolean addPlayListEntry(String entry, String playListDirectory, + private boolean addPlayListEntry(String entry, String playListDirectory, Uri uri, ContentValues values, int index) { - + // watch for trailing whitespace int entryLength = entry.length(); while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; @@ -1146,36 +1154,36 @@ public class MediaScanner // if we have a relative path, combine entry with playListDirectory if (!fullPath) entry = playListDirectory + entry; - + //FIXME - should we look for "../" within the path? - + // best matching MediaFile for the play list entry FileCacheEntry bestMatch = null; - + // number of rightmost file/directory names for bestMatch - int bestMatchLength = 0; - + int bestMatchLength = 0; + Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); while (iterator.hasNext()) { FileCacheEntry cacheEntry = iterator.next(); String path = cacheEntry.mPath; - + if (path.equalsIgnoreCase(entry)) { bestMatch = cacheEntry; break; // don't bother continuing search } - + int matchLength = matchPaths(path, entry); if (matchLength > bestMatchLength) { bestMatch = cacheEntry; bestMatchLength = matchLength; } } - + if (bestMatch == null) { return false; } - + try { // OK, now we need to add this to the database values.clear(); @@ -1189,7 +1197,7 @@ public class MediaScanner return true; } - + private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { BufferedReader reader = null; try { @@ -1266,7 +1274,7 @@ public class MediaScanner public WplHandler(String playListDirectory, Uri uri) { this.playListDirectory = playListDirectory; this.uri = uri; - + RootElement root = new RootElement("smil"); Element body = root.getChild("body"); Element seq = body.getChild("seq"); @@ -1316,12 +1324,12 @@ public class MediaScanner } } } - + private void processPlayLists() throws RemoteException { Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); while (iterator.hasNext()) { FileCacheEntry entry = iterator.next(); - String path = entry.mPath; + String path = entry.mPath; // only process playlist files if they are new or have been modified since the last scan if (entry.mLastModifiedChanged) { @@ -1332,7 +1340,7 @@ public class MediaScanner long rowId = entry.mRowId; if (rowId == 0) { // Create a new playlist - + int lastDot = path.lastIndexOf('.'); String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); values.put(MediaStore.Audio.Playlists.NAME, name); @@ -1343,7 +1351,7 @@ public class MediaScanner membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); } else { uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); - + // update lastModified value of existing playlist values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); mMediaProvider.update(uri, values, null, null); @@ -1352,7 +1360,7 @@ public class MediaScanner membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); mMediaProvider.delete(membersUri, null, null); } - + String playListDirectory = path.substring(0, lastSlash + 1); MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); @@ -1363,7 +1371,7 @@ public class MediaScanner processPlsPlayList(path, playListDirectory, membersUri, values); else if (fileType == MediaFile.FILE_TYPE_WPL) processWplPlayList(path, playListDirectory, membersUri); - + Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, null, null); try { @@ -1377,18 +1385,18 @@ public class MediaScanner } } } - + private native void processDirectory(String path, String extensions, MediaScannerClient client); private native void processFile(String path, String mimeType, MediaScannerClient client); public native void setLocale(String locale); - + public native byte[] extractAlbumArt(FileDescriptor fd); private native final void native_setup(); private native final void native_finalize(); @Override - protected void finalize() { + protected void finalize() { mContext.getContentResolver().releaseProvider(mMediaProvider); - native_finalize(); + native_finalize(); } } |