diff options
| -rw-r--r-- | core/java/android/app/SearchDialog.java | 4 | ||||
| -rw-r--r-- | core/java/android/app/SuggestionsAdapter.java | 145 | ||||
| -rw-r--r-- | core/java/android/webkit/WebSettings.java | 170 | ||||
| -rw-r--r-- | core/res/res/color/search_url_text.xml | 21 | ||||
| -rw-r--r-- | core/res/res/values/colors.xml | 4 | ||||
| -rw-r--r-- | libs/audioflinger/AudioFlinger.cpp | 13 | ||||
| -rw-r--r-- | libs/audioflinger/AudioFlinger.h | 2 | ||||
| -rw-r--r-- | libs/utils/String8.cpp | 2 | ||||
| -rw-r--r-- | media/java/android/media/ExifInterface.java | 398 | ||||
| -rw-r--r-- | media/java/android/media/MediaScanner.java | 240 | ||||
| -rw-r--r-- | media/libmedia/ToneGenerator.cpp | 2 | ||||
| -rw-r--r-- | packages/VpnServices/src/com/android/server/vpn/VpnService.java | 35 |
12 files changed, 874 insertions, 162 deletions
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 022a9d9..6d6aca4 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -991,7 +991,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS }; @Override - public void cancel() { + public void dismiss() { if (!isShowing()) return; // We made sure the IME was displayed, so also make sure it is closed @@ -1003,7 +1003,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS getWindow().getDecorView().getWindowToken(), 0); } - super.cancel(); + super.dismiss(); } /** diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java index 49c94d1..c8e952f 100644 --- a/core/java/android/app/SuggestionsAdapter.java +++ b/core/java/android/app/SuggestionsAdapter.java @@ -18,7 +18,8 @@ package android.app; import android.content.ContentResolver; import android.content.Context; -import android.content.res.Resources.NotFoundException; +import android.content.res.ColorStateList; +import android.content.res.Resources; import android.database.Cursor; import android.graphics.Canvas; import android.graphics.drawable.Drawable; @@ -300,29 +301,17 @@ class SuggestionsAdapter extends ResourceCursorAdapter { ((SuggestionItemView)view).setColor(backgroundColor); final boolean isHtml = mFormatCol > 0 && "html".equals(cursor.getString(mFormatCol)); - setViewText(cursor, views.mText1, mText1Col, isHtml); - setViewText(cursor, views.mText2, mText2Col, isHtml); - setViewIcon(cursor, views.mIcon1, mIconName1Col); - setViewIcon(cursor, views.mIcon2, mIconName2Col); - } - - private void setViewText(Cursor cursor, TextView v, int textCol, boolean isHtml) { - if (v == null) { - return; - } - CharSequence text = null; - if (textCol >= 0) { - String str = cursor.getString(textCol); - text = (str != null && isHtml) ? Html.fromHtml(str) : str; + String text1 = null; + if (mText1Col >= 0) { + text1 = cursor.getString(mText1Col); } - // Set the text even if it's null, since we need to clear any previous text. - v.setText(text); - - if (TextUtils.isEmpty(text)) { - v.setVisibility(View.GONE); - } else { - v.setVisibility(View.VISIBLE); + String text2 = null; + if (mText2Col >= 0) { + text2 = cursor.getString(mText2Col); } + ((SuggestionItemView)view).setTextStrings(text1, text2, isHtml, mProviderContext); + setViewIcon(cursor, views.mIcon1, mIconName1Col); + setViewIcon(cursor, views.mIcon2, mIconName2Col); } private void setViewIcon(Cursor cursor, ImageView v, int iconNameCol) { @@ -476,7 +465,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter { if (drawable != null) { mOutsideDrawablesCache.put(drawableId, drawable); } - } catch (NotFoundException nfe) { + } catch (Resources.NotFoundException nfe) { if (DBG) Log.d(LOG_TAG, "Icon resource not found: " + drawableId); // drawable = null; } @@ -509,8 +498,82 @@ class SuggestionsAdapter extends ResourceCursorAdapter { * draws on top of the list view selection highlight). */ private class SuggestionItemView extends ViewGroup { + /** + * Parses a given HTMl string and manages Spannable variants of the string for different + * states of the suggestion item (selected, pressed and normal). Colors for these different + * states are specified in the html font tag color attribute in the format '@<RESOURCEID>' + * where RESOURCEID is the ID of a ColorStateList or Color resource. + */ + private class MultiStateText { + private CharSequence mNormal = null; // text to display in normal state. + private CharSequence mSelected = null; // text to display in selected state. + private CharSequence mPressed = null; // text to display in pressed state. + private String mPlainText = null; // valid if the text is stateless plain text. + + public MultiStateText(boolean isHtml, String text, Context context) { + if (!isHtml || text == null) { + mPlainText = text; + return; + } + + String textNormal = text; + String textSelected = text; + String textPressed = text; + int textLength = text.length(); + int start = text.indexOf("\"@"); + + // For each font color attribute which has the value in the form '@<RESOURCEID>', + // try to load the resource and create the display strings for the 3 states. + while (start >= 0) { + start++; + int end = text.indexOf("\"", start); + if (end == -1) break; + + String colorIdString = text.substring(start, end); + int colorId = Integer.parseInt(colorIdString.substring(1)); + try { + // The following call works both for color lists and colors. + ColorStateList csl = context.getResources().getColorStateList(colorId); + int normalColor = csl.getColorForState( + View.EMPTY_STATE_SET, csl.getDefaultColor()); + int selectedColor = csl.getColorForState( + View.SELECTED_STATE_SET, csl.getDefaultColor()); + int pressedColor = csl.getColorForState( + View.PRESSED_STATE_SET, csl.getDefaultColor()); + + // Convert the int color values into a hex string, and strip the first 2 + // characters which will be the alpha (html doesn't want this). + textNormal = textNormal.replace(colorIdString, + "#" + Integer.toHexString(normalColor).substring(2)); + textSelected = textSelected.replace(colorIdString, + "#" + Integer.toHexString(selectedColor).substring(2)); + textPressed = textPressed.replace(colorIdString, + "#" + Integer.toHexString(pressedColor).substring(2)); + } catch (Resources.NotFoundException e) { + // Nothing to do. + } + + start = text.indexOf("\"@", end); + } + mNormal = Html.fromHtml(textNormal); + mSelected = Html.fromHtml(textSelected); + mPressed = Html.fromHtml(textPressed); + } + public CharSequence normal() { + return (mPlainText != null) ? mPlainText : mNormal; + } + public CharSequence selected() { + return (mPlainText != null) ? mPlainText : mSelected; + } + public CharSequence pressed() { + return (mPlainText != null) ? mPlainText : mPressed; + } + } + private int mBackgroundColor; // the background color to draw in normal state. private View mView; // the suggestion item's view. + private MultiStateText mText1Strings = null; + private MultiStateText mText2Strings = null; protected SuggestionItemView(Context context, Cursor cursor) { // Initialize ourselves @@ -537,12 +600,48 @@ class SuggestionsAdapter extends ResourceCursorAdapter { } } + private void setInitialTextForView(TextView view, MultiStateText multiState, + String plainText) { + // Set the text even if it's null, since we need to clear any previous text. + CharSequence text = (multiState != null) ? multiState.normal() : plainText; + view.setText(text); + + if (TextUtils.isEmpty(text)) { + view.setVisibility(View.GONE); + } else { + view.setVisibility(View.VISIBLE); + } + } + + public void setTextStrings(String text1, String text2, boolean isHtml, Context context) { + mText1Strings = new MultiStateText(isHtml, text1, context); + mText2Strings = new MultiStateText(isHtml, text2, context); + + ChildViewCache views = (ChildViewCache) getTag(); + setInitialTextForView(views.mText1, mText1Strings, text1); + setInitialTextForView(views.mText2, mText2Strings, text2); + } + + public void updateTextViewContentIfRequired() { + // Check if the pressed or selected state has changed since the last call. + boolean isPressedNow = isPressed(); + boolean isSelectedNow = isSelected(); + + ChildViewCache views = (ChildViewCache) getTag(); + views.mText1.setText((isPressedNow ? mText1Strings.pressed() : + (isSelectedNow ? mText1Strings.selected() : mText1Strings.normal()))); + views.mText2.setText((isPressedNow ? mText2Strings.pressed() : + (isSelectedNow ? mText2Strings.selected() : mText2Strings.normal()))); + } + public void setColor(int backgroundColor) { mBackgroundColor = backgroundColor; } @Override public void dispatchDraw(Canvas canvas) { + updateTextViewContentIfRequired(); + if (mBackgroundColor != 0 && !isPressed() && !isSelected()) { canvas.drawColor(mBackgroundColor); } diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index ec671d5..9f01923 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -16,15 +16,27 @@ package android.webkit; +import android.content.ContentValues; import android.content.Context; +import android.content.SharedPreferences; import android.os.Build; import android.os.Handler; import android.os.Message; +import android.preference.PreferenceManager; import android.provider.Checkin; +import android.provider.Settings; +import android.util.Log; +import java.io.File; import java.lang.SecurityException; + +import android.content.SharedPreferences.Editor; import android.content.pm.PackageManager; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteStatement; +import java.util.HashSet; import java.util.Locale; /** @@ -176,6 +188,43 @@ public class WebSettings { private boolean mBuiltInZoomControls = false; private boolean mAllowFileAccess = true; + // Donut-specific hack to keep Gears permissions in sync with the + // system location setting. + // TODO: Make sure this hack is removed in Eclair, when Gears + // is also removed. + // Used to remember if we checked the Gears permissions already. + static boolean mCheckedGearsPermissions = false; + // The Gears permissions database directory. + private final static String GEARS_DATABASE_DIR = "gears"; + // The Gears permissions database file name. + private final static String GEARS_DATABASE_FILE = "permissions.db"; + // The Gears location permissions table. + private final static String GEARS_LOCATION_ACCESS_TABLE_NAME = + "LocationAccess"; + // The Gears storage access permissions table. + private final static String GEARS_STORAGE_ACCESS_TABLE_NAME = "Access"; + // The Gears permissions db schema version table. + private final static String GEARS_SCHEMA_VERSION_TABLE_NAME = + "VersionInfo"; + // The shared pref name. + private static final String LAST_KNOWN_LOCATION_SETTING = + "lastKnownLocationSystemSetting"; + // The Browser package name. + private static final String BROWSER_PACKAGE_NAME = "com.android.browser"; + // The Google URLs whitelisted for Gears location access. + private static HashSet<String> sGearsWhiteList; + + static { + sGearsWhiteList = new HashSet<String>(); + // NOTE: DO NOT ADD A "/" AT THE END! + sGearsWhiteList.add("http://www.google.com"); + sGearsWhiteList.add("http://www.google.co.uk"); + } + + private static final String LOGTAG = "webcore"; + static final boolean DEBUG = false; + static final boolean LOGV_ENABLED = DEBUG; + // Class to handle messages before WebCore is ready. private class EventHandler { // Message id for syncing @@ -196,6 +245,7 @@ public class WebSettings { switch (msg.what) { case SYNC: synchronized (WebSettings.this) { + checkGearsPermissions(); if (mBrowserFrame.mNativeFrame != 0) { nativeSync(mBrowserFrame.mNativeFrame); } @@ -1163,6 +1213,126 @@ public class WebSettings { return size; } + private void checkGearsPermissions() { + // Did we already check the permissions? + if (mCheckedGearsPermissions) { + return; + } + // Are we running in the browser? + if (!BROWSER_PACKAGE_NAME.equals(mContext.getPackageName())) { + return; + } + // Is the pluginsPath sane? + if (mPluginsPath == null || mPluginsPath.length() == 0) { + // We don't yet have a meaningful plugin path, so + // we can't do anything about the Gears permissions. + return; + } + // Remember we checked the Gears permissions. + mCheckedGearsPermissions = true; + // Get the current system settings. + int setting = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.USE_LOCATION_FOR_SERVICES, -1); + // Check if we need to set the Gears permissions. + if (setting != -1 && locationSystemSettingChanged(setting)) { + setGearsPermissionForGoogleDomains(setting); + } + } + + private boolean locationSystemSettingChanged(int newSetting) { + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(mContext); + int oldSetting = 0; + oldSetting = prefs.getInt(LAST_KNOWN_LOCATION_SETTING, oldSetting); + if (oldSetting == newSetting) { + return false; + } + Editor ed = prefs.edit(); + ed.putInt(LAST_KNOWN_LOCATION_SETTING, newSetting); + ed.commit(); + return true; + } + + private void setGearsPermissionForGoogleDomains(int systemPermission) { + // Transform the system permission into a Gears permission + int gearsPermission = (systemPermission == 1 ? 1 : 2); + // Build the path to the Gears library. + + File file = new File(mPluginsPath).getParentFile(); + if (file == null) { + return; + } + // Build the Gears database file name. + file = new File(file.getAbsolutePath() + File.separator + + GEARS_DATABASE_DIR + File.separator + GEARS_DATABASE_FILE); + // Remember whether or not we need to create the LocationAccess table. + boolean needToCreateTables = !file.exists(); + // Try opening the Gears database. + SQLiteDatabase permissions; + try { + permissions = SQLiteDatabase.openOrCreateDatabase(file, null); + } catch (SQLiteException e) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "Could not open Gears permission DB: " + + e.getMessage()); + } + // Just bail out. + return; + } + // We now have a database open. Begin a transaction. + permissions.beginTransaction(); + try { + if (needToCreateTables) { + // Create the tables. Note that this creates the + // Gears tables for the permissions DB schema version 2. + // The Gears schema upgrade process will take care of the rest. + // First, the storage access table. + SQLiteStatement statement = permissions.compileStatement( + "CREATE TABLE IF NOT EXISTS " + + GEARS_STORAGE_ACCESS_TABLE_NAME + + " (Name TEXT UNIQUE, Value)"); + statement.execute(); + // Next the location access table. + statement = permissions.compileStatement( + "CREATE TABLE IF NOT EXISTS " + + GEARS_LOCATION_ACCESS_TABLE_NAME + + " (Name TEXT UNIQUE, Value)"); + statement.execute(); + // Finally, the schema version table. + statement = permissions.compileStatement( + "CREATE TABLE IF NOT EXISTS " + + GEARS_SCHEMA_VERSION_TABLE_NAME + + " (Name TEXT UNIQUE, Value)"); + statement.execute(); + // Set the schema version to 2. + ContentValues schema = new ContentValues(); + schema.put("Name", "Version"); + schema.put("Value", 2); + permissions.insert(GEARS_SCHEMA_VERSION_TABLE_NAME, null, + schema); + } + + ContentValues permissionValues = new ContentValues(); + + for (String url : sGearsWhiteList) { + permissionValues.put("Name", url); + permissionValues.put("Value", gearsPermission); + permissions.replace(GEARS_LOCATION_ACCESS_TABLE_NAME, null, + permissionValues); + permissionValues.clear(); + } + // Commit the transaction. + permissions.setTransactionSuccessful(); + } catch (SQLiteException e) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "Could not set the Gears permissions: " + + e.getMessage()); + } + } finally { + permissions.endTransaction(); + permissions.close(); + } + } /* Post a SYNC message to handle syncing the native settings. */ private synchronized void postSync() { // Only post if a sync is not pending diff --git a/core/res/res/color/search_url_text.xml b/core/res/res/color/search_url_text.xml new file mode 100644 index 0000000..449fdf0 --- /dev/null +++ b/core/res/res/color/search_url_text.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2009 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true" android:color="@android:color/search_url_text_pressed"/> + <item android:state_selected="true" android:color="@android:color/search_url_text_selected"/> + <item android:color="@android:color/search_url_text_normal"/> <!-- not selected --> +</selector> diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index d284d0f..b7de997 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -74,7 +74,9 @@ <color name="perms_normal_perm_color">#c0c0c0</color> <!-- For search-related UIs --> - <color name="search_url_text">#7fa87f</color> + <color name="search_url_text_normal">#7fa87f</color> + <color name="search_url_text_selected">@android:color/black</color> + <color name="search_url_text_pressed">@android:color/black</color> <color name="search_widget_corpus_item_background">@android:color/lighter_gray</color> </resources> diff --git a/libs/audioflinger/AudioFlinger.cpp b/libs/audioflinger/AudioFlinger.cpp index 8a19fbd..f5bdeda 100644 --- a/libs/audioflinger/AudioFlinger.cpp +++ b/libs/audioflinger/AudioFlinger.cpp @@ -738,12 +738,13 @@ bool AudioFlinger::streamMute(int stream) const bool AudioFlinger::isMusicActive() const { + Mutex::Autolock _l(mLock); #ifdef WITH_A2DP if (isA2dpEnabled()) { - return mA2dpMixerThread->isMusicActive(); + return mA2dpMixerThread->isMusicActive_l(); } #endif - return mHardwareMixerThread->isMusicActive(); + return mHardwareMixerThread->isMusicActive_l(); } status_t AudioFlinger::setParameter(const char* key, const char* value) @@ -1444,7 +1445,8 @@ bool AudioFlinger::MixerThread::streamMute(int stream) const return mStreamTypes[stream].mute; } -bool AudioFlinger::MixerThread::isMusicActive() const +// isMusicActive_l() must be called with AudioFlinger::mLock held +bool AudioFlinger::MixerThread::isMusicActive_l() const { size_t count = mActiveTracks.size(); for (size_t i = 0 ; i < count ; ++i) { @@ -2030,7 +2032,10 @@ void AudioFlinger::MixerThread::OutputTrack::write(int16_t* data, uint32_t frame inBuffer.i16 = data; if (mCblk->user == 0) { - if (mOutputMixerThread->isMusicActive()) { + mOutputMixerThread->mAudioFlinger->mLock.lock(); + bool isMusicActive = mOutputMixerThread->isMusicActive_l(); + mOutputMixerThread->mAudioFlinger->mLock.unlock(); + if (isMusicActive) { mCblk->forceReady = 1; LOGV("OutputTrack::start() force ready"); } else if (mCblk->frameCount > frames){ diff --git a/libs/audioflinger/AudioFlinger.h b/libs/audioflinger/AudioFlinger.h index 8e47b29..634934e 100644 --- a/libs/audioflinger/AudioFlinger.h +++ b/libs/audioflinger/AudioFlinger.h @@ -463,7 +463,7 @@ private: virtual float streamVolume(int stream) const; virtual bool streamMute(int stream) const; - bool isMusicActive() const; + bool isMusicActive_l() const; sp<Track> createTrack_l( diff --git a/libs/utils/String8.cpp b/libs/utils/String8.cpp index 71bf3ce..e908ec1 100644 --- a/libs/utils/String8.cpp +++ b/libs/utils/String8.cpp @@ -681,7 +681,7 @@ size_t strnlen32(const char32_t *s, size_t maxlen) return ss-s; } -size_t utf8_codepoint_count(const char *src) +size_t utf8_length(const char *src) { const char *cur = src; size_t ret = 0; 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(); } } diff --git a/media/libmedia/ToneGenerator.cpp b/media/libmedia/ToneGenerator.cpp index c22cd53..5435da7 100644 --- a/media/libmedia/ToneGenerator.cpp +++ b/media/libmedia/ToneGenerator.cpp @@ -1225,6 +1225,8 @@ audioCallback_EndLoop: LOGV("Cbk restarting track\n"); if (lpToneGen->prepareWave()) { lpToneGen->mState = TONE_STARTING; + // must reload lpToneDesc as prepareWave() may change mpToneDesc + lpToneDesc = lpToneGen->mpToneDesc; } else { LOGW("Cbk restarting prepareWave() failed\n"); lpToneGen->mState = TONE_IDLE; diff --git a/packages/VpnServices/src/com/android/server/vpn/VpnService.java b/packages/VpnServices/src/com/android/server/vpn/VpnService.java index a60788a..22669d2 100644 --- a/packages/VpnServices/src/com/android/server/vpn/VpnService.java +++ b/packages/VpnServices/src/com/android/server/vpn/VpnService.java @@ -189,7 +189,7 @@ abstract class VpnService<E extends VpnProfile> { mServiceHelper.stop(); } catch (Throwable e) { - Log.e(TAG, "onError()", e); + Log.e(TAG, "onDisconnect()", e); onFinalCleanUp(); } } @@ -219,21 +219,28 @@ abstract class VpnService<E extends VpnProfile> { } private void waitUntilConnectedOrTimedout() { - sleep(2000); // 2 seconds - for (int i = 0; i < 60; i++) { - if (VPN_IS_UP.equals(SystemProperties.get(VPN_UP))) { - onConnected(); - return; - } - sleep(500); // 0.5 second - } + // Run this in the background thread to not block UI + new Thread(new Runnable() { + public void run() { + sleep(2000); // 2 seconds + for (int i = 0; i < 60; i++) { + if (VPN_IS_UP.equals(SystemProperties.get(VPN_UP))) { + onConnected(); + return; + } else if (mState != VpnState.CONNECTING) { + break; + } + sleep(500); // 0.5 second + } - synchronized (this) { - if (mState == VpnState.CONNECTING) { - Log.d(TAG, " connecting timed out !!"); - onError(); + synchronized (VpnService.this) { + if (mState == VpnState.CONNECTING) { + Log.d(TAG, " connecting timed out !!"); + onError(); + } + } } - } + }).start(); } private synchronized void onConnected() { |
