summaryrefslogtreecommitdiffstats
path: root/media
diff options
context:
space:
mode:
authorAndroid (Google) Code Review <android-gerrit@google.com>2009-07-08 23:04:34 -0700
committerAndroid (Google) Code Review <android-gerrit@google.com>2009-07-08 23:04:34 -0700
commitb799616d8f0f94e866767fa730f56d82543fb49b (patch)
tree74b348f81119b86c4cc0131bc940a3b08e7e84be /media
parentfe811d8bd0da15f14702968a9c7deb02db7eec9f (diff)
parent20b03ea70bda3c4fb34e53cdf25cf98c4adb193f (diff)
downloadframeworks_base-b799616d8f0f94e866767fa730f56d82543fb49b.zip
frameworks_base-b799616d8f0f94e866767fa730f56d82543fb49b.tar.gz
frameworks_base-b799616d8f0f94e866767fa730f56d82543fb49b.tar.bz2
Merge change 6208 into donut
* changes: Move ExifInterface to android.media package so we can reference it from MediaScanner. Also hide public constructor and wrap common use cases as atomic operation to avoid race condition in jhead native codes.
Diffstat (limited to 'media')
-rw-r--r--media/java/android/media/ExifInterface.java398
-rw-r--r--media/java/android/media/MediaScanner.java240
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();
}
}