Merge "Put back the status bar animations."
diff --git a/Android.mk b/Android.mk
index ea8314c..752a5f8 100644
--- a/Android.mk
+++ b/Android.mk
@@ -183,6 +183,7 @@
media/java/android/media/IAudioFocusDispatcher.aidl \
media/java/android/media/IMediaScannerListener.aidl \
media/java/android/media/IMediaScannerService.aidl \
+ media/java/android/media/IRemoteControlClient.aidl \
telephony/java/com/android/internal/telephony/IPhoneStateListener.aidl \
telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl \
telephony/java/com/android/internal/telephony/ITelephony.aidl \
diff --git a/api/current.txt b/api/current.txt
index 817ed9c..876d555 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -15,6 +15,7 @@
field public static final java.lang.String ACCESS_SURFACE_FLINGER = "android.permission.ACCESS_SURFACE_FLINGER";
field public static final java.lang.String ACCESS_WIFI_STATE = "android.permission.ACCESS_WIFI_STATE";
field public static final java.lang.String ACCOUNT_MANAGER = "android.permission.ACCOUNT_MANAGER";
+ field public static final java.lang.String ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL";
field public static final java.lang.String AUTHENTICATE_ACCOUNTS = "android.permission.AUTHENTICATE_ACCOUNTS";
field public static final java.lang.String BATTERY_STATS = "android.permission.BATTERY_STATS";
field public static final java.lang.String BIND_APPWIDGET = "android.permission.BIND_APPWIDGET";
@@ -24134,7 +24135,7 @@
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator CREATOR;
field public static final int RESULT_ATTR_IN_THE_DICTIONARY = 1; // 0x1
- field public static final int RESULT_ATTR_LOOKS_TYPO = 2; // 0x2
+ field public static final int RESULT_ATTR_LOOKS_LIKE_TYPO = 2; // 0x2
}
public final class TextInfo implements android.os.Parcelable {
diff --git a/core/java/android/server/BluetoothPanProfileHandler.java b/core/java/android/server/BluetoothPanProfileHandler.java
index 0d63e19..ff22a45 100644
--- a/core/java/android/server/BluetoothPanProfileHandler.java
+++ b/core/java/android/server/BluetoothPanProfileHandler.java
@@ -173,7 +173,7 @@
if (!mBluetoothService.disconnectPanServerDeviceNative(objectPath,
device.getAddress(),
- panDevice.mIfaceAddr)) {
+ panDevice.mIface)) {
errorLog("could not disconnect Pan Server Device "+device.getAddress());
// Restore prev state
@@ -291,6 +291,7 @@
panDevice.mState = state;
panDevice.mIfaceAddr = ifaceAddr;
panDevice.mLocalRole = role;
+ panDevice.mIface = iface;
}
Intent intent = new Intent(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java
index 66d2641..744d564 100644
--- a/core/java/android/view/HardwareRenderer.java
+++ b/core/java/android/view/HardwareRenderer.java
@@ -713,17 +713,23 @@
// Cancels any existing buffer to ensure we'll get a buffer
// of the right size before we call eglSwapBuffers
sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
- sEgl.eglDestroySurface(sEglDisplay, mEglSurface);
- mEglSurface = sEgl.eglCreateWindowSurface(sEglDisplay, sEglConfig, holder, null);
+
+ if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) {
+ sEgl.eglDestroySurface(sEglDisplay, mEglSurface);
+ }
- if (mEglSurface == null || mEglSurface == EGL_NO_SURFACE) {
- int error = sEgl.eglGetError();
- if (error == EGL_BAD_NATIVE_WINDOW) {
- Log.e(LOG_TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
- return;
+ if (holder.getSurface().isValid()) {
+ mEglSurface = sEgl.eglCreateWindowSurface(sEglDisplay, sEglConfig, holder, null);
+
+ if (mEglSurface == null || mEglSurface == EGL_NO_SURFACE) {
+ int error = sEgl.eglGetError();
+ if (error == EGL_BAD_NATIVE_WINDOW) {
+ Log.e(LOG_TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
+ return;
+ }
+ throw new RuntimeException("createWindowSurface failed "
+ + getEGLErrorString(error));
}
- throw new RuntimeException("createWindowSurface failed "
- + getEGLErrorString(error));
}
}
diff --git a/core/java/android/view/textservice/SuggestionsInfo.java b/core/java/android/view/textservice/SuggestionsInfo.java
index 3332f1e..ed0f89d 100644
--- a/core/java/android/view/textservice/SuggestionsInfo.java
+++ b/core/java/android/view/textservice/SuggestionsInfo.java
@@ -34,9 +34,9 @@
/**
* Flag of the attributes of the suggestions that can be obtained by
* {@link #getSuggestionsAttributes}: this tells that the text service thinks the requested
- * word looks a typo.
+ * word looks like a typo.
*/
- public static final int RESULT_ATTR_LOOKS_TYPO = 0x0002;
+ public static final int RESULT_ATTR_LOOKS_LIKE_TYPO = 0x0002;
private final int mSuggestionsAttributes;
private final String[] mSuggestions;
private final boolean mSuggestionsAvailable;
diff --git a/core/java/android/webkit/DeviceMotionService.java b/core/java/android/webkit/DeviceMotionService.java
index 7d7a0f0..b4d5759 100755
--- a/core/java/android/webkit/DeviceMotionService.java
+++ b/core/java/android/webkit/DeviceMotionService.java
@@ -99,6 +99,7 @@
mUpdateRunnable = new Runnable() {
@Override
public void run() {
+ assert mIsRunning;
mManager.onMotionChange(new Double(mLastAcceleration[0]),
new Double(mLastAcceleration[1]), new Double(mLastAcceleration[2]),
INTERVAL_MILLIS);
@@ -157,6 +158,11 @@
assert WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName());
assert(event.sensor.getType() == Sensor.TYPE_ACCELEROMETER);
+ // We may get callbacks after the call to getSensorManager().unregisterListener() returns.
+ if (!mIsRunning) {
+ return;
+ }
+
boolean firstData = mLastAcceleration == null;
mLastAcceleration = event.values;
if (firstData) {
diff --git a/core/java/android/webkit/DeviceOrientationService.java b/core/java/android/webkit/DeviceOrientationService.java
index f3c0576..47c8ab7 100755
--- a/core/java/android/webkit/DeviceOrientationService.java
+++ b/core/java/android/webkit/DeviceOrientationService.java
@@ -188,6 +188,7 @@
assert(event.values.length == 3);
assert WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName());
+ // We may get callbacks after the call to getSensorManager().unregisterListener() returns.
if (!mIsRunning) {
return;
}
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index f4fd551..7620a63 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -8405,9 +8405,9 @@
}
}
- // Called by JNI to invalidate the View, given rectangle coordinates in
- // content space
- private void pageSwapCallback() {
+ /** @hide Called by JNI when pages are swapped (only occurs with hardware
+ * acceleration) */
+ protected void pageSwapCallback() {
if (inEditingMode()) {
didUpdateWebTextViewDimensions(ANYWHERE);
}
@@ -8426,11 +8426,11 @@
WebViewCore.ViewState viewState = draw.mViewState;
boolean isPictureAfterFirstLayout = viewState != null;
- // Request a callback on pageSwap (to reposition the webtextview)
- boolean registerPageSwapCallback =
- !mZoomManager.isFixedLengthAnimationInProgress() && inEditingMode();
-
if (updateBaseLayer) {
+ // Request a callback on pageSwap (to reposition the webtextview)
+ boolean registerPageSwapCallback =
+ !mZoomManager.isFixedLengthAnimationInProgress() && inEditingMode();
+
setBaseLayer(draw.mBaseLayer, draw.mInvalRegion,
getSettings().getShowVisualIndicator(),
isPictureAfterFirstLayout, registerPageSwapCallback);
@@ -9084,6 +9084,16 @@
}
}
+ /** @hide send content invalidate */
+ protected void contentInvalidateAll() {
+ mWebViewCore.sendMessage(EventHub.CONTENT_INVALIDATE_ALL);
+ }
+
+ /** @hide call pageSwapCallback upon next page swap */
+ protected void registerPageSwapCallback() {
+ nativeRegisterPageSwapCallback();
+ }
+
/**
* Begin collecting per-tile profiling data
*
@@ -9245,6 +9255,7 @@
private native void nativeStopGL();
private native Rect nativeSubtractLayers(Rect content);
private native int nativeTextGeneration();
+ private native void nativeRegisterPageSwapCallback();
private native void nativeTileProfilingStart();
private native float nativeTileProfilingStop();
private native void nativeTileProfilingClear();
diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java
index 8d8023b..400cdbd 100644
--- a/core/java/android/webkit/WebViewCore.java
+++ b/core/java/android/webkit/WebViewCore.java
@@ -999,6 +999,7 @@
static final int DUMP_V8COUNTERS = 173;
static final int SET_JS_FLAGS = 174;
+ static final int CONTENT_INVALIDATE_ALL = 175;
// Geolocation
static final int GEOLOCATION_PERMISSIONS_PROVIDE = 180;
@@ -1503,6 +1504,10 @@
nativeSetJsFlags((String)msg.obj);
break;
+ case CONTENT_INVALIDATE_ALL:
+ nativeContentInvalidateAll();
+ break;
+
case SAVE_WEBARCHIVE:
WebView.SaveWebArchiveMessage saveMessage =
(WebView.SaveWebArchiveMessage)msg.obj;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 65ee745..9de94cf 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -343,6 +343,9 @@
private Drawable mSelectHandleRight;
private Drawable mSelectHandleCenter;
+ // Global listener that detects changes in the global position of the TextView
+ private PositionListener mPositionListener;
+
private float mLastDownPositionX, mLastDownPositionY;
private Callback mCustomSelectionActionModeCallback;
@@ -394,7 +397,7 @@
*/
boolean onEditorAction(TextView v, int actionId, KeyEvent event);
}
-
+
public TextView(Context context) {
this(context, null);
}
@@ -2081,7 +2084,7 @@
TextAppearance_textStyle, -1);
setTypefaceByIndex(typefaceIndex, styleIndex);
-
+
if (appearance.getBoolean(com.android.internal.R.styleable.TextAppearance_textAllCaps,
false)) {
setTransformationMethod(new AllCapsTransformationMethod(getContext()));
@@ -3019,7 +3022,7 @@
* To style your strings, attach android.text.style.* objects to a
* {@link android.text.SpannableString SpannableString}, or see the
* <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
- * Available Resource Types</a> documentation for an example of setting
+ * Available Resource Types</a> documentation for an example of setting
* formatted text in the XML resource file.
*
* @attr ref android.R.styleable#TextView_text
@@ -8757,32 +8760,247 @@
return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
}
+ private PositionListener getPositionListener() {
+ if (mPositionListener == null) {
+ mPositionListener = new PositionListener();
+ }
+ return mPositionListener;
+ }
+
+ private interface TextViewPositionListener {
+ public void updatePosition(int parentPositionX, int parentPositionY, boolean modified);
+ }
+
+ private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
+ // 3 handles, 2 ActionPopup (suggestionsPopup first hides the others)
+ private final int MAXIMUM_NUMBER_OF_LISTENERS = 5;
+ private TextViewPositionListener[] mPositionListeners =
+ new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
+ private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
+ private boolean mPositionHasChanged = true;
+ // Absolute position of the TextView with respect to its parent window
+ private int mPositionX, mPositionY;
+ private int mNumberOfListeners;
+
+ public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
+ if (mNumberOfListeners == 0) {
+ updatePosition();
+ ViewTreeObserver vto = TextView.this.getViewTreeObserver();
+ vto.addOnPreDrawListener(this);
+ }
+
+ int emptySlotIndex = -1;
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ TextViewPositionListener listener = mPositionListeners[i];
+ if (listener == positionListener) {
+ return;
+ } else if (emptySlotIndex < 0 && listener == null) {
+ emptySlotIndex = i;
+ }
+ }
+
+ mPositionListeners[emptySlotIndex] = positionListener;
+ mCanMove[emptySlotIndex] = canMove;
+ mNumberOfListeners++;
+ }
+
+ public void removeSubscriber(TextViewPositionListener positionListener) {
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ if (mPositionListeners[i] == positionListener) {
+ mPositionListeners[i] = null;
+ mNumberOfListeners--;
+ break;
+ }
+ }
+
+ if (mNumberOfListeners == 0) {
+ ViewTreeObserver vto = TextView.this.getViewTreeObserver();
+ vto.removeOnPreDrawListener(this);
+ }
+ }
+
+ public int getPositionX() {
+ return mPositionX;
+ }
+
+ public int getPositionY() {
+ return mPositionY;
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ updatePosition();
+
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ if (mPositionHasChanged || mCanMove[i]) {
+ TextViewPositionListener positionListener = mPositionListeners[i];
+ if (positionListener != null) {
+ positionListener.updatePosition(mPositionX, mPositionY,
+ mPositionHasChanged);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private void updatePosition() {
+ TextView.this.getLocationInWindow(mTempCoords);
+
+ mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
+
+ mPositionX = mTempCoords[0];
+ mPositionY = mTempCoords[1];
+ }
+
+ public boolean isVisible(int positionX, int positionY) {
+ final TextView textView = TextView.this;
+
+ if (mTempRect == null) mTempRect = new Rect();
+ final Rect clip = mTempRect;
+ clip.left = getCompoundPaddingLeft();
+ clip.top = getExtendedPaddingTop();
+ clip.right = textView.getWidth() - getCompoundPaddingRight();
+ clip.bottom = textView.getHeight() - getExtendedPaddingBottom();
+
+ final ViewParent parent = textView.getParent();
+ if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) {
+ return false;
+ }
+
+ int posX = mPositionX + positionX;
+ int posY = mPositionY + positionY;
+
+ // Offset by 1 to take into account 0.5 and int rounding around getPrimaryHorizontal.
+ return posX >= clip.left - 1 && posX <= clip.right + 1 &&
+ posY >= clip.top && posY <= clip.bottom;
+ }
+
+ public boolean isOffsetVisible(int offset) {
+ final int line = mLayout.getLineForOffset(offset);
+ final int lineBottom = mLayout.getLineBottom(line);
+ final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset);
+ return isVisible(primaryHorizontal, lineBottom);
+ }
+ }
+
+ private abstract class PinnedPopupWindow implements TextViewPositionListener {
+ protected PopupWindow mPopupWindow;
+ protected LinearLayout mContentView;
+ int mPositionX, mPositionY;
+
+ protected abstract void createPopupWindow();
+ protected abstract void initContentView();
+ protected abstract int getTextOffset();
+ protected abstract int getVerticalLocalPosition(int line);
+ protected abstract int clipVertically(int positionY);
+
+ public PinnedPopupWindow() {
+ createPopupWindow();
+
+ mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
+ mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
+ mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ mContentView = new LinearLayout(TextView.this.getContext());
+ LayoutParams wrapContent = new LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ mContentView.setLayoutParams(wrapContent);
+
+ initContentView();
+ mPopupWindow.setContentView(mContentView);
+ }
+
+ public void show() {
+ final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
+ mContentView.measure(
+ View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
+ View.MeasureSpec.AT_MOST),
+ View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
+ View.MeasureSpec.AT_MOST));
+
+ TextView.this.getPositionListener().addSubscriber(this, false);
+
+ computeLocalPosition();
+
+ final PositionListener positionListener = TextView.this.getPositionListener();
+ updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
+ }
+
+ private void computeLocalPosition() {
+ final int offset = getTextOffset();
+
+ final int width = mContentView.getMeasuredWidth();
+ mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - width / 2.0f);
+ mPositionX += viewportToContentHorizontalOffset();
+
+ final int line = mLayout.getLineForOffset(offset);
+ mPositionY = getVerticalLocalPosition(line);
+ mPositionY += viewportToContentVerticalOffset();
+ }
+
+ private void updatePosition(int parentPositionX, int parentPositionY) {
+ int positionX = parentPositionX + mPositionX;
+ int positionY = parentPositionY + mPositionY;
+
+ positionY = clipVertically(positionY);
+
+ // Horizontal clipping
+ final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
+ final int width = mContentView.getMeasuredWidth();
+ positionX = Math.min(displayMetrics.widthPixels - width, positionX);
+ positionX = Math.max(0, positionX);
+
+ if (isShowing()) {
+ mPopupWindow.update(positionX, positionY, -1, -1);
+ } else {
+ mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
+ positionX, positionY);
+ }
+ }
+
+ public void hide() {
+ mPopupWindow.dismiss();
+ TextView.this.getPositionListener().removeSubscriber(this);
+ }
+
+ @Override
+ public void updatePosition(int parentPositionX, int parentPositionY, boolean modified) {
+ if (isShowing() && getPositionListener().isOffsetVisible(getTextOffset())) {
+ updatePosition(parentPositionX, parentPositionY);
+ } else {
+ hide();
+ }
+ }
+
+ public boolean isShowing() {
+ return mPopupWindow.isShowing();
+ }
+ }
+
private static class SuggestionRangeSpan extends UnderlineSpan {
// TODO themable, would be nice to make it a child class of TextAppearanceSpan, but
// there is no way to have underline and TextAppearanceSpan.
}
- private class SuggestionsPopupWindow implements OnClickListener {
+ private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnClickListener {
private static final int MAX_NUMBER_SUGGESTIONS = 5;
private static final int NO_SUGGESTIONS = -1;
- private final PopupWindow mPopupWindow;
- private LinearLayout mSuggestionsContainer;
private WordIterator mSuggestionWordIterator;
private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0];
- public SuggestionsPopupWindow() {
+ @Override
+ protected void createPopupWindow() {
mPopupWindow = new PopupWindow(TextView.this.mContext, null,
- com.android.internal.R.attr.textSuggestionsWindowStyle);
- mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
+ com.android.internal.R.attr.textSuggestionsWindowStyle);
mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
mPopupWindow.setOutsideTouchable(true);
- mPopupWindow.setClippingEnabled(true);
+ mPopupWindow.setClippingEnabled(false);
+ }
- mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
- mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
-
- mSuggestionsContainer = new LinearLayout(TextView.this.mContext);
- mSuggestionsContainer.setOrientation(LinearLayout.VERTICAL);
+ @Override
+ protected void initContentView() {
+ mContentView.setOrientation(LinearLayout.VERTICAL);
LayoutInflater inflater = (LayoutInflater) TextView.this.mContext.
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@@ -8795,7 +9013,7 @@
// Inflate the suggestion items once and for all.
for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) {
View childView = inflater.inflate(mTextEditSuggestionItemLayout,
- mSuggestionsContainer, false);
+ mContentView, false);
if (! (childView instanceof TextView)) {
throw new IllegalArgumentException(
@@ -8803,11 +9021,9 @@
}
childView.setTag(new SuggestionInfo());
- mSuggestionsContainer.addView(childView);
+ mContentView.addView(childView);
childView.setOnClickListener(this);
}
-
- mPopupWindow.setContentView(mSuggestionsContainer);
}
private class SuggestionInfo {
@@ -8827,30 +9043,61 @@
SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
// Cache the span length for performance reason.
- final HashMap<SuggestionSpan, Integer> spanLengthMap =
- new HashMap<SuggestionSpan, Integer>();
+ final HashMap<SuggestionSpan, Integer> spansLengths =
+ new HashMap<SuggestionSpan, Integer>();
for (SuggestionSpan suggestionSpan : suggestionSpans) {
int start = spannable.getSpanStart(suggestionSpan);
int end = spannable.getSpanEnd(suggestionSpan);
- spanLengthMap.put(suggestionSpan, end - start);
+ spansLengths.put(suggestionSpan, Integer.valueOf(end - start));
}
// The suggestions are sorted according to the lenght of the text that they cover
// (shorter first)
Arrays.sort(suggestionSpans, new Comparator<SuggestionSpan>() {
public int compare(SuggestionSpan span1, SuggestionSpan span2) {
- return spanLengthMap.get(span1) - spanLengthMap.get(span2);
+ return spansLengths.get(span1).intValue() - spansLengths.get(span2).intValue();
}
});
return suggestionSpans;
}
+ @Override
public void show() {
if (!(mText instanceof Editable)) return;
+ updateSuggestions();
- Spannable spannable = (Spannable) TextView.this.mText;
+ super.show();
+ }
+
+ @Override
+ protected int getTextOffset() {
+ return getSelectionStart();
+ }
+
+ @Override
+ protected int getVerticalLocalPosition(int line) {
+ return mLayout.getLineBottom(line);
+ }
+
+ @Override
+ protected int clipVertically(int positionY) {
+ final int height = mContentView.getMeasuredHeight();
+ final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
+ return Math.min(positionY, displayMetrics.heightPixels - height);
+ }
+
+ @Override
+ public void hide() {
+ super.hide();
+ if ((mText instanceof Editable) && mSuggestionRangeSpan != null) {
+ ((Editable) mText).removeSpan(mSuggestionRangeSpan);
+ }
+ }
+
+ private void updateSuggestions() {
+ Spannable spannable = (Spannable)TextView.this.mText;
SuggestionSpan[] suggestionSpans = getSuggestionSpans();
final int nbSpans = suggestionSpans.length;
@@ -8869,7 +9116,7 @@
String[] suggestions = suggestionSpan.getSuggestions();
int nbSuggestions = suggestions.length;
for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
- TextView textView = (TextView) mSuggestionsContainer.getChildAt(
+ TextView textView = (TextView) mContentView.getChildAt(
totalNbSuggestions);
textView.setText(suggestions[suggestionIndex]);
SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
@@ -8889,7 +9136,7 @@
if (totalNbSuggestions == 0) {
// TODO Replace by final text, use a dedicated layout, add a fade out timer...
- TextView textView = (TextView) mSuggestionsContainer.getChildAt(0);
+ TextView textView = (TextView) mContentView.getChildAt(0);
textView.setText("No suggestions available");
SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
suggestionInfo.spanStart = NO_SUGGESTIONS;
@@ -8900,26 +9147,17 @@
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
for (int i = 0; i < totalNbSuggestions; i++) {
- final TextView textView = (TextView) mSuggestionsContainer.getChildAt(i);
+ final TextView textView = (TextView) mContentView.getChildAt(i);
highlightTextDifferences(textView, spanUnionStart, spanUnionEnd);
}
}
for (int i = 0; i < totalNbSuggestions; i++) {
- mSuggestionsContainer.getChildAt(i).setVisibility(VISIBLE);
+ mContentView.getChildAt(i).setVisibility(VISIBLE);
}
for (int i = totalNbSuggestions; i < MAX_NUMBER_SUGGESTIONS; i++) {
- mSuggestionsContainer.getChildAt(i).setVisibility(GONE);
+ mContentView.getChildAt(i).setVisibility(GONE);
}
-
- final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
- final int screenWidth = displayMetrics.widthPixels;
- final int screenHeight = displayMetrics.heightPixels;
- mSuggestionsContainer.measure(
- View.MeasureSpec.makeMeasureSpec(screenWidth, View.MeasureSpec.AT_MOST),
- View.MeasureSpec.makeMeasureSpec(screenHeight, View.MeasureSpec.AT_MOST));
-
- positionAtCursor();
}
private long[] getWordLimits(CharSequence text) {
@@ -9071,17 +9309,6 @@
textView.setText(ssb);
}
- public void hide() {
- if ((mText instanceof Editable) && mSuggestionRangeSpan != null) {
- ((Editable) mText).removeSpan(mSuggestionRangeSpan);
- }
- mPopupWindow.dismiss();
- }
-
- public boolean isShowing() {
- return mPopupWindow.isShowing();
- }
-
@Override
public void onClick(View view) {
if (view instanceof TextView) {
@@ -9141,44 +9368,6 @@
}
hide();
}
-
- void positionAtCursor() {
- View contentView = mPopupWindow.getContentView();
- int width = contentView.getMeasuredWidth();
- int height = contentView.getMeasuredHeight();
- final int offset = TextView.this.getSelectionStart();
- final int line = mLayout.getLineForOffset(offset);
- final int lineBottom = mLayout.getLineBottom(line);
- float primaryHorizontal = mLayout.getPrimaryHorizontal(offset);
-
- final Rect bounds = sCursorControllerTempRect;
- bounds.left = (int) (primaryHorizontal - width / 2.0f);
- bounds.top = lineBottom;
-
- bounds.right = bounds.left + width;
- bounds.bottom = bounds.top + height;
-
- convertFromViewportToContentCoordinates(bounds);
-
- final int[] coords = mTempCoords;
- TextView.this.getLocationInWindow(coords);
- coords[0] += bounds.left;
- coords[1] += bounds.top;
-
- final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
- final int screenHeight = displayMetrics.heightPixels;
-
- // Vertical clipping
- if (coords[1] + height > screenHeight) {
- coords[1] = screenHeight - height;
- }
-
- // Horizontal clipping
- coords[0] = Math.min(displayMetrics.widthPixels - width, coords[0]);
- coords[0] = Math.max(0, coords[0]);
-
- mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
- }
}
void showSuggestions() {
@@ -9359,7 +9548,7 @@
boolean allowText = getContext().getResources().getBoolean(
com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
- mode.setTitle(allowText ?
+ mode.setTitle(allowText ?
mContext.getString(com.android.internal.R.string.textSelectionCABTitle) : null);
mode.setSubtitle(null);
@@ -9452,29 +9641,23 @@
}
}
- private class ActionPopupWindow implements OnClickListener {
- private static final int TEXT_EDIT_ACTION_POPUP_TEXT =
+ private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
+ private static final int POPUP_TEXT_LAYOUT =
com.android.internal.R.layout.text_edit_action_popup_text;
- private final PopupWindow mPopupWindow;
private TextView mPasteTextView;
private TextView mReplaceTextView;
- private LinearLayout mContentView;
// Whether or not the Paste action should be available when the action popup is displayed
private boolean mWithPaste;
- public ActionPopupWindow() {
+ @Override
+ protected void createPopupWindow() {
mPopupWindow = new PopupWindow(TextView.this.mContext, null,
com.android.internal.R.attr.textSelectHandleWindowStyle);
mPopupWindow.setClippingEnabled(true);
- mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL);
+ }
- mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
- mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
-
- mContentView = new LinearLayout(TextView.this.getContext());
- LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- mContentView.setLayoutParams(wrapContent);
+ @Override
+ protected void initContentView() {
mContentView.setOrientation(LinearLayout.HORIZONTAL);
mContentView.setBackgroundResource(
com.android.internal.R.drawable.text_edit_side_paste_window);
@@ -9482,36 +9665,26 @@
LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- mPasteTextView = (TextView) inflater.inflate(TEXT_EDIT_ACTION_POPUP_TEXT, null);
+ LayoutParams wrapContent = new LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
mPasteTextView.setLayoutParams(wrapContent);
mContentView.addView(mPasteTextView);
mPasteTextView.setText(com.android.internal.R.string.paste);
mPasteTextView.setOnClickListener(this);
- mReplaceTextView = (TextView) inflater.inflate(TEXT_EDIT_ACTION_POPUP_TEXT, null);
+ mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
mReplaceTextView.setLayoutParams(wrapContent);
mContentView.addView(mReplaceTextView);
mReplaceTextView.setText(com.android.internal.R.string.replace);
mReplaceTextView.setOnClickListener(this);
-
- mPopupWindow.setContentView(mContentView);
}
+ @Override
public void show() {
mPasteTextView.setVisibility(mWithPaste && canPaste() ? View.VISIBLE : View.GONE);
-
- final int size = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- mContentView.measure(size, size);
-
- positionAtCursor();
- }
-
- public void hide() {
- mPopupWindow.dismiss();
- }
-
- public boolean isShowing() {
- return mPopupWindow.isShowing();
+ super.show();
}
@Override
@@ -9524,48 +9697,30 @@
}
}
- void positionAtCursor() {
- int width = mContentView.getMeasuredWidth();
- int height = mContentView.getMeasuredHeight();
- final int selectionStart = TextView.this.getSelectionStart();
- final int selectionEnd = TextView.this.getSelectionEnd();
- final int offset = (selectionStart + selectionEnd) / 2;
- final int line = mLayout.getLineForOffset(offset);
- final int lineTop = mLayout.getLineTop(line);
- float primaryHorizontal = mLayout.getPrimaryHorizontal(offset);
+ @Override
+ protected int getTextOffset() {
+ return (getSelectionStart() + getSelectionEnd()) / 2;
+ }
- final Rect bounds = sCursorControllerTempRect;
- bounds.left = (int) (primaryHorizontal - width / 2.0f);
- bounds.top = lineTop - height;
+ @Override
+ protected int getVerticalLocalPosition(int line) {
+ return mLayout.getLineTop(line) - mContentView.getMeasuredHeight();
+ }
- bounds.right = bounds.left + width;
- bounds.bottom = bounds.top + height;
-
- convertFromViewportToContentCoordinates(bounds);
-
- final int[] coords = mTempCoords;
- TextView.this.getLocationInWindow(coords);
- coords[0] += bounds.left;
- coords[1] += bounds.top;
-
- // Vertical clipping, move under edited line and to the side of insertion cursor
- if (coords[1] < 0) {
- coords[1] += height;
- final int lineBottom = mLayout.getLineBottom(line);
- final int lineHeight = lineBottom - lineTop;
- coords[1] += lineHeight;
+ @Override
+ protected int clipVertically(int positionY) {
+ if (positionY < 0) {
+ final int offset = getTextOffset();
+ final int line = mLayout.getLineForOffset(offset);
+ positionY += mLayout.getLineBottom(line) - mLayout.getLineTop(line);
+ positionY += mContentView.getMeasuredHeight();
// Assumes insertion and selection handles share the same height
final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
- coords[1] += handle.getIntrinsicHeight();
+ positionY += handle.getIntrinsicHeight();
}
- // Horizontal clipping
- coords[0] = Math.max(0, coords[0]);
- final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
- coords[0] = Math.min(screenWidth - width, coords[0]);
-
- mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
+ return positionY;
}
public void setShowWithPaste(boolean withPaste) {
@@ -9573,7 +9728,7 @@
}
}
- private abstract class HandleView extends View implements ViewTreeObserver.OnPreDrawListener {
+ private abstract class HandleView extends View implements TextViewPositionListener {
protected Drawable mDrawable;
private final PopupWindow mContainer;
// Position with respect to the parent TextView
@@ -9581,21 +9736,19 @@
private boolean mIsDragging;
// Offset from touch position to mPosition
private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
- protected float mHotspotX;
+ protected int mHotspotX;
// Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
private float mTouchOffsetY;
// Where the touch position should be on the handle to ensure a maximum cursor visibility
private float mIdealVerticalOffset;
// Parent's (TextView) previous position in window
private int mLastParentX, mLastParentY;
- // PopupWindow container absolute position with respect to the enclosing window
- private int mContainerPositionX, mContainerPositionY;
- // Visible or not (scrolled off screen), whether or not this handle should be visible
- private boolean mIsActive = false;
- // Used to detect that setFrame was called
- private boolean mNeedsUpdate = true;
// Transient action popup window for Paste and Replace actions
protected ActionPopupWindow mActionPopupWindow;
+ // Previous text character offset
+ private int mPreviousOffset = -1;
+ // Previous text character offset
+ private boolean mPositionHasChanged = true;
// Used to delay the appearance of the action popup window
private Runnable mActionPopupShower;
@@ -9615,15 +9768,6 @@
mIdealVerticalOffset = 0.7f * handleHeight;
}
- @Override
- protected boolean setFrame(int left, int top, int right, int bottom) {
- boolean changed = super.setFrame(left, top, right, bottom);
- // onPreDraw is called for PhoneWindow before the layout of this view is
- // performed. Make sure to update position, even if container didn't move.
- if (changed) mNeedsUpdate = true;
- return changed;
- }
-
protected abstract void initDrawable();
// Touch-up filter: number of previous positions remembered
@@ -9641,12 +9785,6 @@
}
private void addPositionToTouchUpFilter(int offset) {
- if (mNumberPreviousOffsets > 0 &&
- mPreviousOffsets[mPreviousOffsetIndex] == offset) {
- // Make sure only actual changes of position are recorded.
- return;
- }
-
mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
mPreviousOffsets[mPreviousOffsetIndex] = offset;
mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
@@ -9665,7 +9803,7 @@
if (i > 0 && i < iMax &&
(now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
- updateOffset(mPreviousOffsets[index]);
+ positionAtCursorOffset(mPreviousOffsets[index]);
}
}
@@ -9675,18 +9813,14 @@
}
public void show() {
- if (isShowing()) {
- mContainer.update(mContainerPositionX, mContainerPositionY, -1, -1);
- } else {
- mContainer.showAtLocation(TextView.this, 0,
- mContainerPositionX, mContainerPositionY);
+ if (isShowing()) return;
- if (!mIsActive) {
- ViewTreeObserver vto = TextView.this.getViewTreeObserver();
- vto.addOnPreDrawListener(this);
- mIsActive = true;
- }
- }
+ getPositionListener().addSubscriber(this, true);
+
+ // Make sure the offset is always considered new, even when focusing at same position
+ mPreviousOffset = -1;
+ positionAtCursorOffset(getCurrentCursorOffset());
+
hideActionPopupWindow();
}
@@ -9699,9 +9833,7 @@
public void hide() {
dismiss();
- ViewTreeObserver vto = TextView.this.getViewTreeObserver();
- vto.removeOnPreDrawListener(this);
- mIsActive = false;
+ TextView.this.getPositionListener().removeSubscriber(this);
}
void showActionPopupWindow(int delay, boolean withPaste) {
@@ -9734,7 +9866,7 @@
return mContainer.isShowing();
}
- private boolean isPositionVisible() {
+ private boolean isVisible() {
// Always show a dragging handle.
if (mIsDragging) {
return true;
@@ -9744,103 +9876,71 @@
return false;
}
- final int extendedPaddingTop = getExtendedPaddingTop();
- final int extendedPaddingBottom = getExtendedPaddingBottom();
- final int compoundPaddingLeft = getCompoundPaddingLeft();
- final int compoundPaddingRight = getCompoundPaddingRight();
-
- final TextView textView = TextView.this;
-
- if (mTempRect == null) mTempRect = new Rect();
- final Rect clip = mTempRect;
- clip.left = compoundPaddingLeft;
- clip.top = extendedPaddingTop;
- clip.right = textView.getWidth() - compoundPaddingRight;
- clip.bottom = textView.getHeight() - extendedPaddingBottom;
-
- final ViewParent parent = textView.getParent();
- if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) {
- return false;
- }
-
- final int[] coords = mTempCoords;
- textView.getLocationInWindow(coords);
- final int posX = coords[0] + mPositionX + (int) mHotspotX;
- final int posY = coords[1] + mPositionY;
-
- // Offset by 1 to take into account 0.5 and int rounding around getPrimaryHorizontal.
- return posX >= clip.left - 1 && posX <= clip.right + 1 &&
- posY >= clip.top && posY <= clip.bottom;
+ return getPositionListener().isVisible(mPositionX + mHotspotX, mPositionY);
}
public abstract int getCurrentCursorOffset();
- public abstract void updateOffset(int offset);
+ public abstract void updateSelection(int offset);
public abstract void updatePosition(float x, float y);
protected void positionAtCursorOffset(int offset) {
- // A HandleView relies on the layout, which may be nulled by external methods.
+ // A HandleView relies on the layout, which may be nulled by external methods
if (mLayout == null) {
// Will update controllers' state, hiding them and stopping selection mode if needed
prepareCursorControllers();
return;
}
- addPositionToTouchUpFilter(offset);
- final int line = mLayout.getLineForOffset(offset);
- final int lineBottom = mLayout.getLineBottom(line);
+ if (offset != mPreviousOffset) {
+ updateSelection(offset);
+ addPositionToTouchUpFilter(offset);
+ final int line = mLayout.getLineForOffset(offset);
- mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
- mPositionY = lineBottom;
+ mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
+ mPositionY = mLayout.getLineBottom(line);
- // Take TextView's padding into account.
- mPositionX += viewportToContentHorizontalOffset();
- mPositionY += viewportToContentVerticalOffset();
+ // Take TextView's padding into account.
+ mPositionX += viewportToContentHorizontalOffset();
+ mPositionY += viewportToContentVerticalOffset();
+
+ mPreviousOffset = offset;
+ mPositionHasChanged = true;
+ }
}
- private void checkForContainerPositionChange() {
- positionAtCursorOffset(getCurrentCursorOffset());
-
- final int previousContainerPositionX = mContainerPositionX;
- final int previousContainerPositionY = mContainerPositionY;
-
- TextView.this.getLocationInWindow(mTempCoords);
- mContainerPositionX = mTempCoords[0] + mPositionX;
- mContainerPositionY = mTempCoords[1] + mPositionY;
-
- mNeedsUpdate |= previousContainerPositionX != mContainerPositionX;
- mNeedsUpdate |= previousContainerPositionY != mContainerPositionY;
- }
-
- public boolean onPreDraw() {
- checkForContainerPositionChange();
- if (mNeedsUpdate) {
+ public void updatePosition(int parentPositionX, int parentPositionY, boolean modified) {
+ if (modified || mPositionHasChanged) {
if (mIsDragging) {
- if (mTempCoords[0] != mLastParentX || mTempCoords[1] != mLastParentY) {
- mTouchToWindowOffsetX += mTempCoords[0] - mLastParentX;
- mTouchToWindowOffsetY += mTempCoords[1] - mLastParentY;
- mLastParentX = mTempCoords[0];
- mLastParentY = mTempCoords[1];
+ // Update touchToWindow offset in case of parent scrolling while dragging
+ if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
+ mTouchToWindowOffsetX += parentPositionX - mLastParentX;
+ mTouchToWindowOffsetY += parentPositionY - mLastParentY;
+ mLastParentX = parentPositionX;
+ mLastParentY = parentPositionY;
}
onHandleMoved();
}
- if (isPositionVisible()) {
- mContainer.update(mContainerPositionX, mContainerPositionY, -1, -1);
-
- if (mIsActive && !isShowing()) {
- show();
+ if (isVisible()) {
+ final int positionX = parentPositionX + mPositionX;
+ final int positionY = parentPositionY + mPositionY;
+ if (isShowing()) {
+ mContainer.update(positionX, positionY, -1, -1);
+ } else {
+ mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
+ positionX, positionY);
}
} else {
if (isShowing()) {
dismiss();
}
}
- mNeedsUpdate = false;
+
+ mPositionHasChanged = false;
}
- return true;
}
@Override
@@ -9857,10 +9957,9 @@
mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
- final int[] coords = mTempCoords;
- TextView.this.getLocationInWindow(coords);
- mLastParentX = coords[0];
- mLastParentY = coords[1];
+ final PositionListener positionListener = getPositionListener();
+ mLastParentX = positionListener.getPositionX();
+ mLastParentY = positionListener.getPositionY();
mIsDragging = true;
break;
}
@@ -9963,7 +10062,7 @@
mTextSelectHandleRes);
}
mDrawable = mSelectHandleCenter;
- mHotspotX = mDrawable.getIntrinsicWidth() / 2.0f;
+ mHotspotX = mDrawable.getIntrinsicWidth() / 2;
}
@Override
@@ -10008,13 +10107,13 @@
}
@Override
- public void updateOffset(int offset) {
+ public void updateSelection(int offset) {
Selection.setSelection((Spannable) mText, offset);
}
@Override
public void updatePosition(float x, float y) {
- updateOffset(getOffsetForPosition(x, y));
+ positionAtCursorOffset(getOffsetForPosition(x, y));
}
@Override
@@ -10038,7 +10137,7 @@
mTextSelectHandleLeftRes);
}
mDrawable = mSelectHandleLeft;
- mHotspotX = mDrawable.getIntrinsicWidth() * 3.0f / 4.0f;
+ mHotspotX = (mDrawable.getIntrinsicWidth() * 3) / 4;
}
@Override
@@ -10047,7 +10146,7 @@
}
@Override
- public void updateOffset(int offset) {
+ public void updateSelection(int offset) {
Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
}
@@ -10063,7 +10162,7 @@
// Handles can not cross and selection is at least one character
if (offset >= selectionEnd) offset = selectionEnd - 1;
- Selection.setSelection((Spannable) mText, offset, selectionEnd);
+ positionAtCursorOffset(offset);
}
public ActionPopupWindow getActionPopupWindow() {
@@ -10079,7 +10178,7 @@
mTextSelectHandleRightRes);
}
mDrawable = mSelectHandleRight;
- mHotspotX = mDrawable.getIntrinsicWidth() / 4.0f;
+ mHotspotX = mDrawable.getIntrinsicWidth() / 4;
}
@Override
@@ -10088,7 +10187,7 @@
}
@Override
- public void updateOffset(int offset) {
+ public void updateSelection(int offset) {
Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
}
@@ -10104,7 +10203,7 @@
// Handles can not cross and selection is at least one character
if (offset <= selectionStart) offset = selectionStart + 1;
- Selection.setSelection((Spannable) mText, selectionStart, offset);
+ positionAtCursorOffset(offset);
}
public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
diff --git a/core/java/com/android/internal/view/menu/MenuBuilder.java b/core/java/com/android/internal/view/menu/MenuBuilder.java
index 7839a08..5e70e4c 100644
--- a/core/java/com/android/internal/view/menu/MenuBuilder.java
+++ b/core/java/com/android/internal/view/menu/MenuBuilder.java
@@ -150,6 +150,11 @@
private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters =
new CopyOnWriteArrayList<WeakReference<MenuPresenter>>();
+
+ /**
+ * Currently expanded menu item; must be collapsed when we clear.
+ */
+ private MenuItemImpl mExpandedItem;
/**
* Called by menu to notify of close and selection changes.
@@ -512,6 +517,9 @@
}
public void clear() {
+ if (mExpandedItem != null) {
+ collapseItemActionView(mExpandedItem);
+ }
mItems.clear();
onItemsChanged(true);
@@ -1223,11 +1231,14 @@
}
startDispatchingItemsChanged();
+ if (expanded) {
+ mExpandedItem = item;
+ }
return expanded;
}
public boolean collapseItemActionView(MenuItemImpl item) {
- if (mPresenters.isEmpty()) return false;
+ if (mPresenters.isEmpty() || mExpandedItem != item) return false;
boolean collapsed = false;
@@ -1242,6 +1253,9 @@
}
startDispatchingItemsChanged();
+ if (collapsed) {
+ mExpandedItem = null;
+ }
return collapsed;
}
}
diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java
index 446c842..4878b0f 100644
--- a/core/java/com/android/internal/widget/ActionBarView.java
+++ b/core/java/com/android/internal/widget/ActionBarView.java
@@ -1265,9 +1265,8 @@
@Override
public void initForMenu(Context context, MenuBuilder menu) {
// Clear the expanded action view when menus change.
- mExpandedActionView = null;
- if (mCurrentExpandedItem != null) {
- mCurrentExpandedItem.collapseActionView();
+ if (mMenu != null && mCurrentExpandedItem != null) {
+ mMenu.collapseItemActionView(mCurrentExpandedItem);
}
mMenu = menu;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f99a94c..21c3f1e 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -294,12 +294,21 @@
<!-- Allows an application to read/write the voicemails owned by its own
package. -->
+ <!-- TODO: delete this permission when dependent content provider &
+ application code has been migrated to use ADD_VOICEMAIL instead -->
<permission android:name="com.android.voicemail.permission.READ_WRITE_OWN_VOICEMAIL"
android:permissionGroup="android.permission-group.PERSONAL_INFO"
android:protectionLevel="dangerous"
android:label="@string/permlab_readWriteOwnVoicemail"
android:description="@string/permdesc_readWriteOwnVoicemail" />
+ <!-- Allows an application to add voicemails into the system. -->
+ <permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"
+ android:permissionGroup="android.permission-group.PERSONAL_INFO"
+ android:protectionLevel="dangerous"
+ android:label="@string/permlab_addVoicemail"
+ android:description="@string/permdesc_addVoicemail" />
+
<!-- ======================================= -->
<!-- Permissions for accessing location info -->
<!-- ======================================= -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index c5aa4b2..a6c92f2 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2166,6 +2166,14 @@
voicemails that its associated service can access.</string>
<!-- Title of an application permission, listed so the user can choose whether
+ they want to allow the application to do this. [CHAR LIMIT=NONE] -->
+ <string name="permlab_addVoicemail">add voicemail</string>
+ <!-- Description of an application permission, listed so the user can choose whether
+ they want to allow the application to do this. [CHAR LIMIT=NONE] -->
+ <string name="permdesc_addVoicemail">Allows the application to add messages
+ to your voicemail inbox.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether
they want to allow the application to do this. -->
<string name="permlab_writeGeolocationPermissions">Modify Browser geolocation permissions</string>
<!-- Description of an application permission, listed so the user can choose whether
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 7258e11..731d1f3 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1646,7 +1646,8 @@
IAudioService service = getService();
try {
status = service.requestAudioFocus(streamType, durationHint, mICallBack,
- mAudioFocusDispatcher, getIdForAudioFocusListener(l));
+ mAudioFocusDispatcher, getIdForAudioFocusListener(l),
+ mContext.getPackageName() /* package name */);
} catch (RemoteException e) {
Log.e(TAG, "Can't call requestAudioFocus() from AudioService due to "+e);
}
@@ -1682,7 +1683,9 @@
* in the application manifest.
*/
public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
- //TODO enforce the rule about the receiver being declared in the manifest
+ if (eventReceiver == null) {
+ return;
+ }
IAudioService service = getService();
try {
service.registerMediaButtonEventReceiver(eventReceiver);
@@ -1697,6 +1700,9 @@
* that was registered with {@link #registerMediaButtonEventReceiver(ComponentName)}.
*/
public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) {
+ if (eventReceiver == null) {
+ return;
+ }
IAudioService service = getService();
try {
service.unregisterMediaButtonEventReceiver(eventReceiver);
@@ -1706,6 +1712,126 @@
}
/**
+ * @hide
+ * Registers the remote control client for providing information to display on the remotes.
+ * @param eventReceiver identifier of a {@link android.content.BroadcastReceiver}
+ * that will receive the media button intent, and associated with the remote control
+ * client. This method has no effect if
+ * {@link #registerMediaButtonEventReceiver(ComponentName)} hasn't been called
+ * with the same eventReceiver, or if
+ * {@link #unregisterMediaButtonEventReceiver(ComponentName)} has been called.
+ * @param rcClient the client associated with the event receiver, responsible for providing
+ * the information to display on the remote control.
+ */
+ public void registerRemoteControlClient(ComponentName eventReceiver,
+ IRemoteControlClient rcClient) {
+ if (eventReceiver == null) {
+ return;
+ }
+ IAudioService service = getService();
+ try {
+ service.registerRemoteControlClient(eventReceiver, rcClient,
+ // used to match media button event receiver and audio focus
+ mContext.getPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in registerRemoteControlClient"+e);
+ }
+ }
+
+ /**
+ * @hide
+ * @param eventReceiver
+ */
+ public void unregisterRemoteControlClient(ComponentName eventReceiver) {
+ if (eventReceiver == null) {
+ return;
+ }
+ IAudioService service = getService();
+ try {
+ // unregistering a IRemoteControlClient is equivalent to setting it to null
+ service.registerRemoteControlClient(eventReceiver, null, mContext.getPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in unregisterRemoteControlClient"+e);
+ }
+ }
+
+ /**
+ * @hide
+ * Definitions of constants to be used in {@link android.media.IRemoteControlClient}.
+ */
+ public final class RemoteControlParameters {
+ public final static int PLAYSTATE_STOPPED = 1;
+ public final static int PLAYSTATE_PAUSED = 2;
+ public final static int PLAYSTATE_PLAYING = 3;
+ public final static int PLAYSTATE_FAST_FORWARDING = 4;
+ public final static int PLAYSTATE_REWINDING = 5;
+ public final static int PLAYSTATE_SKIPPING_FORWARDS = 6;
+ public final static int PLAYSTATE_SKIPPING_BACKWARDS = 7;
+ public final static int PLAYSTATE_BUFFERING = 8;
+
+ public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0;
+ public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1;
+ public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2;
+ public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3;
+ public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4;
+ public final static int FLAG_KEY_MEDIA_STOP = 1 << 5;
+ public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6;
+ public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7;
+ }
+
+ /**
+ * @hide
+ * Broadcast intent action indicating that the displays on the remote controls
+ * should be updated because a new remote control client is now active. If there is no
+ * {@link #EXTRA_REMOTE_CONTROL_CLIENT}, the remote control display should be cleared
+ * because there is no valid client to supply it with information.
+ *
+ * @see #EXTRA_REMOTE_CONTROL_CLIENT
+ */
+ public static final String REMOTE_CONTROL_CLIENT_CHANGED =
+ "android.media.REMOTE_CONTROL_CLIENT_CHANGED";
+
+ /**
+ * @hide
+ * The IRemoteControlClient monotonically increasing generation counter.
+ *
+ * @see #REMOTE_CONTROL_CLIENT_CHANGED_ACTION
+ */
+ public static final String EXTRA_REMOTE_CONTROL_CLIENT =
+ "android.media.EXTRA_REMOTE_CONTROL_CLIENT";
+
+ /**
+ * @hide
+ * FIXME to be changed to address Neel's comments
+ * Force a refresh of the remote control client associated with the event receiver.
+ * @param eventReceiver
+ */
+ public void refreshRemoteControlDisplay(ComponentName eventReceiver) {
+ IAudioService service = getService();
+ try {
+ service.refreshRemoteControlDisplay(eventReceiver);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in refreshRemoteControlDisplay"+e);
+ }
+ }
+
+ /**
+ * @hide
+ * FIXME API to be used by implementors of remote controls, not a candidate for SDK
+ */
+ public void registerRemoteControlObserver() {
+
+ }
+
+ /**
+ * @hide
+ * FIXME API to be used by implementors of remote controls, not a candidate for SDK
+ */
+ public void unregisterRemoteControlObserver() {
+
+ }
+
+ /**
* @hide
* Reload audio settings. This method is called by Settings backup
* agent when audio settings are restored and causes the AudioService
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index 682560a..cd55b0e 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -55,6 +55,7 @@
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
+import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
@@ -113,6 +114,8 @@
private static final int MSG_SET_FORCE_USE = 10;
private static final int MSG_PERSIST_MEDIABUTTONRECEIVER = 11;
private static final int MSG_BT_HEADSET_CNCT_FAILED = 12;
+ private static final int MSG_RCDISPLAY_CLEAR = 13;
+ private static final int MSG_RCDISPLAY_UPDATE = 14;
private static final int BTA2DP_DOCK_TIMEOUT_MILLIS = 8000;
// Timeout for connection to bluetooth headset service
@@ -184,7 +187,7 @@
AudioSystem.STREAM_RING, // STREAM_RING
AudioSystem.STREAM_MUSIC, // STREAM_MUSIC
AudioSystem.STREAM_ALARM, // STREAM_ALARM
- AudioSystem.STREAM_NOTIFICATION, // STREAM_NOTIFICATION
+ AudioSystem.STREAM_RING, // STREAM_NOTIFICATION
AudioSystem.STREAM_BLUETOOTH_SCO, // STREAM_BLUETOOTH_SCO
AudioSystem.STREAM_SYSTEM, // STREAM_SYSTEM_ENFORCED
AudioSystem.STREAM_VOICE_CALL, // STREAM_DTMF
@@ -239,9 +242,6 @@
*/
private int mVibrateSetting;
- /** @see System#NOTIFICATIONS_USE_RING_VOLUME */
- private int mNotificationsUseRingVolume;
-
// Broadcast receiver for device connections intent broadcasts
private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
@@ -371,7 +371,9 @@
// Register for media button intent broadcasts.
intentFilter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
- intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ // Workaround for bug on priority setting
+ //intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ intentFilter.setPriority(Integer.MAX_VALUE);
context.registerReceiver(mMediaButtonReceiver, intentFilter);
// Register for phone state monitoring
@@ -451,16 +453,6 @@
System.MUTE_STREAMS_AFFECTED,
((1 << AudioSystem.STREAM_MUSIC)|(1 << AudioSystem.STREAM_RING)|(1 << AudioSystem.STREAM_SYSTEM)));
- if (mVoiceCapable) {
- mNotificationsUseRingVolume = System.getInt(cr,
- Settings.System.NOTIFICATIONS_USE_RING_VOLUME, 1);
- } else {
- mNotificationsUseRingVolume = 1;
- }
-
- if (mNotificationsUseRingVolume == 1) {
- STREAM_VOLUME_ALIAS[AudioSystem.STREAM_NOTIFICATION] = AudioSystem.STREAM_RING;
- }
// Each stream will read its own persisted settings
// Broadcast the sticky intent
@@ -885,7 +877,8 @@
requestAudioFocus(AudioManager.STREAM_RING,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, cb,
null /* IAudioFocusDispatcher allowed to be null only for this clientId */,
- IN_VOICE_COMM_FOCUS_ID /*clientId*/);
+ IN_VOICE_COMM_FOCUS_ID /*clientId*/,
+ "system");
}
}
@@ -897,7 +890,8 @@
requestAudioFocus(AudioManager.STREAM_RING,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, cb,
null /* IAudioFocusDispatcher allowed to be null only for this clientId */,
- IN_VOICE_COMM_FOCUS_ID /*clientId*/);
+ IN_VOICE_COMM_FOCUS_ID /*clientId*/,
+ "system");
}
// if exiting call
else if (newMode == AudioSystem.MODE_NORMAL) {
@@ -2155,6 +2149,33 @@
persistMediaButtonReceiver( (ComponentName) msg.obj );
break;
+ case MSG_RCDISPLAY_CLEAR:
+ Log.i(TAG, "Clear remote control display");
+ Intent clearIntent = new Intent(AudioManager.REMOTE_CONTROL_CLIENT_CHANGED);
+ // no extra means no IRemoteControlClient, which is a request to clear
+ clearIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ mContext.sendBroadcast(clearIntent);
+ break;
+
+ case MSG_RCDISPLAY_UPDATE:
+ synchronized(mCurrentRcLock) {
+ if (mCurrentRcClientRef.get() == null) {
+ // the remote control display owner has changed between the
+ // the message to update the display was sent, and the time it
+ // gets to be processed (now)
+ } else {
+ mCurrentRcClientGen++;
+ Log.i(TAG, "Display/update remote control ");
+ Intent rcClientIntent = new Intent(
+ AudioManager.REMOTE_CONTROL_CLIENT_CHANGED);
+ rcClientIntent.putExtra(AudioManager.EXTRA_REMOTE_CONTROL_CLIENT,
+ mCurrentRcClientGen);
+ rcClientIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ mContext.sendBroadcast(rcClientIntent);
+ }
+ }
+ break;
+
case MSG_BT_HEADSET_CNCT_FAILED:
resetBluetoothSco();
break;
@@ -2168,8 +2189,6 @@
super(new Handler());
mContentResolver.registerContentObserver(Settings.System.getUriFor(
Settings.System.MODE_RINGER_STREAMS_AFFECTED), false, this);
- mContentResolver.registerContentObserver(Settings.System.getUriFor(
- Settings.System.NOTIFICATIONS_USE_RING_VOLUME), false, this);
}
@Override
@@ -2193,29 +2212,6 @@
mRingerModeAffectedStreams = ringerModeAffectedStreams;
setRingerModeInt(getRingerMode(), false);
}
-
- int notificationsUseRingVolume = Settings.System.getInt(mContentResolver,
- Settings.System.NOTIFICATIONS_USE_RING_VOLUME,
- 1);
- if (mVoiceCapable) {
- if (notificationsUseRingVolume != mNotificationsUseRingVolume) {
- mNotificationsUseRingVolume = notificationsUseRingVolume;
- if (mNotificationsUseRingVolume == 1) {
- STREAM_VOLUME_ALIAS[AudioSystem.STREAM_NOTIFICATION] = AudioSystem.STREAM_RING;
- mStreamStates[AudioSystem.STREAM_NOTIFICATION].setVolumeIndexSettingName(
- System.VOLUME_SETTINGS[AudioSystem.STREAM_RING]);
- } else {
- STREAM_VOLUME_ALIAS[AudioSystem.STREAM_NOTIFICATION] = AudioSystem.STREAM_NOTIFICATION;
- mStreamStates[AudioSystem.STREAM_NOTIFICATION].setVolumeIndexSettingName(
- System.VOLUME_SETTINGS[AudioSystem.STREAM_NOTIFICATION]);
- // Persist notification volume volume as it was not persisted while aliased to ring volume
- // and persist with no delay as there might be registered observers of the persisted
- // notification volume.
- sendMsg(mAudioHandler, MSG_PERSIST_VOLUME, AudioSystem.STREAM_NOTIFICATION,
- SENDMSG_REPLACE, 1, 1, mStreamStates[AudioSystem.STREAM_NOTIFICATION], 0);
- }
- }
- }
}
}
}
@@ -2567,23 +2563,25 @@
private static class FocusStackEntry {
public int mStreamType = -1;// no stream type
- public boolean mIsTransportControlReceiver = false;
public IAudioFocusDispatcher mFocusDispatcher = null;
public IBinder mSourceRef = null;
public String mClientId;
public int mFocusChangeType;
+ public String mPackageName;
+ public int mCallingUid;
public FocusStackEntry() {
}
- public FocusStackEntry(int streamType, int duration, boolean isTransportControlReceiver,
- IAudioFocusDispatcher afl, IBinder source, String id) {
+ public FocusStackEntry(int streamType, int duration,
+ IAudioFocusDispatcher afl, IBinder source, String id, String pn, int uid) {
mStreamType = streamType;
- mIsTransportControlReceiver = isTransportControlReceiver;
mFocusDispatcher = afl;
mSourceRef = source;
mClientId = id;
mFocusChangeType = duration;
+ mPackageName = pn;
+ mCallingUid = uid;
}
}
@@ -2600,13 +2598,15 @@
while(stackIterator.hasNext()) {
FocusStackEntry fse = stackIterator.next();
pw.println(" source:" + fse.mSourceRef + " -- client: " + fse.mClientId
- + " -- duration: " +fse.mFocusChangeType);
+ + " -- duration: " + fse.mFocusChangeType
+ + " -- uid: " + fse.mCallingUid);
}
}
}
/**
* Helper function:
+ * Called synchronized on mAudioFocusLock
* Remove a focus listener from the focus stack.
* @param focusListenerToRemove the focus listener
* @param signal if true and the listener was at the top of the focus stack, i.e. it was holding
@@ -2621,6 +2621,10 @@
if (signal) {
// notify the new top of the stack it gained focus
notifyTopOfAudioFocusStack();
+ // there's a new top of the stack, let the remote control know
+ synchronized(mRCStack) {
+ checkUpdateRemoteControlDisplay();
+ }
}
} else {
// focus is abandoned by a client that's not at the top of the stack,
@@ -2639,6 +2643,7 @@
/**
* Helper function:
+ * Called synchronized on mAudioFocusLock
* Remove focus listeners from the focus stack for a particular client.
*/
private void removeFocusStackEntryForClient(IBinder cb) {
@@ -2658,6 +2663,10 @@
// we removed an entry at the top of the stack:
// notify the new top of the stack it gained focus.
notifyTopOfAudioFocusStack();
+ // there's a new top of the stack, let the remote control know
+ synchronized(mRCStack) {
+ checkUpdateRemoteControlDisplay();
+ }
}
}
@@ -2700,7 +2709,7 @@
/** @see AudioManager#requestAudioFocus(IAudioFocusDispatcher, int, int) */
public int requestAudioFocus(int mainStreamType, int focusChangeHint, IBinder cb,
- IAudioFocusDispatcher fd, String clientId) {
+ IAudioFocusDispatcher fd, String clientId, String callingPackageName) {
Log.i(TAG, " AudioFocus requestAudioFocus() from " + clientId);
// the main stream type for the audio focus request is currently not used. It may
// potentially be used to handle multiple stream type-dependent audio focuses.
@@ -2743,8 +2752,13 @@
removeFocusStackEntry(clientId, false);
// push focus requester at the top of the audio focus stack
- mFocusStack.push(new FocusStackEntry(mainStreamType, focusChangeHint, false, fd, cb,
- clientId));
+ mFocusStack.push(new FocusStackEntry(mainStreamType, focusChangeHint, fd, cb,
+ clientId, callingPackageName, Binder.getCallingUid()));
+
+ // there's a new top of the stack, let the remote control know
+ synchronized(mRCStack) {
+ checkUpdateRemoteControlDisplay();
+ }
}//synchronized(mAudioFocusLock)
// handle the potential premature death of the new holder of the focus
@@ -2831,19 +2845,100 @@
}
}
- private static class RemoteControlStackEntry {
- public ComponentName mReceiverComponent;// always non null
- // TODO implement registration expiration?
- //public int mRegistrationTime;
+ private final static Object mCurrentRcLock = new Object();
+ /**
+ * The one remote control client to be polled for display information.
+ * This object is never null, but its reference might.
+ * Access protected by mCurrentRcLock.
+ */
+ private static SoftReference<IRemoteControlClient> mCurrentRcClientRef =
+ new SoftReference<IRemoteControlClient>(null);
- public RemoteControlStackEntry() {
- }
+ /**
+ * A monotonically increasing generation counter for mCurrentRcClientRef.
+ * Only accessed with a lock on mCurrentRcLock.
+ */
+ private static int mCurrentRcClientGen = 0;
- public RemoteControlStackEntry(ComponentName r) {
- mReceiverComponent = r;
+ /**
+ * Returns the current remote control client.
+ * @param rcClientId the counter value that matches the extra
+ * {@link AudioManager#EXTRA_REMOTE_CONTROL_CLIENT} in the
+ * {@link AudioManager#REMOTE_CONTROL_CLIENT_CHANGED} event
+ * @return the current IRemoteControlClient from which information to display on the remote
+ * control can be retrieved, or null if rcClientId doesn't match the current generation
+ * counter.
+ */
+ public static IRemoteControlClient getRemoteControlClient(int rcClientId) {
+ synchronized(mCurrentRcLock) {
+ if (rcClientId == mCurrentRcClientGen) {
+ return mCurrentRcClientRef.get();
+ } else {
+ return null;
+ }
}
}
+ /**
+ * Inner class to monitor remote control client deaths, and remove the client for the
+ * remote control stack if necessary.
+ */
+ private class RcClientDeathHandler implements IBinder.DeathRecipient {
+ private IBinder mCb; // To be notified of client's death
+ private ComponentName mRcEventReceiver;
+
+ RcClientDeathHandler(IBinder cb, ComponentName eventReceiver) {
+ mCb = cb;
+ mRcEventReceiver = eventReceiver;
+ }
+
+ public void binderDied() {
+ Log.w(TAG, " RemoteControlClient died");
+ // remote control client died, make sure the displays don't use it anymore
+ // by setting its remote control client to null
+ registerRemoteControlClient(mRcEventReceiver, null, null/*ignored*/);
+ }
+
+ public IBinder getBinder() {
+ return mCb;
+ }
+ }
+
+ private static class RemoteControlStackEntry {
+ /** the target for the ACTION_MEDIA_BUTTON events */
+ public ComponentName mReceiverComponent;// always non null
+ public String mCallingPackageName;
+ public int mCallingUid;
+
+ /** provides access to the information to display on the remote control */
+ public SoftReference<IRemoteControlClient> mRcClientRef;
+ public RcClientDeathHandler mRcClientDeathHandler;
+
+ public RemoteControlStackEntry(ComponentName r) {
+ mReceiverComponent = r;
+ mCallingUid = -1;
+ mRcClientRef = new SoftReference<IRemoteControlClient>(null);
+ }
+
+ public void unlinkToRcClientDeath() {
+ if ((mRcClientDeathHandler != null) && (mRcClientDeathHandler.mCb != null)) {
+ try {
+ mRcClientDeathHandler.mCb.unlinkToDeath(mRcClientDeathHandler, 0);
+ } catch (java.util.NoSuchElementException e) {
+ // not much we can do here
+ Log.e(TAG, "Encountered " + e + " in unlinkToRcClientDeath()");
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * The stack of remote control event receivers.
+ * Code sections and methods that modify the remote control event receiver stack are
+ * synchronized on mRCStack, but also BEFORE on mFocusLock as any change in either
+ * stack, audio focus or RC, can lead to a change in the remote control display
+ */
private Stack<RemoteControlStackEntry> mRCStack = new Stack<RemoteControlStackEntry>();
/**
@@ -2855,8 +2950,10 @@
synchronized(mRCStack) {
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
while(stackIterator.hasNext()) {
- RemoteControlStackEntry fse = stackIterator.next();
- pw.println(" receiver:" + fse.mReceiverComponent);
+ RemoteControlStackEntry rcse = stackIterator.next();
+ pw.println(" receiver: " + rcse.mReceiverComponent +
+ " -- client: " + rcse.mRcClientRef.get() +
+ " -- uid: " + rcse.mCallingUid);
}
}
}
@@ -2909,6 +3006,7 @@
ComponentName receiverComponentName = ComponentName.unflattenFromString(receiverName);
registerMediaButtonEventReceiver(receiverComponentName);
}
+ // upon restoring (e.g. after boot), do we want to refresh all remotes?
}
/**
@@ -2921,14 +3019,20 @@
return;
}
Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ RemoteControlStackEntry rcse = null;
+ boolean wasInsideStack = false;
while(stackIterator.hasNext()) {
- RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
+ rcse = (RemoteControlStackEntry)stackIterator.next();
if(rcse.mReceiverComponent.equals(newReceiver)) {
+ wasInsideStack = true;
stackIterator.remove();
break;
}
}
- mRCStack.push(new RemoteControlStackEntry(newReceiver));
+ if (!wasInsideStack) {
+ rcse = new RemoteControlStackEntry(newReceiver);
+ }
+ mRCStack.push(rcse);
// post message to persist the default media button receiver
mAudioHandler.sendMessage( mAudioHandler.obtainMessage(
@@ -2950,13 +3054,88 @@
}
}
+ /**
+ * Helper function:
+ * Called synchronized on mRCStack
+ */
+ private boolean isCurrentRcController(ComponentName eventReceiver) {
+ if (!mRCStack.empty() && mRCStack.peek().mReceiverComponent.equals(eventReceiver)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Helper function:
+ * Called synchronized on mRCStack
+ */
+ private void clearRemoteControlDisplay() {
+ synchronized(mCurrentRcLock) {
+ mCurrentRcClientRef.clear();
+ }
+ mAudioHandler.sendMessage( mAudioHandler.obtainMessage(MSG_RCDISPLAY_CLEAR) );
+ }
+
+ /**
+ * Helper function:
+ * Called synchronized on mRCStack
+ * mRCStack.empty() is false
+ */
+ private void updateRemoteControlDisplay() {
+ RemoteControlStackEntry rcse = mRCStack.peek();
+ // this is where we enforce opt-in for information display on the remote controls
+ // with the new AudioManager.registerRemoteControlClient() API
+ if (rcse.mRcClientRef.get() == null) {
+ // FIXME remove log before release: this warning will be displayed for every AF change
+ Log.w(TAG, "Can't update remote control display with null remote control client");
+ clearRemoteControlDisplay();
+ return;
+ }
+ synchronized(mCurrentRcLock) {
+ mCurrentRcClientRef = rcse.mRcClientRef;
+ }
+ mAudioHandler.sendMessage( mAudioHandler.obtainMessage(MSG_RCDISPLAY_UPDATE, 0, 0, rcse) );
+ }
+
+ /**
+ * Helper function:
+ * Called synchronized on mFocusLock, then mRCStack
+ * Check whether the remote control display should be updated, triggers the update if required
+ */
+ private void checkUpdateRemoteControlDisplay() {
+ // determine whether the remote control display should be refreshed
+ // if either stack is empty, there is a mismatch, so clear the RC display
+ if (mRCStack.isEmpty() || mFocusStack.isEmpty()) {
+ clearRemoteControlDisplay();
+ return;
+ }
+ // if the top of the two stacks belong to different packages, there is a mismatch, clear
+ if ((mRCStack.peek().mCallingPackageName != null)
+ && (mFocusStack.peek().mPackageName != null)
+ && !(mRCStack.peek().mCallingPackageName.compareTo(
+ mFocusStack.peek().mPackageName) == 0)) {
+ clearRemoteControlDisplay();
+ return;
+ }
+ // if the audio focus didn't originate from the same Uid as the one in which the remote
+ // control information will be retrieved, clear
+ if (mRCStack.peek().mCallingUid != mFocusStack.peek().mCallingUid) {
+ clearRemoteControlDisplay();
+ return;
+ }
+ // refresh conditions were verified: update the remote controls
+ updateRemoteControlDisplay();
+ }
/** see AudioManager.registerMediaButtonEventReceiver(ComponentName eventReceiver) */
public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
Log.i(TAG, " Remote Control registerMediaButtonEventReceiver() for " + eventReceiver);
- synchronized(mRCStack) {
- pushMediaButtonReceiver(eventReceiver);
+ synchronized(mAudioFocusLock) {
+ synchronized(mRCStack) {
+ pushMediaButtonReceiver(eventReceiver);
+ checkUpdateRemoteControlDisplay();
+ }
}
}
@@ -2964,11 +3143,74 @@
public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) {
Log.i(TAG, " Remote Control unregisterMediaButtonEventReceiver() for " + eventReceiver);
- synchronized(mRCStack) {
- removeMediaButtonReceiver(eventReceiver);
+ synchronized(mAudioFocusLock) {
+ synchronized(mRCStack) {
+ boolean topOfStackWillChange = isCurrentRcController(eventReceiver);
+ removeMediaButtonReceiver(eventReceiver);
+ if (topOfStackWillChange) {
+ checkUpdateRemoteControlDisplay();
+ }
+ }
}
}
+ /** see AudioManager.registerRemoteControlClient(ComponentName eventReceiver, ...) */
+ public void registerRemoteControlClient(ComponentName eventReceiver,
+ IRemoteControlClient rcClient, String callingPackageName) {
+ synchronized(mAudioFocusLock) {
+ synchronized(mRCStack) {
+ // store the new display information
+ Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+ while(stackIterator.hasNext()) {
+ RemoteControlStackEntry rcse = stackIterator.next();
+ if(rcse.mReceiverComponent.equals(eventReceiver)) {
+ // already had a remote control client?
+ if (rcse.mRcClientDeathHandler != null) {
+ // stop monitoring the old client's death
+ rcse.unlinkToRcClientDeath();
+ }
+ // save the new remote control client
+ rcse.mRcClientRef = new SoftReference<IRemoteControlClient>(rcClient);
+ rcse.mCallingPackageName = callingPackageName;
+ rcse.mCallingUid = Binder.getCallingUid();
+ if (rcClient == null) {
+ break;
+ }
+ // monitor the new client's death
+ IBinder b = rcClient.asBinder();
+ RcClientDeathHandler rcdh =
+ new RcClientDeathHandler(b, rcse.mReceiverComponent);
+ try {
+ b.linkToDeath(rcdh, 0);
+ } catch (RemoteException e) {
+ // remote control client is DOA, disqualify it
+ Log.w(TAG, "registerRemoteControlClient() has a dead client " + b);
+ rcse.mRcClientRef.clear();
+ }
+ rcse.mRcClientDeathHandler = rcdh;
+ break;
+ }
+ }
+ // if the eventReceiver is at the top of the stack
+ // then check for potential refresh of the remote controls
+ if (isCurrentRcController(eventReceiver)) {
+ checkUpdateRemoteControlDisplay();
+ }
+ }
+ }
+ }
+
+ /** see AudioManager.refreshRemoteControlDisplay(ComponentName er) */
+ public void refreshRemoteControlDisplay(ComponentName eventReceiver) {
+ synchronized(mAudioFocusLock) {
+ synchronized(mRCStack) {
+ // only refresh if the eventReceiver is at the top of the stack
+ if (isCurrentRcController(eventReceiver)) {
+ checkUpdateRemoteControlDisplay();
+ }
+ }
+ }
+ }
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index e3bd7b4..1a05f152 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -18,6 +18,9 @@
import android.content.ComponentName;
import android.media.IAudioFocusDispatcher;
+import android.media.IRemoteControlClient;
+import android.net.Uri;
+import android.os.Bundle;
/**
* {@hide}
@@ -77,7 +80,7 @@
boolean isBluetoothScoOn();
int requestAudioFocus(int mainStreamType, int durationHint, IBinder cb, IAudioFocusDispatcher l,
- String clientId);
+ String clientId, String callingPackageName);
int abandonAudioFocus(IAudioFocusDispatcher l, String clientId);
@@ -87,6 +90,11 @@
void unregisterMediaButtonEventReceiver(in ComponentName eventReceiver);
+ void registerRemoteControlClient(in ComponentName eventReceiver,
+ in IRemoteControlClient rcClient, in String callingPackageName);
+
+ void refreshRemoteControlDisplay(in ComponentName eventReceiver);
+
void startBluetoothSco(IBinder cb);
void stopBluetoothSco(IBinder cb);
diff --git a/media/java/android/media/IRemoteControlClient.aidl b/media/java/android/media/IRemoteControlClient.aidl
new file mode 100644
index 0000000..a49371c
--- /dev/null
+++ b/media/java/android/media/IRemoteControlClient.aidl
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2011 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.graphics.Bitmap;
+
+/**
+ * {@hide}
+ */
+interface IRemoteControlClient
+{
+ /**
+ * Called by a remote control to retrieve a String of information to display.
+ * @param field the identifier for a metadata field to retrieve. Valid values are
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUM},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUMARTIST},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ARTIST},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_AUTHOR},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_CD_TRACK_NUMBER},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPILATION},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPOSER},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DATE},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DISC_NUMBER},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DURATION},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_GENRE},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_WRITER},
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_YEAR}.
+ * @return null if the given field is not supported, or the String matching the metadata field.
+ */
+ String getMetadataString(int field);
+
+ /**
+ * Returns the current playback state.
+ * @return one of the following values:
+ * {@link android.media.AudioManager.RemoteControl#PLAYSTATE_STOPPED},
+ * {@link android.media.AudioManager.RemoteControl#PLAYSTATE_PAUSED},
+ * {@link android.media.AudioManager.RemoteControl#PLAYSTATE_PLAYING},
+ * {@link android.media.AudioManager.RemoteControl#PLAYSTATE_FAST_FORWARDING},
+ * {@link android.media.AudioManager.RemoteControl#PLAYSTATE_REWINDING},
+ * {@link android.media.AudioManager.RemoteControl#PLAYSTATE_SKIPPING_FORWARDS},
+ * {@link android.media.AudioManager.RemoteControl#PLAYSTATE_SKIPPING_BACKWARDS},
+ * {@link android.media.AudioManager.RemoteControl#PLAYSTATE_BUFFERING}.
+ */
+ int getPlaybackState();
+
+ /**
+ * Returns the flags for the media transport control buttons this client supports.
+ * @see {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_PREVIOUS},
+ * {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_REWIND},
+ * {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_PLAY},
+ * {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_PLAY_PAUSE},
+ * {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_PAUSE},
+ * {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_STOP},
+ * {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_FAST_FORWARD},
+ * {@link android.media.AudioManager.RemoteControl#FLAG_KEY_MEDIA_NEXT}
+ */
+ int getTransportControlFlags();
+
+ Bitmap getAlbumArt(int width, int height);
+}
diff --git a/services/audioflinger/AudioFlinger.cpp b/services/audioflinger/AudioFlinger.cpp
index ec45530..2355d5c 100644
--- a/services/audioflinger/AudioFlinger.cpp
+++ b/services/audioflinger/AudioFlinger.cpp
@@ -51,6 +51,8 @@
#include <media/EffectsFactoryApi.h>
#include <audio_effects/effect_visualizer.h>
+#include <audio_effects/effect_ns.h>
+#include <audio_effects/effect_aec.h>
#include <cpustats/ThreadCpuUsage.h>
#include <powermanager/PowerManager.h>
@@ -148,7 +150,8 @@
AudioFlinger::AudioFlinger()
: BnAudioFlinger(),
- mPrimaryHardwareDev(0), mMasterVolume(1.0f), mMasterMute(false), mNextUniqueId(1)
+ mPrimaryHardwareDev(0), mMasterVolume(1.0f), mMasterMute(false), mNextUniqueId(1),
+ mBtNrec(false)
{
}
@@ -717,6 +720,31 @@
final_result = result ?: final_result;
}
mHardwareStatus = AUDIO_HW_IDLE;
+ // disable AEC and NS if the device is a BT SCO headset supporting those pre processings
+ AudioParameter param = AudioParameter(keyValuePairs);
+ String8 value;
+ if (param.get(String8(AUDIO_PARAMETER_KEY_BT_NREC), value) == NO_ERROR) {
+ Mutex::Autolock _l(mLock);
+ bool btNrec = (value == AUDIO_PARAMETER_VALUE_ON);
+ if (mBtNrec != btNrec) {
+ for (size_t i = 0; i < mRecordThreads.size(); i++) {
+ sp<RecordThread> thread = mRecordThreads.valueAt(i);
+ RecordThread::RecordTrack *track = thread->track();
+ if (track != NULL) {
+ audio_devices_t device = (audio_devices_t)(
+ thread->device() & AUDIO_DEVICE_IN_ALL);
+ bool suspend = audio_is_bluetooth_sco_device(device) && btNrec;
+ thread->setEffectSuspended(FX_IID_AEC,
+ suspend,
+ track->sessionId());
+ thread->setEffectSuspended(FX_IID_NS,
+ suspend,
+ track->sessionId());
+ }
+ }
+ mBtNrec = btNrec;
+ }
+ }
return final_result;
}
@@ -1130,6 +1158,140 @@
LOGW("power manager service died !!!");
}
+void AudioFlinger::ThreadBase::setEffectSuspended(
+ const effect_uuid_t *type, bool suspend, int sessionId)
+{
+ Mutex::Autolock _l(mLock);
+ setEffectSuspended_l(type, suspend, sessionId);
+}
+
+void AudioFlinger::ThreadBase::setEffectSuspended_l(
+ const effect_uuid_t *type, bool suspend, int sessionId)
+{
+ sp<EffectChain> chain;
+ chain = getEffectChain_l(sessionId);
+ if (chain != 0) {
+ if (type != NULL) {
+ chain->setEffectSuspended_l(type, suspend);
+ } else {
+ chain->setEffectSuspendedAll_l(suspend);
+ }
+ }
+
+ updateSuspendedSessions_l(type, suspend, sessionId);
+}
+
+void AudioFlinger::ThreadBase::checkSuspendOnAddEffectChain_l(const sp<EffectChain>& chain)
+{
+ int index = mSuspendedSessions.indexOfKey(chain->sessionId());
+ if (index < 0) {
+ return;
+ }
+
+ KeyedVector <int, sp<SuspendedSessionDesc> > sessionEffects =
+ mSuspendedSessions.editValueAt(index);
+
+ for (size_t i = 0; i < sessionEffects.size(); i++) {
+ sp <SuspendedSessionDesc> desc = sessionEffects.valueAt(i);
+ for (int j = 0; j < desc->mRefCount; j++) {
+ if (sessionEffects.keyAt(i) == EffectChain::kKeyForSuspendAll) {
+ chain->setEffectSuspendedAll_l(true);
+ } else {
+ LOGV("checkSuspendOnAddEffectChain_l() suspending effects %08x",
+ desc->mType.timeLow);
+ chain->setEffectSuspended_l(&desc->mType, true);
+ }
+ }
+ }
+}
+
+void AudioFlinger::ThreadBase::updateSuspendedSessionsOnRemoveEffectChain_l(
+ const sp<EffectChain>& chain)
+{
+ int index = mSuspendedSessions.indexOfKey(chain->sessionId());
+ if (index < 0) {
+ return;
+ }
+ LOGV("updateSuspendedSessionsOnRemoveEffectChain_l() removed suspended session %d",
+ chain->sessionId());
+ mSuspendedSessions.removeItemsAt(index);
+}
+
+void AudioFlinger::ThreadBase::updateSuspendedSessions_l(const effect_uuid_t *type,
+ bool suspend,
+ int sessionId)
+{
+ int index = mSuspendedSessions.indexOfKey(sessionId);
+
+ KeyedVector <int, sp<SuspendedSessionDesc> > sessionEffects;
+
+ if (suspend) {
+ if (index >= 0) {
+ sessionEffects = mSuspendedSessions.editValueAt(index);
+ } else {
+ mSuspendedSessions.add(sessionId, sessionEffects);
+ }
+ } else {
+ if (index < 0) {
+ return;
+ }
+ sessionEffects = mSuspendedSessions.editValueAt(index);
+ }
+
+
+ int key = EffectChain::kKeyForSuspendAll;
+ if (type != NULL) {
+ key = type->timeLow;
+ }
+ index = sessionEffects.indexOfKey(key);
+
+ sp <SuspendedSessionDesc> desc;
+ if (suspend) {
+ if (index >= 0) {
+ desc = sessionEffects.valueAt(index);
+ } else {
+ desc = new SuspendedSessionDesc();
+ if (type != NULL) {
+ memcpy(&desc->mType, type, sizeof(effect_uuid_t));
+ }
+ sessionEffects.add(key, desc);
+ LOGV("updateSuspendedSessions_l() suspend adding effect %08x", key);
+ }
+ desc->mRefCount++;
+ } else {
+ if (index < 0) {
+ return;
+ }
+ desc = sessionEffects.valueAt(index);
+ if (--desc->mRefCount == 0) {
+ LOGV("updateSuspendedSessions_l() restore removing effect %08x", key);
+ sessionEffects.removeItemsAt(index);
+ if (sessionEffects.isEmpty()) {
+ LOGV("updateSuspendedSessions_l() restore removing session %d",
+ sessionId);
+ mSuspendedSessions.removeItem(sessionId);
+ }
+ }
+ }
+ if (!sessionEffects.isEmpty()) {
+ mSuspendedSessions.replaceValueFor(sessionId, sessionEffects);
+ }
+}
+
+void AudioFlinger::ThreadBase::checkSuspendOnEffectEnabled(const sp<EffectModule>& effect,
+ bool enabled,
+ int sessionId)
+{
+ Mutex::Autolock _l(mLock);
+
+ // TODO: implement PlaybackThread or RecordThread specific behavior here
+
+ sp<EffectChain> chain = getEffectChain_l(sessionId);
+ if (chain != 0) {
+ chain->checkSuspendOnEffectEnabled(effect, enabled);
+ }
+}
+
// ----------------------------------------------------------------------------
AudioFlinger::PlaybackThread::PlaybackThread(const sp<AudioFlinger>& audioFlinger,
@@ -4143,7 +4305,11 @@
}
mTrack = track.get();
-
+ // disable AEC and NS if the device is a BT SCO headset supporting those pre processings
+ bool suspend = audio_is_bluetooth_sco_device(
+ (audio_devices_t)(mDevice & AUDIO_DEVICE_IN_ALL)) && mAudioFlinger->btNrec();
+ setEffectSuspended_l(FX_IID_AEC, suspend, sessionId);
+ setEffectSuspended_l(FX_IID_NS, suspend, sessionId);
}
lStatus = NO_ERROR;
@@ -4363,6 +4529,13 @@
status = BAD_VALUE;
} else {
mDevice &= (uint32_t)~(value & AUDIO_DEVICE_IN_ALL);
+ // disable AEC and NS if the device is a BT SCO headset supporting those pre processings
+ if (mTrack != NULL) {
+ bool suspend = audio_is_bluetooth_sco_device(
+ (audio_devices_t)value) && mAudioFlinger->btNrec();
+ setEffectSuspended_l(FX_IID_AEC, suspend, mTrack->sessionId());
+ setEffectSuspended_l(FX_IID_NS, suspend, mTrack->sessionId());
+ }
}
mDevice |= (uint32_t)value;
}
@@ -4490,6 +4663,12 @@
return result;
}
+AudioFlinger::RecordThread::RecordTrack* AudioFlinger::RecordThread::track()
+{
+ Mutex::Autolock _l(mLock);
+ return mTrack;
+}
+
// ----------------------------------------------------------------------------
int AudioFlinger::openOutput(uint32_t *pDevices,
@@ -4874,10 +5053,6 @@
}
-// this UUID must match the one defined in media/libeffects/EffectVisualizer.cpp
-static const effect_uuid_t VISUALIZATION_UUID_ =
- {0xd069d9e0, 0x8329, 0x11df, 0x9168, {0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b}};
-
sp<IEffect> AudioFlinger::createEffect(pid_t pid,
effect_descriptor_t *pDesc,
const sp<IEffectClient>& effectClient,
@@ -4915,14 +5090,6 @@
goto Exit;
}
- // check recording permission for visualizer
- if ((memcmp(&pDesc->type, SL_IID_VISUALIZATION, sizeof(effect_uuid_t)) == 0 ||
- memcmp(&pDesc->uuid, &VISUALIZATION_UUID_, sizeof(effect_uuid_t)) == 0) &&
- !recordingAllowed()) {
- lStatus = PERMISSION_DENIED;
- goto Exit;
- }
-
if (io == 0) {
if (sessionId == AUDIO_SESSION_OUTPUT_STAGE) {
// output must be specified by AudioPolicyManager when using session
@@ -5003,6 +5170,13 @@
goto Exit;
}
+ // check recording permission for visualizer
+ if ((memcmp(&desc.type, SL_IID_VISUALIZATION, sizeof(effect_uuid_t)) == 0) &&
+ !recordingAllowed()) {
+ lStatus = PERMISSION_DENIED;
+ goto Exit;
+ }
+
// return effect descriptor
memcpy(pDesc, &desc, sizeof(effect_descriptor_t));
@@ -5069,10 +5243,10 @@
return handle;
}
-status_t AudioFlinger::moveEffects(int session, int srcOutput, int dstOutput)
+status_t AudioFlinger::moveEffects(int sessionId, int srcOutput, int dstOutput)
{
LOGV("moveEffects() session %d, srcOutput %d, dstOutput %d",
- session, srcOutput, dstOutput);
+ sessionId, srcOutput, dstOutput);
Mutex::Autolock _l(mLock);
if (srcOutput == dstOutput) {
LOGW("moveEffects() same dst and src outputs %d", dstOutput);
@@ -5091,24 +5265,24 @@
Mutex::Autolock _dl(dstThread->mLock);
Mutex::Autolock _sl(srcThread->mLock);
- moveEffectChain_l(session, srcThread, dstThread, false);
+ moveEffectChain_l(sessionId, srcThread, dstThread, false);
return NO_ERROR;
}
// moveEffectChain_l mustbe called with both srcThread and dstThread mLocks held
-status_t AudioFlinger::moveEffectChain_l(int session,
+status_t AudioFlinger::moveEffectChain_l(int sessionId,
AudioFlinger::PlaybackThread *srcThread,
AudioFlinger::PlaybackThread *dstThread,
bool reRegister)
{
LOGV("moveEffectChain_l() session %d from thread %p to thread %p",
- session, srcThread, dstThread);
+ sessionId, srcThread, dstThread);
- sp<EffectChain> chain = srcThread->getEffectChain_l(session);
+ sp<EffectChain> chain = srcThread->getEffectChain_l(sessionId);
if (chain == 0) {
LOGW("moveEffectChain_l() effect chain for session %d not on source thread %p",
- session, srcThread);
+ sessionId, srcThread);
return INVALID_OPERATION;
}
@@ -5143,7 +5317,7 @@
AudioSystem::registerEffect(&effect->desc(),
dstOutput,
strategy,
- session,
+ sessionId,
effect->id());
}
effect = chain->getEffectFromId_l(0);
@@ -5385,6 +5559,7 @@
void AudioFlinger::ThreadBase::disconnectEffect(const sp<EffectModule>& effect,
const wp<EffectHandle>& handle) {
+
Mutex::Autolock _l(mLock);
LOGV("disconnectEffect() %p effect %p", this, effect.get());
// delete the effect module if removing last handle on it
@@ -5451,6 +5626,7 @@
if (mEffectChains[i]->sessionId() < session) break;
}
mEffectChains.insertAt(chain, i);
+ checkSuspendOnAddEffectChain_l(chain);
return NO_ERROR;
}
@@ -5463,6 +5639,7 @@
for (size_t i = 0; i < mEffectChains.size(); i++) {
if (chain == mEffectChains[i]) {
+ updateSuspendedSessionsOnRemoveEffectChain_l(chain);
mEffectChains.removeAt(i);
// detach all active tracks from the chain
for (size_t i = 0 ; i < mActiveTracks.size() ; ++i) {
@@ -5540,6 +5717,8 @@
chain->setInBuffer(NULL);
chain->setOutBuffer(NULL);
+ checkSuspendOnAddEffectChain_l(chain);
+
mEffectChains.add(chain);
return NO_ERROR;
@@ -5552,6 +5731,7 @@
"removeEffectChain_l() %p invalid chain size %d on thread %p",
chain.get(), mEffectChains.size(), this);
if (mEffectChains.size() == 1) {
+ updateSuspendedSessionsOnRemoveEffectChain_l(chain);
mEffectChains.removeAt(0);
}
return 0;
@@ -5570,7 +5750,7 @@
int id,
int sessionId)
: mThread(wThread), mChain(chain), mId(id), mSessionId(sessionId), mEffectInterface(NULL),
- mStatus(NO_INIT), mState(IDLE)
+ mStatus(NO_INIT), mState(IDLE), mSuspended(false)
{
LOGV("Constructor %p", this);
int lStatus;
@@ -5634,14 +5814,17 @@
}
// if inserted in first place, move effect control from previous owner to this handle
if (i == 0) {
+ bool enabled = false;
if (h != 0) {
- h->setControl(false, true);
+ enabled = h->enabled();
+ h->setControl(false/*hasControl*/, true /*signal*/, enabled /*enabled*/);
}
- handle->setControl(true, false);
+ handle->setControl(true /*hasControl*/, false /*signal*/, enabled /*enabled*/);
status = NO_ERROR;
} else {
status = ALREADY_EXISTS;
}
+ LOGV("addHandle() %p added handle %p in position %d", this, handle.get(), i);
mHandles.insertAt(handle, i);
return status;
}
@@ -5657,13 +5840,21 @@
if (i == size) {
return size;
}
+ LOGV("removeHandle() %p removed handle %p in position %d", this, handle.unsafe_get(), i);
+
+ bool enabled = false;
+ EffectHandle *hdl = handle.unsafe_get();
+ if (hdl) {
+ LOGV("removeHandle() unsafe_get OK");
+ enabled = hdl->enabled();
+ }
mHandles.removeAt(i);
size = mHandles.size();
// if removed from first place, move effect control from this handle to next in line
if (i == 0 && size != 0) {
sp<EffectHandle> h = mHandles[0].promote();
if (h != 0) {
- h->setControl(true, true);
+ h->setControl(true /*hasControl*/, true /*signal*/ , enabled /*enabled*/);
}
}
@@ -5677,8 +5868,21 @@
return size;
}
+sp<AudioFlinger::EffectHandle> AudioFlinger::EffectModule::controlHandle()
+{
+ Mutex::Autolock _l(mLock);
+ sp<EffectHandle> handle;
+ if (mHandles.size() != 0) {
+ handle = mHandles[0].promote();
+ }
+ return handle;
+}
+
+
+
void AudioFlinger::EffectModule::disconnect(const wp<EffectHandle>& handle)
{
+ LOGV("disconnect() %p handle %p ", this, handle.unsafe_get());
// keep a strong reference on this EffectModule to avoid calling the
// destructor before we exit
sp<EffectModule> keep(this);
@@ -6139,6 +6343,17 @@
return status;
}
+void AudioFlinger::EffectModule::setSuspended(bool suspended)
+{
+ Mutex::Autolock _l(mLock);
+ mSuspended = suspended;
+}
+bool AudioFlinger::EffectModule::suspended()
+{
+ Mutex::Autolock _l(mLock);
+ return mSuspended;
+}
+
status_t AudioFlinger::EffectModule::dump(int fd, const Vector<String16>& args)
{
const size_t SIZE = 256;
@@ -6235,7 +6450,8 @@
const sp<IEffectClient>& effectClient,
int32_t priority)
: BnEffect(),
- mEffect(effect), mEffectClient(effectClient), mClient(client), mPriority(priority), mHasControl(false)
+ mEffect(effect), mEffectClient(effectClient), mClient(client),
+ mPriority(priority), mHasControl(false), mEnabled(false)
{
LOGV("constructor %p", this);
@@ -6258,30 +6474,66 @@
{
LOGV("Destructor %p", this);
disconnect();
+ LOGV("Destructor DONE %p", this);
}
status_t AudioFlinger::EffectHandle::enable()
{
+ LOGV("enable %p", this);
if (!mHasControl) return INVALID_OPERATION;
if (mEffect == 0) return DEAD_OBJECT;
+ mEnabled = true;
+
+ sp<ThreadBase> thread = mEffect->thread().promote();
+ if (thread != 0) {
+ thread->checkSuspendOnEffectEnabled(mEffect, true, mEffect->sessionId());
+ }
+
+ // checkSuspendOnEffectEnabled() can suspend this same effect when enabled
+ if (mEffect->suspended()) {
+ return NO_ERROR;
+ }
+
return mEffect->setEnabled(true);
}
status_t AudioFlinger::EffectHandle::disable()
{
+ LOGV("disable %p", this);
if (!mHasControl) return INVALID_OPERATION;
- if (mEffect == NULL) return DEAD_OBJECT;
+ if (mEffect == 0) return DEAD_OBJECT;
- return mEffect->setEnabled(false);
+ mEnabled = false;
+
+ if (mEffect->suspended()) {
+ return NO_ERROR;
+ }
+
+ status_t status = mEffect->setEnabled(false);
+
+ sp<ThreadBase> thread = mEffect->thread().promote();
+ if (thread != 0) {
+ thread->checkSuspendOnEffectEnabled(mEffect, false, mEffect->sessionId());
+ }
+
+ return status;
}
void AudioFlinger::EffectHandle::disconnect()
{
+ LOGV("disconnect %p", this);
if (mEffect == 0) {
return;
}
+
mEffect->disconnect(this);
+
+ sp<ThreadBase> thread = mEffect->thread().promote();
+ if (thread != 0) {
+ thread->checkSuspendOnEffectEnabled(mEffect, false, mEffect->sessionId());
+ }
+
// release sp on module => module destructor can be called now
mEffect.clear();
if (mCblk) {
@@ -6373,11 +6625,13 @@
return mCblkMemory;
}
-void AudioFlinger::EffectHandle::setControl(bool hasControl, bool signal)
+void AudioFlinger::EffectHandle::setControl(bool hasControl, bool signal, bool enabled)
{
LOGV("setControl %p control %d", this, hasControl);
mHasControl = hasControl;
+ mEnabled = enabled;
+
if (signal && mEffectClient != 0) {
mEffectClient->controlStatusChanged(hasControl);
}
@@ -6448,7 +6702,7 @@
}
-// getEffectFromDesc_l() must be called with PlaybackThread::mLock held
+// getEffectFromDesc_l() must be called with ThreadBase::mLock held
sp<AudioFlinger::EffectModule> AudioFlinger::EffectChain::getEffectFromDesc_l(effect_descriptor_t *descriptor)
{
sp<EffectModule> effect;
@@ -6463,7 +6717,7 @@
return effect;
}
-// getEffectFromId_l() must be called with PlaybackThread::mLock held
+// getEffectFromId_l() must be called with ThreadBase::mLock held
sp<AudioFlinger::EffectModule> AudioFlinger::EffectChain::getEffectFromId_l(int id)
{
sp<EffectModule> effect;
@@ -6479,6 +6733,22 @@
return effect;
}
+// getEffectFromType_l() must be called with ThreadBase::mLock held
+sp<AudioFlinger::EffectModule> AudioFlinger::EffectChain::getEffectFromType_l(
+ const effect_uuid_t *type)
+{
+ sp<EffectModule> effect;
+ size_t size = mEffects.size();
+
+ for (size_t i = 0; i < size; i++) {
+ if (memcmp(&mEffects[i]->desc().type, type, sizeof(effect_uuid_t)) == 0) {
+ effect = mEffects[i];
+ break;
+ }
+ }
+ return effect;
+}
+
// Must be called with EffectChain::mLock locked
void AudioFlinger::EffectChain::process_l()
{
@@ -6773,6 +7043,166 @@
return NO_ERROR;
}
+// must be called with ThreadBase::mLock held
+void AudioFlinger::EffectChain::setEffectSuspended_l(
+ const effect_uuid_t *type, bool suspend)
+{
+ sp<SuspendedEffectDesc> desc;
+ // use effect type UUID timelow as key as there is no real risk of identical
+ // timeLow fields among effect type UUIDs.
+ int index = mSuspendedEffects.indexOfKey(type->timeLow);
+ if (suspend) {
+ if (index >= 0) {
+ desc = mSuspendedEffects.valueAt(index);
+ } else {
+ desc = new SuspendedEffectDesc();
+ memcpy(&desc->mType, type, sizeof(effect_uuid_t));
+ mSuspendedEffects.add(type->timeLow, desc);
+ LOGV("setEffectSuspended_l() add entry for %08x", type->timeLow);
+ }
+ if (desc->mRefCount++ == 0) {
+ sp<EffectModule> effect = getEffectIfEnabled(type);
+ if (effect != 0) {
+ desc->mEffect = effect;
+ effect->setSuspended(true);
+ effect->setEnabled(false);
+ }
+ }
+ } else {
+ if (index < 0) {
+ return;
+ }
+ desc = mSuspendedEffects.valueAt(index);
+ if (desc->mRefCount <= 0) {
+ LOGW("setEffectSuspended_l() restore refcount should not be 0 %d", desc->mRefCount);
+ desc->mRefCount = 1;
+ }
+ if (--desc->mRefCount == 0) {
+ LOGV("setEffectSuspended_l() remove entry for %08x", mSuspendedEffects.keyAt(index));
+ if (desc->mEffect != 0) {
+ sp<EffectModule> effect = desc->mEffect.promote();
+ if (effect != 0) {
+ effect->setSuspended(false);
+ sp<EffectHandle> handle = effect->controlHandle();
+ if (handle != 0) {
+ effect->setEnabled(handle->enabled());
+ }
+ }
+ desc->mEffect.clear();
+ }
+ mSuspendedEffects.removeItemsAt(index);
+ }
+ }
+}
+
+// must be called with ThreadBase::mLock held
+void AudioFlinger::EffectChain::setEffectSuspendedAll_l(bool suspend)
+{
+ sp<SuspendedEffectDesc> desc;
+
+ int index = mSuspendedEffects.indexOfKey((int)kKeyForSuspendAll);
+ if (suspend) {
+ if (index >= 0) {
+ desc = mSuspendedEffects.valueAt(index);
+ } else {
+ desc = new SuspendedEffectDesc();
+ mSuspendedEffects.add((int)kKeyForSuspendAll, desc);
+ LOGV("setEffectSuspendedAll_l() add entry for 0");
+ }
+ if (desc->mRefCount++ == 0) {
+ Vector< sp<EffectModule> > effects = getSuspendEligibleEffects();
+ for (size_t i = 0; i < effects.size(); i++) {
+ setEffectSuspended_l(&effects[i]->desc().type, true);
+ }
+ }
+ } else {
+ if (index < 0) {
+ return;
+ }
+ desc = mSuspendedEffects.valueAt(index);
+ if (desc->mRefCount <= 0) {
+ LOGW("setEffectSuspendedAll_l() restore refcount should not be 0 %d", desc->mRefCount);
+ desc->mRefCount = 1;
+ }
+ if (--desc->mRefCount == 0) {
+ Vector<const effect_uuid_t *> types;
+ for (size_t i = 0; i < mSuspendedEffects.size(); i++) {
+ if (mSuspendedEffects.keyAt(i) == (int)kKeyForSuspendAll) {
+ continue;
+ }
+ types.add(&mSuspendedEffects.valueAt(i)->mType);
+ }
+ for (size_t i = 0; i < types.size(); i++) {
+ setEffectSuspended_l(types[i], false);
+ }
+ LOGV("setEffectSuspendedAll_l() remove entry for %08x", mSuspendedEffects.keyAt(index));
+ mSuspendedEffects.removeItem((int)kKeyForSuspendAll);
+ }
+ }
+}
+
+Vector< sp<AudioFlinger::EffectModule> > AudioFlinger::EffectChain::getSuspendEligibleEffects()
+{
+ Vector< sp<EffectModule> > effects;
+ for (size_t i = 0; i < mEffects.size(); i++) {
+ effect_descriptor_t desc = mEffects[i]->desc();
+ // auxiliary effects and vizualizer are never suspended on output mix
+ if ((mSessionId == AUDIO_SESSION_OUTPUT_MIX) && (
+ ((desc.flags & EFFECT_FLAG_TYPE_MASK) == EFFECT_FLAG_TYPE_AUXILIARY) ||
+ (memcmp(&desc.type, SL_IID_VISUALIZATION, sizeof(effect_uuid_t)) == 0))) {
+ continue;
+ }
+ effects.add(mEffects[i]);
+ }
+ return effects;
+}
+
+sp<AudioFlinger::EffectModule> AudioFlinger::EffectChain::getEffectIfEnabled(
+ const effect_uuid_t *type)
+{
+ sp<EffectModule> effect;
+ effect = getEffectFromType_l(type);
+ if (effect != 0 && !effect->isEnabled()) {
+ effect.clear();
+ }
+ return effect;
+}
+
+void AudioFlinger::EffectChain::checkSuspendOnEffectEnabled(const sp<EffectModule>& effect,
+ bool enabled)
+{
+ int index = mSuspendedEffects.indexOfKey(effect->desc().type.timeLow);
+ if (enabled) {
+ if (index < 0) {
+ // if the effect is not suspend check if all effects are suspended
+ index = mSuspendedEffects.indexOfKey((int)kKeyForSuspendAll);
+ if (index < 0) {
+ return;
+ }
+ setEffectSuspended_l(&effect->desc().type, enabled);
+ index = mSuspendedEffects.indexOfKey(effect->desc().type.timeLow);
+ }
+ LOGV("checkSuspendOnEffectEnabled() enable suspending fx %08x",
+ effect->desc().type.timeLow);
+ sp<SuspendedEffectDesc> desc = mSuspendedEffects.valueAt(index);
+ // if effect is requested to suspended but was not yet enabled, supend it now.
+ if (desc->mEffect == 0) {
+ desc->mEffect = effect;
+ effect->setEnabled(false);
+ effect->setSuspended(true);
+ }
+ } else {
+ if (index < 0) {
+ return;
+ }
+ LOGV("checkSuspendOnEffectEnabled() disable restoring fx %08x",
+ effect->desc().type.timeLow);
+ sp<SuspendedEffectDesc> desc = mSuspendedEffects.valueAt(index);
+ desc->mEffect.clear();
+ effect->setSuspended(false);
+ }
+}
+
#undef LOG_TAG
#define LOG_TAG "AudioFlinger"
diff --git a/services/audioflinger/AudioFlinger.h b/services/audioflinger/AudioFlinger.h
index 7b6215f..791341a 100644
--- a/services/audioflinger/AudioFlinger.h
+++ b/services/audioflinger/AudioFlinger.h
@@ -165,7 +165,7 @@
int *id,
int *enabled);
- virtual status_t moveEffects(int session, int srcOutput, int dstOutput);
+ virtual status_t moveEffects(int sessionId, int srcOutput, int dstOutput);
enum hardware_call_state {
AUDIO_HW_IDLE = 0,
@@ -206,6 +206,8 @@
uint32_t getMode() { return mMode; }
+ bool btNrec() { return mBtNrec; }
+
private:
AudioFlinger();
virtual ~AudioFlinger();
@@ -477,14 +479,45 @@
// strategy is only meaningful for PlaybackThread which implements this method
virtual uint32_t getStrategyForSession_l(int sessionId) { return 0; }
+ // suspend or restore effect according to the type of effect passed. a NULL
+ // type pointer means suspend all effects in the session
+ void setEffectSuspended(const effect_uuid_t *type,
+ bool suspend,
+ int sessionId = AUDIO_SESSION_OUTPUT_MIX);
+ // check if some effects must be suspended/restored when an effect is enabled
+ // or disabled
+ virtual void checkSuspendOnEffectEnabled(const sp<EffectModule>& effect,
+ bool enabled,
+ int sessionId = AUDIO_SESSION_OUTPUT_MIX);
+
mutable Mutex mLock;
protected:
+ // entry describing an effect being suspended in mSuspendedSessions keyed vector
+ class SuspendedSessionDesc : public RefBase {
+ public:
+ SuspendedSessionDesc() : mRefCount(0) {}
+
+ int mRefCount; // number of active suspend requests
+ effect_uuid_t mType; // effect type UUID
+ };
+
void acquireWakeLock();
void acquireWakeLock_l();
void releaseWakeLock();
void releaseWakeLock_l();
+ void setEffectSuspended_l(const effect_uuid_t *type,
+ bool suspend,
+ int sessionId = AUDIO_SESSION_OUTPUT_MIX);
+ // updated mSuspendedSessions when an effect suspended or restored
+ void updateSuspendedSessions_l(const effect_uuid_t *type,
+ bool suspend,
+ int sessionId);
+ // check if some effects must be suspended when an effect chain is added
+ void checkSuspendOnAddEffectChain_l(const sp<EffectChain>& chain);
+ // updated mSuspendedSessions when an effect chain is removed
+ void updateSuspendedSessionsOnRemoveEffectChain_l(const sp<EffectChain>& chain);
friend class Track;
friend class TrackBase;
@@ -519,6 +552,9 @@
sp<IPowerManager> mPowerManager;
sp<IBinder> mWakeLockToken;
sp<PMDeathRecipient> mDeathRecipient;
+ // list of suspended effects per session and per type. The first vector is
+ // keyed by session ID, the second by type UUID timeLow field
+ KeyedVector< int, KeyedVector< int, sp<SuspendedSessionDesc> > > mSuspendedSessions;
};
// --- PlaybackThread ---
@@ -848,7 +884,7 @@
void audioConfigChanged_l(int event, int ioHandle, void *param2);
uint32_t nextUniqueId();
- status_t moveEffectChain_l(int session,
+ status_t moveEffectChain_l(int sessionId,
AudioFlinger::PlaybackThread *srcThread,
AudioFlinger::PlaybackThread *dstThread,
bool reRegister);
@@ -908,6 +944,7 @@
bool setOverflow() { bool tmp = mOverflow; mOverflow = true; return tmp; }
void dump(char* buffer, size_t size);
+
private:
friend class AudioFlinger;
friend class RecordThread;
@@ -950,8 +987,6 @@
AudioStreamIn* getInput() { return mInput; }
virtual audio_stream_t* stream() { return &mInput->stream->common; }
-
- void setTrack(RecordTrack *recordTrack) { mTrack = recordTrack; }
virtual status_t getNextBuffer(AudioBufferProvider::Buffer* buffer);
virtual void releaseBuffer(AudioBufferProvider::Buffer* buffer);
virtual bool checkForNewParameters_l();
@@ -963,6 +998,7 @@
virtual status_t addEffectChain_l(const sp<EffectChain>& chain);
virtual size_t removeEffectChain_l(const sp<EffectChain>& chain);
virtual uint32_t hasAudioSession(int sessionId);
+ RecordTrack* track();
private:
RecordThread();
@@ -1059,6 +1095,7 @@
int16_t *outBuffer() { return mConfig.outputCfg.buffer.s16; }
void setChain(const wp<EffectChain>& chain) { mChain = chain; }
void setThread(const wp<ThreadBase>& thread) { mThread = thread; }
+ wp<ThreadBase>& thread() { return mThread; }
status_t addHandle(sp<EffectHandle>& handle);
void disconnect(const wp<EffectHandle>& handle);
@@ -1071,6 +1108,10 @@
status_t setVolume(uint32_t *left, uint32_t *right, bool controller);
status_t setMode(uint32_t mode);
status_t stop();
+ void setSuspended(bool suspended);
+ bool suspended();
+
+ sp<EffectHandle> controlHandle();
status_t dump(int fd, const Vector<String16>& args);
@@ -1099,6 +1140,7 @@
uint32_t mMaxDisableWaitCnt; // maximum grace period before forcing an effect off after
// sending disable command.
uint32_t mDisableWaitCnt; // current process() calls count during disable period.
+ bool mSuspended; // effect is suspended: temporarily disabled by framework
};
// The EffectHandle class implements the IEffect interface. It provides resources
@@ -1131,13 +1173,17 @@
// Give or take control of effect module
- void setControl(bool hasControl, bool signal);
+ // - hasControl: true if control is given, false if removed
+ // - signal: true client app should be signaled of change, false otherwise
+ // - enabled: state of the effect when control is passed
+ void setControl(bool hasControl, bool signal, bool enabled);
void commandExecuted(uint32_t cmdCode,
uint32_t cmdSize,
void *pCmdData,
uint32_t replySize,
void *pReplyData);
void setEnabled(bool enabled);
+ bool enabled() { return mEnabled; }
// Getters
int id() { return mEffect->id(); }
@@ -1160,6 +1206,8 @@
uint8_t* mBuffer; // pointer to parameter area in shared memory
int mPriority; // client application priority to control the effect
bool mHasControl; // true if this handle is controlling the effect
+ bool mEnabled; // cached enable state: needed when the effect is
+ // restored after being suspended
};
// the EffectChain class represents a group of effects associated to one audio session.
@@ -1174,6 +1222,10 @@
EffectChain(const wp<ThreadBase>& wThread, int sessionId);
~EffectChain();
+ // special key used for an entry in mSuspendedEffects keyed vector
+ // corresponding to a suspend all request.
+ static const int kKeyForSuspendAll = 0;
+
void process_l();
void lock() {
@@ -1191,6 +1243,7 @@
sp<EffectModule> getEffectFromDesc_l(effect_descriptor_t *descriptor);
sp<EffectModule> getEffectFromId_l(int id);
+ sp<EffectModule> getEffectFromType_l(const effect_uuid_t *type);
bool setVolume_l(uint32_t *left, uint32_t *right);
void setDevice_l(uint32_t device);
void setMode_l(uint32_t mode);
@@ -1221,6 +1274,15 @@
void setStrategy(uint32_t strategy)
{ mStrategy = strategy; }
+ // suspend effect of the given type
+ void setEffectSuspended_l(const effect_uuid_t *type,
+ bool suspend);
+ // suspend all eligible effects
+ void setEffectSuspendedAll_l(bool suspend);
+ // check if effects should be suspend or restored when a given effect is enable or disabled
+ virtual void checkSuspendOnEffectEnabled(const sp<EffectModule>& effect,
+ bool enabled);
+
status_t dump(int fd, const Vector<String16>& args);
protected:
@@ -1228,6 +1290,21 @@
EffectChain(const EffectChain&);
EffectChain& operator =(const EffectChain&);
+ class SuspendedEffectDesc : public RefBase {
+ public:
+ SuspendedEffectDesc() : mRefCount(0) {}
+
+ int mRefCount;
+ effect_uuid_t mType;
+ wp<EffectModule> mEffect;
+ };
+
+ // get a list of effect modules to suspend when an effect of the type
+ // passed is enabled.
+ Vector< sp<EffectModule> > getSuspendEligibleEffects();
+ // get an effect module if it is currently enable
+ sp<EffectModule> getEffectIfEnabled(const effect_uuid_t *type);
+
wp<ThreadBase> mThread; // parent mixer thread
Mutex mLock; // mutex protecting effect list
Vector<sp<EffectModule> > mEffects; // list of effect modules
@@ -1243,6 +1320,10 @@
uint32_t mNewLeftVolume; // new volume on left channel
uint32_t mNewRightVolume; // new volume on right channel
uint32_t mStrategy; // strategy for this effect chain
+ // mSuspendedEffects lists all effect currently suspended in the chain
+ // use effect type UUID timelow field as key. There is no real risk of identical
+ // timeLow fields among effect type UUIDs.
+ KeyedVector< int, sp<SuspendedEffectDesc> > mSuspendedEffects;
};
struct AudioStreamOut {
@@ -1283,7 +1364,8 @@
DefaultKeyedVector< pid_t, sp<NotificationClient> > mNotificationClients;
volatile int32_t mNextUniqueId;
- uint32_t mMode;
+ uint32_t mMode;
+ bool mBtNrec;
};
diff --git a/tests/TileBenchmark/res/values/strings.xml b/tests/TileBenchmark/res/values/strings.xml
index 66972ac..c4fd189 100644
--- a/tests/TileBenchmark/res/values/strings.xml
+++ b/tests/TileBenchmark/res/values/strings.xml
@@ -71,8 +71,16 @@
<string name="frames_per_second">Frames/sec</string>
<!-- Portion of viewport covered by good tiles [CHAR LIMIT=15] -->
<string name="viewport_coverage">Coverage</string>
+ <!-- Milliseconds taken to inval, and re-render the page [CHAR LIMIT=15] -->
+ <string name="render_millis">RenderMillis</string>
<!-- Format string for stat value overlay [CHAR LIMIT=15] -->
<string name="format_stat">%4.4f</string>
+
+ <!-- Format string for viewport position value overlay [CHAR LIMIT=25] -->
+ <string name="format_view_pos">View:(%1$d,%2$d)-(%3$d,%4$d)</string>
+ <!-- Format string for viewport position value overlay [CHAR LIMIT=25] -->
+ <string name="format_inval_pos">Inval:(%1$d,%2$d)-(%3$d,%4$d)</string>
+
<!-- Format string for displaying aggregate stats+values (nr of valid tiles,
etc.) [CHAR LIMIT=20] -->
<string name="format_stat_name">%1$-20s %2$3d</string>
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java
index 36694a7..1eb1c00 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java
@@ -83,14 +83,14 @@
}
};
- private class LoadFileTask extends AsyncTask<String, Void, TileData[][]> {
+ private class LoadFileTask extends AsyncTask<String, Void, RunData> {
@Override
- protected TileData[][] doInBackground(String... params) {
- TileData[][] data = null;
+ protected RunData doInBackground(String... params) {
+ RunData data = null;
try {
FileInputStream fis = openFileInput(params[0]);
ObjectInputStream in = new ObjectInputStream(fis);
- data = (TileData[][]) in.readObject();
+ data = (RunData) in.readObject();
in.close();
} catch (IOException ex) {
ex.printStackTrace();
@@ -101,7 +101,7 @@
}
@Override
- protected void onPostExecute(TileData data[][]) {
+ protected void onPostExecute(RunData data) {
if (data == null) {
Toast.makeText(getApplicationContext(),
getResources().getString(R.string.error_no_data),
@@ -110,7 +110,7 @@
}
mPlaybackView.setData(data);
- mFrameMax = data.length - 1;
+ mFrameMax = data.frames.length - 1;
mSeekBar.setMax(mFrameMax);
setFrame(null, 0);
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
index 35b1563..aad138c 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
@@ -22,10 +22,12 @@
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
-import android.os.Bundle;
+
+import com.test.tilebenchmark.RunData.TileData;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
public class PlaybackGraphs {
private static final int BAR_WIDTH = PlaybackView.TILE_SCALE * 3;
@@ -44,7 +46,7 @@
return 0.0f;
}
- private interface MetricGen {
+ protected interface MetricGen {
public double getValue(TileData[] frame);
public double getMax();
@@ -52,7 +54,7 @@
public int getLabelId();
};
- private static MetricGen[] Metrics = new MetricGen[] {
+ protected static MetricGen[] Metrics = new MetricGen[] {
new MetricGen() {
// framerate graph
@Override
@@ -99,7 +101,7 @@
}
};
- private interface StatGen {
+ protected interface StatGen {
public double getValue(double sortedValues[]);
public int getLabelId();
@@ -116,7 +118,7 @@
+ sortedValues[intIndex + 1] * (alpha);
}
- private static StatGen[] Stats = new StatGen[] {
+ protected static StatGen[] Stats = new StatGen[] {
new StatGen() {
@Override
public double getValue(double[] sortedValues) {
@@ -157,21 +159,22 @@
}
private ArrayList<ShapeDrawable> mShapes = new ArrayList<ShapeDrawable>();
- private double[][] mStats = new double[Metrics.length][Stats.length];
+ protected double[][] mStats = new double[Metrics.length][Stats.length];
+ protected HashMap<String, Double> mSingleStats;
- public void setData(TileData[][] tileProfilingData) {
+ public void setData(RunData data) {
mShapes.clear();
- double metricValues[] = new double[tileProfilingData.length];
+ double metricValues[] = new double[data.frames.length];
- if (tileProfilingData.length == 0) {
+ if (data.frames.length == 0) {
return;
}
for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
// create graph out of rectangles, one per frame
int lastBar = 0;
- for (int frameIndex = 0; frameIndex < tileProfilingData.length; frameIndex++) {
- TileData frame[] = tileProfilingData[frameIndex];
+ for (int frameIndex = 0; frameIndex < data.frames.length; frameIndex++) {
+ TileData frame[] = data.frames[frameIndex];
int newBar = (frame[0].top + frame[0].bottom) / 2;
MetricGen s = Metrics[metricIndex];
@@ -194,9 +197,11 @@
// store aggregate statistics per metric (median, and similar)
Arrays.sort(metricValues);
for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
- mStats[metricIndex][statIndex] = Stats[statIndex]
- .getValue(metricValues);
+ mStats[metricIndex][statIndex] =
+ Stats[statIndex].getValue(metricValues);
}
+
+ mSingleStats = data.singleStats;
}
}
@@ -215,7 +220,7 @@
}
public void draw(Canvas canvas, ArrayList<ShapeDrawable> shapes,
- String[] strings, Resources resources) {
+ ArrayList<String> strings, Resources resources) {
canvas.scale(CANVAS_SCALE, CANVAS_SCALE);
canvas.translate(BAR_WIDTH * Metrics.length, 0);
@@ -238,26 +243,9 @@
canvas.drawText(label, xPos, yPos, whiteLabels);
}
}
- for (int stringIndex = 0; stringIndex < strings.length; stringIndex++) {
+ for (int stringIndex = 0; stringIndex < strings.size(); stringIndex++) {
int yPos = LABELOFFSET + stringIndex * PlaybackView.TILE_SCALE / 2;
- canvas.drawText(strings[stringIndex], 0, yPos, whiteLabels);
+ canvas.drawText(strings.get(stringIndex), 0, yPos, whiteLabels);
}
}
-
- public Bundle getStatBundle(Resources resources) {
- Bundle b = new Bundle();
-
- for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
- for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
- String metricLabel = resources.getString(
- Metrics[metricIndex].getLabelId());
- String statLabel = resources.getString(
- Stats[statIndex].getLabelId());
- double value = mStats[metricIndex][statIndex];
- b.putDouble(metricLabel + " " + statLabel, value);
- }
- }
-
- return b;
- }
}
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java
index edc8643..5459c1f 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java
@@ -30,10 +30,12 @@
import android.view.MotionEvent;
import android.view.View;
+import com.test.tilebenchmark.RunData.TileData;
+
import java.util.ArrayList;
public class PlaybackView extends View {
- public static final int TILE_SCALE = 300;
+ public static final int TILE_SCALE = 256;
private static final int INVAL_FLAG = -2;
private static final int INVAL_CYCLE = 250;
@@ -41,9 +43,9 @@
private PlaybackGraphs mGraphs;
private ArrayList<ShapeDrawable> mTempShapes = new ArrayList<ShapeDrawable>();
- private TileData mProfData[][] = null;
+ private RunData mProfData = null;
private GestureDetector mGestureDetector = null;
- private String mRenderStrings[] = new String[4];
+ private ArrayList<String> mRenderStrings = new ArrayList<String>();
private class TileDrawable extends ShapeDrawable {
TileData tile;
@@ -135,17 +137,30 @@
invalidate(); // may have animations, force redraw
}
+ private String statString(int labelId, int value) {
+ return getResources().getString(R.string.format_stat_name,
+ getResources().getString(labelId), value);
+ }
+ private String tileString(int formatStringId, TileData t) {
+ return getResources().getString(formatStringId,
+ t.left, t.top, t.right, t.bottom);
+ }
+
public int setFrame(int frame) {
- if (mProfData == null || mProfData.length == 0) {
+ if (mProfData == null || mProfData.frames.length == 0) {
return 0;
}
int readyTiles = 0, unreadyTiles = 0, unplacedTiles = 0, numInvals = 0;
mTempShapes.clear();
+ mRenderStrings.clear();
// create tile shapes (as they're drawn on bottom)
- for (TileData t : mProfData[frame]) {
- if (t.level != INVAL_FLAG && t != mProfData[frame][0]) {
+ for (TileData t : mProfData.frames[frame]) {
+ if (t == mProfData.frames[frame][0]){
+ // viewport 'tile', add coords to render strings
+ mRenderStrings.add(tileString(R.string.format_view_pos, t));
+ } else if (t.level != INVAL_FLAG) {
int colorId;
if (t.isReady) {
readyTiles++;
@@ -159,14 +174,16 @@
}
mTempShapes.add(new TileDrawable(t, colorId));
} else {
+ // inval 'tile', count and add coords to render strings
numInvals++;
+ mRenderStrings.add(tileString(R.string.format_inval_pos, t));
}
}
// create invalidate shapes (drawn above tiles)
int invalId = 0;
- for (TileData t : mProfData[frame]) {
- if (t.level == INVAL_FLAG && t != mProfData[frame][0]) {
+ for (TileData t : mProfData.frames[frame]) {
+ if (t.level == INVAL_FLAG && t != mProfData.frames[frame][0]) {
TileDrawable invalShape = new TileDrawable(t,
R.color.inval_region_start);
ValueAnimator tileAnimator = ObjectAnimator.ofInt(invalShape,
@@ -186,26 +203,20 @@
}
}
- mRenderStrings[0] = getResources().getString(R.string.format_stat_name,
- getResources().getString(R.string.ready_tiles), readyTiles);
- mRenderStrings[1] = getResources().getString(R.string.format_stat_name,
- getResources().getString(R.string.unready_tiles), unreadyTiles);
- mRenderStrings[2] = getResources().getString(R.string.format_stat_name,
- getResources().getString(R.string.unplaced_tiles),
- unplacedTiles);
- mRenderStrings[3] = getResources().getString(R.string.format_stat_name,
- getResources().getString(R.string.number_invalidates),
- numInvals);
+ mRenderStrings.add(statString(R.string.ready_tiles, readyTiles));
+ mRenderStrings.add(statString(R.string.unready_tiles, unreadyTiles));
+ mRenderStrings.add(statString(R.string.unplaced_tiles, unplacedTiles));
+ mRenderStrings.add(statString(R.string.number_invalidates, numInvals));
// draw view rect (using first TileData object, on top)
- TileDrawable viewShape = new TileDrawable(mProfData[frame][0],
+ TileDrawable viewShape = new TileDrawable(mProfData.frames[frame][0],
R.color.view);
mTempShapes.add(viewShape);
this.invalidate();
return frame;
}
- public void setData(TileData[][] tileProfilingData) {
+ public void setData(RunData tileProfilingData) {
mProfData = tileProfilingData;
mGraphs.setData(mProfData);
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
index 1521807..a63a2f0 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
@@ -51,11 +51,11 @@
public class ProfileActivity extends Activity {
public interface ProfileCallback {
- public void profileCallback(TileData data[][]);
+ public void profileCallback(RunData data);
}
public static final String TEMP_FILENAME = "profile.tiles";
- private static final int LOAD_TEST_DELAY = 2000; // nr of millis after load,
+ private static final int LOAD_TEST_DELAY = 1000; // nr of millis after load,
// before test
Button mInspectButton;
@@ -135,6 +135,7 @@
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
view.requestFocus();
+
new CountDownTimer(LOAD_TEST_DELAY, LOAD_TEST_DELAY) {
@Override
public void onTick(long millisUntilFinished) {
@@ -155,10 +156,10 @@
}
private class StoreFileTask extends
- AsyncTask<Pair<String, TileData[][]>, Void, Void> {
+ AsyncTask<Pair<String, RunData>, Void, Void> {
@Override
- protected Void doInBackground(Pair<String, TileData[][]>... params) {
+ protected Void doInBackground(Pair<String, RunData>... params) {
try {
FileOutputStream fos = openFileOutput(params[0].first,
Context.MODE_PRIVATE);
@@ -205,10 +206,8 @@
/** auto - automatically scroll. */
private void startViewProfiling(boolean auto) {
- if (!auto) {
- // manual, toggle capture button to indicate capture state to user
- mCaptureButton.setChecked(true);
- }
+ // toggle capture button to indicate capture state to user
+ mCaptureButton.setChecked(true);
mWeb.startScrollTest(mCallback, auto);
setTestingState(TestingState.START_TESTING);
}
@@ -227,8 +226,8 @@
mCallback = new ProfileCallback() {
@SuppressWarnings("unchecked")
@Override
- public void profileCallback(TileData[][] data) {
- new StoreFileTask().execute(new Pair<String, TileData[][]>(
+ public void profileCallback(RunData data) {
+ new StoreFileTask().execute(new Pair<String, RunData>(
TEMP_FILENAME, data));
mCaptureButton.setChecked(false);
setTestingState(TestingState.STOP_TESTING);
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
index d3941be..3fc4665 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
@@ -18,15 +18,19 @@
import android.content.Context;
import android.util.AttributeSet;
+import android.util.Log;
import android.webkit.WebView;
import com.test.tilebenchmark.ProfileActivity.ProfileCallback;
+import com.test.tilebenchmark.RunData.TileData;
public class ProfiledWebView extends WebView {
private int mSpeed;
+ private boolean isTesting = false;
private boolean isScrolling = false;
private ProfileCallback mCallback;
+ private long mContentInvalMillis;
public ProfiledWebView(Context context) {
super(context);
@@ -47,7 +51,7 @@
@Override
protected void onDraw(android.graphics.Canvas canvas) {
- if (isScrolling) {
+ if (isTesting && isScrolling) {
if (canScrollVertically(1)) {
scrollBy(0, mSpeed);
} else {
@@ -60,31 +64,53 @@
/*
* Called once the page is loaded to start scrolling for evaluating tiles.
- * If autoScrolling isn't set, stop must be called manually.
+ * If autoScrolling isn't set, stop must be called manually. Before
+ * scrolling, invalidate all content and redraw it, measuring time taken.
*/
public void startScrollTest(ProfileCallback callback, boolean autoScrolling) {
isScrolling = autoScrolling;
mCallback = callback;
- tileProfilingStart();
+ isTesting = false;
+ mContentInvalMillis = System.currentTimeMillis();
+ registerPageSwapCallback();
+ contentInvalidateAll();
invalidate();
}
/*
+ * Called after the manual contentInvalidateAll, after the tiles have all
+ * been redrawn.
+ */
+ @Override
+ protected void pageSwapCallback() {
+ mContentInvalMillis = System.currentTimeMillis() - mContentInvalMillis;
+ super.pageSwapCallback();
+ Log.d("ProfiledWebView", "REDRAW TOOK " + mContentInvalMillis
+ + "millis");
+ isTesting = true;
+ invalidate(); // ensure a redraw so that auto-scrolling can occur
+ tileProfilingStart();
+ }
+
+ /*
* Called once the page has stopped scrolling
*/
public void stopScrollTest() {
- super.tileProfilingStop();
+ tileProfilingStop();
+ isTesting = false;
if (mCallback == null) {
tileProfilingClear();
return;
}
- TileData data[][] = new TileData[super.tileProfilingNumFrames()][];
- for (int frame = 0; frame < data.length; frame++) {
- data[frame] = new TileData[
+ RunData data = new RunData(super.tileProfilingNumFrames());
+ data.singleStats.put(getResources().getString(R.string.render_millis),
+ (double)mContentInvalMillis);
+ for (int frame = 0; frame < data.frames.length; frame++) {
+ data.frames[frame] = new TileData[
tileProfilingNumTilesInFrame(frame)];
- for (int tile = 0; tile < data[frame].length; tile++) {
+ for (int tile = 0; tile < data.frames[frame].length; tile++) {
int left = tileProfilingGetInt(frame, tile, "left");
int top = tileProfilingGetInt(frame, tile, "top");
int right = tileProfilingGetInt(frame, tile, "right");
@@ -96,18 +122,18 @@
float scale = tileProfilingGetFloat(frame, tile, "scale");
- data[frame][tile] = new TileData(left, top, right, bottom,
+ data.frames[frame][tile] = data.new TileData(left, top, right, bottom,
isReady, level, scale);
}
}
- super.tileProfilingClear();
+ tileProfilingClear();
mCallback.profileCallback(data);
}
@Override
public void loadUrl(String url) {
- if (!url.startsWith("http://")) {
+ if (!url.startsWith("http://") && !url.startsWith("file://")) {
url = "http://" + url;
}
super.loadUrl(url);
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/RunData.java b/tests/TileBenchmark/src/com/test/tilebenchmark/RunData.java
new file mode 100644
index 0000000..2da61cc
--- /dev/null
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/RunData.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.test.tilebenchmark;
+
+import java.io.Serializable;
+import java.util.HashMap;
+
+public class RunData implements Serializable {
+ public TileData[][] frames;
+ public HashMap<String, Double> singleStats = new HashMap<String, Double>();
+
+ public RunData(int frames) {
+ this.frames = new TileData[frames][];
+ }
+
+ public class TileData implements Serializable {
+ public int left, top, right, bottom;
+ public boolean isReady;
+ public int level;
+ public float scale;
+
+ public TileData(int left, int top, int right, int bottom,
+ boolean isReady, int level, float scale) {
+ this.left = left;
+ this.right = right;
+ this.top = top;
+ this.bottom = bottom;
+ this.isReady = isReady;
+ this.level = level;
+ this.scale = scale;
+ }
+
+ public String toString() {
+ return "Tile (" + left + "," + top + ")->("
+ + right + "," + bottom + ")";
+ }
+ }
+
+}
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/TileData.java b/tests/TileBenchmark/src/com/test/tilebenchmark/TileData.java
deleted file mode 100644
index 3e729a6..0000000
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/TileData.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.test.tilebenchmark;
-
-import java.io.Serializable;
-
-public class TileData implements Serializable {
- int left, top, right, bottom;
- public boolean isReady;
- public int level;
- public float scale;
-
- public TileData(int left, int top, int right, int bottom, boolean isReady,
- int level, float scale) {
- this.left = left;
- this.right = right;
- this.top = top;
- this.bottom = bottom;
- this.isReady = isReady;
- this.level = level;
- this.scale = scale;
- }
-
- public String toString() {
- return "Tile (" + left + "," + top + ")->("
- + right + "," + bottom + ")";
- }
-}
diff --git a/tests/TileBenchmark/tests/Android.mk b/tests/TileBenchmark/tests/Android.mk
new file mode 100644
index 0000000..8b235ec
--- /dev/null
+++ b/tests/TileBenchmark/tests/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := TileBenchmarkTests
+
+LOCAL_INSTRUMENTATION_FOR := TileBenchmark
+
+include $(BUILD_PACKAGE)
diff --git a/tests/TileBenchmark/tests/AndroidManifest.xml b/tests/TileBenchmark/tests/AndroidManifest.xml
new file mode 100644
index 0000000..703b152
--- /dev/null
+++ b/tests/TileBenchmark/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.test.tilebenchmark.tests">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.test.tilebenchmark"
+ android:label="Tests for WebView Tiles."/>
+</manifest>
diff --git a/tests/TileBenchmark/tests/src/com/test/tilebenchmark/PerformanceTest.java b/tests/TileBenchmark/tests/src/com/test/tilebenchmark/PerformanceTest.java
new file mode 100644
index 0000000..0f02239
--- /dev/null
+++ b/tests/TileBenchmark/tests/src/com/test/tilebenchmark/PerformanceTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.test.tilebenchmark;
+
+import com.test.tilebenchmark.ProfileActivity.ProfileCallback;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Environment;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+public class PerformanceTest extends
+ ActivityInstrumentationTestCase2<ProfileActivity> {
+
+ private class StatAggregator extends PlaybackGraphs {
+ private HashMap<String, Double> mDataMap = new HashMap<String, Double>();
+ private int mCount = 0;
+
+ public void aggregate() {
+ mCount++;
+ Resources resources = mView.getResources();
+ for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
+ for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
+ String metricLabel = resources.getString(
+ Metrics[metricIndex].getLabelId());
+ String statLabel = resources.getString(
+ Stats[statIndex].getLabelId());
+
+ String label = metricLabel + " " + statLabel;
+ double aggVal = mDataMap.containsKey(label) ? mDataMap
+ .get(label) : 0;
+
+ aggVal += mStats[metricIndex][statIndex];
+ mDataMap.put(label, aggVal);
+ }
+ }
+ for (Map.Entry<String, Double> e : mSingleStats.entrySet()) {
+ double aggVal = mDataMap.containsKey(e.getKey())
+ ? mDataMap.get(e.getKey()) : 0;
+ mDataMap.put(e.getKey(), aggVal + e.getValue());
+ }
+ }
+
+ public Bundle getBundle() {
+ Bundle b = new Bundle();
+ int count = 0 == mCount ? Integer.MAX_VALUE : mCount;
+ for (Map.Entry<String, Double> e : mDataMap.entrySet()) {
+ b.putDouble(e.getKey(), e.getValue() / count);
+ }
+ return b;
+ }
+ }
+
+ ProfileActivity mActivity;
+ ProfiledWebView mView;
+ StatAggregator mStats = new StatAggregator();
+
+ private static final String LOGTAG = "PerformanceTest";
+ private static final String TEST_LOCATION = "webkit/page_cycler";
+ private static final String URL_PREFIX = "file://";
+ private static final String URL_POSTFIX = "/index.html?skip=true";
+ private static final int MAX_ITERATIONS = 4;
+ private static final String TEST_DIRS[] = {
+ "alexa_us"//, "android", "dom", "intl1", "intl2", "moz", "moz2"
+ };
+
+ public PerformanceTest() {
+ super(ProfileActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mActivity = getActivity();
+ mView = (ProfiledWebView) mActivity.findViewById(R.id.web);
+ }
+
+ private boolean loadUrl(final String url) {
+ try {
+ Log.d(LOGTAG, "test starting for url " + url);
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mView.loadUrl(url);
+ }
+ });
+ synchronized (mStats) {
+ mStats.wait();
+ }
+ mStats.aggregate();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+
+ private boolean runIteration() {
+ File sdFile = Environment.getExternalStorageDirectory();
+ for (String testDirName : TEST_DIRS) {
+ File testDir = new File(sdFile, TEST_LOCATION + "/" + testDirName);
+ Log.d(LOGTAG, "Testing dir: '" + testDir.getAbsolutePath()
+ + "', exists=" + testDir.exists());
+ for (File siteDir : testDir.listFiles()) {
+ if (!siteDir.isDirectory())
+ continue;
+
+ if (!loadUrl(URL_PREFIX + siteDir.getAbsolutePath()
+ + URL_POSTFIX)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public void testMetrics() {
+ String state = Environment.getExternalStorageState();
+
+ if (!Environment.MEDIA_MOUNTED.equals(state)
+ && !Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
+ Log.d(LOGTAG, "ARG Can't access sd card!");
+ // Can't read the SD card, fail and die!
+ getInstrumentation().sendStatus(1, null);
+ return;
+ }
+
+ // use mGraphs as a condition variable between the UI thread and
+ // this(the testing) thread
+ mActivity.setCallback(new ProfileCallback() {
+ @Override
+ public void profileCallback(RunData data) {
+ Log.d(LOGTAG, "test completion callback");
+ mStats.setData(data);
+ synchronized (mStats) {
+ mStats.notify();
+ }
+ }
+ });
+
+ for (int i = 0; i < MAX_ITERATIONS; i++)
+ if (!runIteration()) {
+ getInstrumentation().sendStatus(1, null);
+ return;
+ }
+ getInstrumentation().sendStatus(0, mStats.getBundle());
+ }
+}