diff options
Diffstat (limited to 'core/java/android')
42 files changed, 2029 insertions, 466 deletions
diff --git a/core/java/android/animation/TypeEvaluator.java b/core/java/android/animation/TypeEvaluator.java index e738da1..2640457 100644 --- a/core/java/android/animation/TypeEvaluator.java +++ b/core/java/android/animation/TypeEvaluator.java @@ -19,7 +19,7 @@ package android.animation; /** * Interface for use with the {@link ValueAnimator#setEvaluator(TypeEvaluator)} function. Evaluators * allow developers to create animations on arbitrary property types, by allowing them to supply - * custom evaulators for types that are not automatically understood and used by the animation + * custom evaluators for types that are not automatically understood and used by the animation * system. * * @see ValueAnimator#setEvaluator(TypeEvaluator) @@ -41,4 +41,4 @@ public interface TypeEvaluator<T> { */ public T evaluate(float fraction, T startValue, T endValue); -}
\ No newline at end of file +} diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 2a28b76..e6960b3 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -3865,7 +3865,7 @@ public final class ActivityThread { } } - final void freeTextLayoutCachesIfNeeded(int configDiff) { + static void freeTextLayoutCachesIfNeeded(int configDiff) { if (configDiff != 0) { // Ask text layout engine to free its caches if there is a locale change boolean hasLocaleConfigChange = ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0); diff --git a/core/java/android/app/NativeActivity.java b/core/java/android/app/NativeActivity.java index b84889f..4ca3747 100644 --- a/core/java/android/app/NativeActivity.java +++ b/core/java/android/app/NativeActivity.java @@ -175,16 +175,20 @@ public class NativeActivity extends Activity implements SurfaceHolder.Callback2, ? savedInstanceState.getByteArray(KEY_NATIVE_SAVED_STATE) : null; mNativeHandle = loadNativeCode(path, funcname, Looper.myQueue(), - getFilesDir().getAbsolutePath(), getObbDir().getAbsolutePath(), - getExternalFilesDir(null).getAbsolutePath(), - Build.VERSION.SDK_INT, getAssets(), nativeSavedState); - + getAbsolutePath(getFilesDir()), getAbsolutePath(getObbDir()), + getAbsolutePath(getExternalFilesDir(null)), + Build.VERSION.SDK_INT, getAssets(), nativeSavedState); + if (mNativeHandle == 0) { throw new IllegalArgumentException("Unable to load native library: " + path); } super.onCreate(savedInstanceState); } + private static String getAbsolutePath(File file) { + return (file != null) ? file.getAbsolutePath() : null; + } + @Override protected void onDestroy() { mDestroyed = true; diff --git a/core/java/android/bluetooth/BluetoothA2dp.java b/core/java/android/bluetooth/BluetoothA2dp.java index d7d8cdb..e7e4a0f 100644 --- a/core/java/android/bluetooth/BluetoothA2dp.java +++ b/core/java/android/bluetooth/BluetoothA2dp.java @@ -388,6 +388,66 @@ public final class BluetoothA2dp implements BluetoothProfile { } /** + * Checks if Avrcp device supports the absolute volume feature. + * + * @return true if device supports absolute volume + * @hide + */ + public boolean isAvrcpAbsoluteVolumeSupported() { + if (DBG) Log.d(TAG, "isAvrcpAbsoluteVolumeSupported"); + if (mService != null && isEnabled()) { + try { + return mService.isAvrcpAbsoluteVolumeSupported(); + } catch (RemoteException e) { + Log.e(TAG, "Error talking to BT service in isAvrcpAbsoluteVolumeSupported()", e); + return false; + } + } + if (mService == null) Log.w(TAG, "Proxy not attached to service"); + return false; + } + + /** + * Tells remote device to adjust volume. Only if absolute volume is supported. + * + * @param direction 1 to increase volume, or -1 to decrease volume + * @hide + */ + public void adjustAvrcpAbsoluteVolume(int direction) { + if (DBG) Log.d(TAG, "adjustAvrcpAbsoluteVolume"); + if (mService != null && isEnabled()) { + try { + mService.adjustAvrcpAbsoluteVolume(direction); + return; + } catch (RemoteException e) { + Log.e(TAG, "Error talking to BT service in adjustAvrcpAbsoluteVolume()", e); + return; + } + } + if (mService == null) Log.w(TAG, "Proxy not attached to service"); + } + + /** + * Tells remote device to set an absolute volume. Only if absolute volume is supported + * + * @param volume Absolute volume to be set on AVRCP side + * @hide + */ + public void setAvrcpAbsoluteVolume(int volume) { + if (DBG) Log.d(TAG, "setAvrcpAbsoluteVolume"); + if (mService != null && isEnabled()) { + try { + mService.setAvrcpAbsoluteVolume(volume); + return; + } catch (RemoteException e) { + Log.e(TAG, "Error talking to BT service in setAvrcpAbsoluteVolume()", e); + return; + } + } + if (mService == null) Log.w(TAG, "Proxy not attached to service"); + } + + /** * Check if A2DP profile is streaming music. * * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. diff --git a/core/java/android/bluetooth/BluetoothUuid.java b/core/java/android/bluetooth/BluetoothUuid.java index fe66fbd..6609b98 100644 --- a/core/java/android/bluetooth/BluetoothUuid.java +++ b/core/java/android/bluetooth/BluetoothUuid.java @@ -56,6 +56,8 @@ public final class BluetoothUuid { ParcelUuid.fromString("00001105-0000-1000-8000-00805f9b34fb"); public static final ParcelUuid Hid = ParcelUuid.fromString("00001124-0000-1000-8000-00805f9b34fb"); + public static final ParcelUuid Hogp = + ParcelUuid.fromString("00001812-0000-1000-8000-00805f9b34fb"); public static final ParcelUuid PANU = ParcelUuid.fromString("00001115-0000-1000-8000-00805F9B34FB"); public static final ParcelUuid NAP = diff --git a/core/java/android/bluetooth/IBluetoothA2dp.aidl b/core/java/android/bluetooth/IBluetoothA2dp.aidl index 1f10998..26ff9e2 100644 --- a/core/java/android/bluetooth/IBluetoothA2dp.aidl +++ b/core/java/android/bluetooth/IBluetoothA2dp.aidl @@ -32,5 +32,8 @@ interface IBluetoothA2dp { int getConnectionState(in BluetoothDevice device); boolean setPriority(in BluetoothDevice device, int priority); int getPriority(in BluetoothDevice device); + boolean isAvrcpAbsoluteVolumeSupported(); + oneway void adjustAvrcpAbsoluteVolume(int direction); + oneway void setAvrcpAbsoluteVolume(int volume); boolean isA2dpPlaying(in BluetoothDevice device); } diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 45fed2d..5cabfee 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -2060,7 +2060,7 @@ public abstract class ContentResolver { private final class ParcelFileDescriptorInner extends ParcelFileDescriptor { private final IContentProvider mContentProvider; - private boolean mReleaseProviderFlag = false; + private boolean mProviderReleased; ParcelFileDescriptorInner(ParcelFileDescriptor pfd, IContentProvider icp) { super(pfd); @@ -2069,17 +2069,10 @@ public abstract class ContentResolver { @Override public void close() throws IOException { - if(!mReleaseProviderFlag) { - super.close(); + super.close(); + if (!mProviderReleased) { ContentResolver.this.releaseProvider(mContentProvider); - mReleaseProviderFlag = true; - } - } - - @Override - protected void finalize() throws Throwable { - if (!mReleaseProviderFlag) { - close(); + mProviderReleased = true; } } } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index ff350b9..017ad98 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -2678,6 +2678,10 @@ public class Intent implements Parcelable, Cloneable { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT"; + /** {@hide} */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_MANAGE_DOCUMENT = "android.intent.action.MANAGE_DOCUMENT"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent categories (see addCategory()). diff --git a/core/java/android/database/MatrixCursor.java b/core/java/android/database/MatrixCursor.java index 6e68b6b..2a0d9b9 100644 --- a/core/java/android/database/MatrixCursor.java +++ b/core/java/android/database/MatrixCursor.java @@ -83,11 +83,10 @@ public class MatrixCursor extends AbstractCursor { * row */ public RowBuilder newRow() { - rowCount++; - int endIndex = rowCount * columnCount; + final int row = rowCount++; + final int endIndex = rowCount * columnCount; ensureCapacity(endIndex); - int start = endIndex - columnCount; - return new RowBuilder(start, endIndex); + return new RowBuilder(row); } /** @@ -180,18 +179,29 @@ public class MatrixCursor extends AbstractCursor { } /** - * Builds a row, starting from the left-most column and adding one column - * value at a time. Follows the same ordering as the column names specified - * at cursor construction time. + * Builds a row of values using either of these approaches: + * <ul> + * <li>Values can be added with explicit column ordering using + * {@link #add(Object)}, which starts from the left-most column and adds one + * column value at a time. This follows the same ordering as the column + * names specified at cursor construction time. + * <li>Column and value pairs can be offered for possible inclusion using + * {@link #offer(String, Object)}. If the cursor includes the given column, + * the value will be set for that column, otherwise the value is ignored. + * This approach is useful when matching data to a custom projection. + * </ul> + * Undefined values are left as {@code null}. */ public class RowBuilder { + private final int row; + private final int endIndex; private int index; - private final int endIndex; - RowBuilder(int index, int endIndex) { - this.index = index; - this.endIndex = endIndex; + RowBuilder(int row) { + this.row = row; + this.index = row * columnCount; + this.endIndex = index + columnCount; } /** @@ -210,6 +220,21 @@ public class MatrixCursor extends AbstractCursor { data[index++] = columnValue; return this; } + + /** + * Offer value for possible inclusion if this cursor defines the given + * column. Columns not defined by the cursor are silently ignored. + * + * @return this builder to support chaining + */ + public RowBuilder offer(String columnName, Object value) { + for (int i = 0; i < columnNames.length; i++) { + if (columnName.equals(columnNames[i])) { + data[(row * columnCount) + i] = value; + } + } + return this; + } } // AbstractCursor implementation. diff --git a/core/java/android/gesture/GestureOverlayView.java b/core/java/android/gesture/GestureOverlayView.java index b6c260f..2d47f28 100644 --- a/core/java/android/gesture/GestureOverlayView.java +++ b/core/java/android/gesture/GestureOverlayView.java @@ -486,6 +486,7 @@ public class GestureOverlayView extends FrameLayout { @Override protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); cancelClearAnimation(); } diff --git a/core/java/android/hardware/ICameraService.aidl b/core/java/android/hardware/ICameraService.aidl index 81e564e..fc54828 100644 --- a/core/java/android/hardware/ICameraService.aidl +++ b/core/java/android/hardware/ICameraService.aidl @@ -22,6 +22,7 @@ import android.hardware.IProCameraUser; import android.hardware.IProCameraCallbacks; import android.hardware.camera2.ICameraDeviceUser; import android.hardware.camera2.ICameraDeviceCallbacks; +import android.hardware.camera2.utils.BinderHolder; import android.hardware.ICameraServiceListener; import android.hardware.CameraInfo; @@ -37,17 +38,23 @@ interface ICameraService int getCameraInfo(int cameraId, out CameraInfo info); - ICamera connect(ICameraClient client, int cameraId, + int connect(ICameraClient client, int cameraId, String clientPackageName, - int clientUid); + int clientUid, + // Container for an ICamera object + out BinderHolder device); - IProCameraUser connectPro(IProCameraCallbacks callbacks, int cameraId, + int connectPro(IProCameraCallbacks callbacks, int cameraId, String clientPackageName, - int clientUid); + int clientUid, + // Container for an IProCameraUser object + out BinderHolder device); - ICameraDeviceUser connectDevice(ICameraDeviceCallbacks callbacks, int cameraId, + int connectDevice(ICameraDeviceCallbacks callbacks, int cameraId, String clientPackageName, - int clientUid); + int clientUid, + // Container for an ICameraDeviceUser object + out BinderHolder device); int addListener(ICameraServiceListener listener); int removeListener(ICameraServiceListener listener); diff --git a/core/java/android/hardware/camera2/CameraAccessException.java b/core/java/android/hardware/camera2/CameraAccessException.java index 0089f26..e08d1e6 100644 --- a/core/java/android/hardware/camera2/CameraAccessException.java +++ b/core/java/android/hardware/camera2/CameraAccessException.java @@ -48,11 +48,18 @@ public class CameraAccessException extends AndroidException { /** * The camera device is removable and has been disconnected from the Android - * device, or the camera service has shut down the connection due to a + * device, or the camera id used with {@link android.hardware.camera2.CameraManager#openCamera} + * is no longer valid, or the camera service has shut down the connection due to a * higher-priority access request for the camera device. */ public static final int CAMERA_DISCONNECTED = 4; + /** + * A deprecated HAL version is in use. + * @hide + */ + public static final int CAMERA_DEPRECATED_HAL = 1000; + // Make the eclipse warning about serializable exceptions go away private static final long serialVersionUID = 5630338637471475675L; // randomly generated diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 402c433..8903b4a 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -22,6 +22,7 @@ import android.hardware.ICameraServiceListener; import android.hardware.IProCameraUser; import android.hardware.camera2.utils.CameraBinderDecorator; import android.hardware.camera2.utils.CameraRuntimeException; +import android.hardware.camera2.utils.BinderHolder; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; @@ -171,10 +172,10 @@ public final class CameraManager { * * @param cameraId The unique identifier of the camera device to open * - * @throws IllegalArgumentException if the cameraId does not match any - * currently connected camera device. * @throws CameraAccessException if the camera is disabled by device policy, - * or too many camera devices are already open. + * or too many camera devices are already open, or the cameraId does not match + * any currently available camera device. + * * @throws SecurityException if the application does not have permission to * access the camera * @@ -192,16 +193,11 @@ public final class CameraManager { android.hardware.camera2.impl.CameraDevice device = new android.hardware.camera2.impl.CameraDevice(cameraId); - cameraUser = mCameraService.connectDevice(device.getCallbacks(), + BinderHolder holder = new BinderHolder(); + mCameraService.connectDevice(device.getCallbacks(), Integer.parseInt(cameraId), - mContext.getPackageName(), USE_CALLING_UID); - - // TODO: change ICameraService#connectDevice to return status_t - if (cameraUser == null) { - // TEMPORARY CODE. - // catch-all exception since we aren't yet getting the actual error code - throw new IllegalStateException("Failed to open camera device"); - } + mContext.getPackageName(), USE_CALLING_UID, holder); + cameraUser = ICameraDeviceUser.Stub.asInterface(holder.getBinder()); // TODO: factor out listener to be non-nested, then move setter to constructor device.setRemoteDevice(cameraUser); @@ -214,12 +210,7 @@ public final class CameraManager { throw new IllegalArgumentException("Expected cameraId to be numeric, but it was: " + cameraId); } catch (CameraRuntimeException e) { - if (e.getReason() == CameraAccessException.CAMERA_DISCONNECTED) { - throw new IllegalArgumentException("Invalid camera ID specified -- " + - "perhaps the camera was physically disconnected", e); - } else { - throw e.asChecked(); - } + throw e.asChecked(); } catch (RemoteException e) { // impossible return null; diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 046f1cd..28225e6 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -53,6 +53,7 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { private final Object mLock = new Object(); private final HashSet<Surface> mSurfaceSet = new HashSet<Surface>(); + private Object mUserTag; /** * @hide @@ -89,6 +90,42 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { } } + /** + * Set a tag for this request. + * + * <p>This tag is not used for anything by the camera device, but can be + * used by an application to easily identify a CaptureRequest when it is + * returned by + * {@link CameraDevice.CaptureListener#onCaptureComplete CaptureListener.onCaptureComplete} + * + * @param tag an arbitrary Object to store with this request + * @see #getTag + */ + public void setTag(Object tag) { + synchronized (mLock) { + mUserTag = tag; + } + } + + /** + * Retrieve the tag for this request, if any. + * + * <p>This tag is not used for anything by the camera device, but can be + * used by an application to easily identify a CaptureRequest when it is + * returned by + * {@link CameraDevice.CaptureListener#onCaptureComplete CaptureListener.onCaptureComplete} + * </p> + * + * @return the last tag Object set on this request, or {@code null} if + * no tag has been set. + * @see #setTag + */ + public Object getTag() { + synchronized (mLock) { + return mUserTag; + } + } + public static final Parcelable.Creator<CaptureRequest> CREATOR = new Parcelable.Creator<CaptureRequest>() { @Override diff --git a/core/java/android/hardware/camera2/utils/BinderHolder.aidl b/core/java/android/hardware/camera2/utils/BinderHolder.aidl new file mode 100644 index 0000000..f39d645 --- /dev/null +++ b/core/java/android/hardware/camera2/utils/BinderHolder.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2013 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.hardware.camera2.utils; + +/** @hide */ +parcelable BinderHolder; diff --git a/core/java/android/hardware/camera2/utils/BinderHolder.java b/core/java/android/hardware/camera2/utils/BinderHolder.java new file mode 100644 index 0000000..9eea390 --- /dev/null +++ b/core/java/android/hardware/camera2/utils/BinderHolder.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2013 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.hardware.camera2.utils; + +import android.os.Parcel; +import android.os.Parcelable; +import android.os.IBinder; + +/** + * @hide + */ +public class BinderHolder implements Parcelable { + private IBinder mBinder = null; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mBinder); + } + + public void readFromParcel(Parcel src) { + mBinder = src.readStrongBinder(); + } + + public static final Parcelable.Creator<BinderHolder> CREATOR = + new Parcelable.Creator<BinderHolder>() { + @Override + public BinderHolder createFromParcel(Parcel in) { + return new BinderHolder(in); + } + + @Override + public BinderHolder[] newArray(int size) { + return new BinderHolder[size]; + } + }; + + public IBinder getBinder() { + return mBinder; + } + + public void setBinder(IBinder binder) { + mBinder = binder; + } + + public BinderHolder() {} + + public BinderHolder(IBinder binder) { + mBinder = binder; + } + + private BinderHolder(Parcel in) { + mBinder = in.readStrongBinder(); + } +} + diff --git a/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java b/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java index 586c759..fbe7ff4 100644 --- a/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java +++ b/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java @@ -19,6 +19,7 @@ package android.hardware.camera2.utils; import static android.hardware.camera2.CameraAccessException.CAMERA_DISABLED; import static android.hardware.camera2.CameraAccessException.CAMERA_DISCONNECTED; import static android.hardware.camera2.CameraAccessException.CAMERA_IN_USE; +import static android.hardware.camera2.CameraAccessException.CAMERA_DEPRECATED_HAL; import android.os.DeadObjectException; import android.os.RemoteException; @@ -48,6 +49,7 @@ public class CameraBinderDecorator { public static final int EACCES = -13; public static final int EBUSY = -16; public static final int ENODEV = -19; + public static final int ENOTSUP = -129; private static class CameraBinderDecoratorListener implements Decorator.DecoratorListener { @@ -75,9 +77,6 @@ public class CameraBinderDecorator { case DEAD_OBJECT: UncheckedThrow.throwAnyException(new CameraRuntimeException( CAMERA_DISCONNECTED)); - // TODO: Camera service (native side) should return - // EACCES error - // when there's a policy manager disabled causing this case EACCES: UncheckedThrow.throwAnyException(new CameraRuntimeException( CAMERA_DISABLED)); @@ -87,6 +86,9 @@ public class CameraBinderDecorator { case ENODEV: UncheckedThrow.throwAnyException(new CameraRuntimeException( CAMERA_DISCONNECTED)); + case ENOTSUP: + UncheckedThrow.throwAnyException(new CameraRuntimeException( + CAMERA_DEPRECATED_HAL)); } /** diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 12f7915..46b0150 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -1520,6 +1520,11 @@ public final class Parcel { return fd != null ? new ParcelFileDescriptor(fd) : null; } + /** {@hide} */ + public final FileDescriptor readRawFileDescriptor() { + return nativeReadFileDescriptor(mNativePtr); + } + /*package*/ static native FileDescriptor openFileDescriptor(String file, int mode) throws FileNotFoundException; /*package*/ static native FileDescriptor dupFileDescriptor(FileDescriptor orig) diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 3de362c..579971d 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -16,8 +16,25 @@ package android.os; +import static libcore.io.OsConstants.AF_UNIX; +import static libcore.io.OsConstants.SEEK_SET; +import static libcore.io.OsConstants.SOCK_STREAM; +import static libcore.io.OsConstants.S_ISLNK; +import static libcore.io.OsConstants.S_ISREG; + +import android.content.BroadcastReceiver; +import android.content.ContentProvider; +import android.util.Log; + import dalvik.system.CloseGuard; +import libcore.io.ErrnoException; +import libcore.io.IoUtils; +import libcore.io.Libcore; +import libcore.io.Memory; +import libcore.io.OsConstants; +import libcore.io.StructStat; + import java.io.Closeable; import java.io.File; import java.io.FileDescriptor; @@ -27,36 +44,80 @@ import java.io.FileOutputStream; import java.io.IOException; import java.net.DatagramSocket; import java.net.Socket; +import java.nio.ByteOrder; /** * The FileDescriptor returned by {@link Parcel#readFileDescriptor}, allowing * you to close it when done with it. */ public class ParcelFileDescriptor implements Parcelable, Closeable { - private final FileDescriptor mFileDescriptor; + private static final String TAG = "ParcelFileDescriptor"; + + private final FileDescriptor mFd; + + /** + * Optional socket used to communicate close events, status at close, and + * detect remote process crashes. + */ + private FileDescriptor mCommFd; /** * Wrapped {@link ParcelFileDescriptor}, if any. Used to avoid - * double-closing {@link #mFileDescriptor}. + * double-closing {@link #mFd}. */ private final ParcelFileDescriptor mWrapped; + /** + * Maximum {@link #mStatusBuf} size; longer status messages will be + * truncated. + */ + private static final int MAX_STATUS = 1024; + + /** + * Temporary buffer used by {@link #readCommStatus(FileDescriptor, byte[])}, + * allocated on-demand. + */ + private byte[] mStatusBuf; + + /** + * Status read by {@link #checkError(boolean)}, or null if not read yet. + */ + private Status mStatus; + private volatile boolean mClosed; private final CloseGuard mGuard = CloseGuard.get(); /** - * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied - * and this file doesn't already exist, then create the file with - * permissions such that any application can read it. + * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied and + * this file doesn't already exist, then create the file with permissions + * such that any application can read it. + * + * @deprecated Creating world-readable files is very dangerous, and likely + * to cause security holes in applications. It is strongly + * discouraged; instead, applications should use more formal + * mechanism for interactions such as {@link ContentProvider}, + * {@link BroadcastReceiver}, and {@link android.app.Service}. + * There are no guarantees that this access mode will remain on + * a file, such as when it goes through a backup and restore. */ + @Deprecated public static final int MODE_WORLD_READABLE = 0x00000001; /** - * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied - * and this file doesn't already exist, then create the file with - * permissions such that any application can write it. + * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied and + * this file doesn't already exist, then create the file with permissions + * such that any application can write it. + * + * @deprecated Creating world-writable files is very dangerous, and likely + * to cause security holes in applications. It is strongly + * discouraged; instead, applications should use more formal + * mechanism for interactions such as {@link ContentProvider}, + * {@link BroadcastReceiver}, and {@link android.app.Service}. + * There are no guarantees that this access mode will remain on + * a file, such as when it goes through a backup and restore. */ + @Deprecated public static final int MODE_WORLD_WRITEABLE = 0x00000002; /** @@ -90,32 +151,102 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { public static final int MODE_APPEND = 0x02000000; /** + * Create a new ParcelFileDescriptor wrapped around another descriptor. By + * default all method calls are delegated to the wrapped descriptor. + */ + public ParcelFileDescriptor(ParcelFileDescriptor wrapped) { + // We keep a strong reference to the wrapped PFD, and rely on its + // finalizer to trigger CloseGuard. All calls are delegated to wrapper. + mWrapped = wrapped; + mFd = null; + mCommFd = null; + mClosed = true; + } + + /** {@hide} */ + public ParcelFileDescriptor(FileDescriptor fd) { + this(fd, null); + } + + /** {@hide} */ + public ParcelFileDescriptor(FileDescriptor fd, FileDescriptor commChannel) { + if (fd == null) { + throw new NullPointerException("FileDescriptor must not be null"); + } + mWrapped = null; + mFd = fd; + mCommFd = commChannel; + mGuard.open("close"); + } + + /** * Create a new ParcelFileDescriptor accessing a given file. * * @param file The file to be opened. * @param mode The desired access mode, must be one of - * {@link #MODE_READ_ONLY}, {@link #MODE_WRITE_ONLY}, or - * {@link #MODE_READ_WRITE}; may also be any combination of - * {@link #MODE_CREATE}, {@link #MODE_TRUNCATE}, - * {@link #MODE_WORLD_READABLE}, and {@link #MODE_WORLD_WRITEABLE}. - * - * @return Returns a new ParcelFileDescriptor pointing to the given - * file. + * {@link #MODE_READ_ONLY}, {@link #MODE_WRITE_ONLY}, or + * {@link #MODE_READ_WRITE}; may also be any combination of + * {@link #MODE_CREATE}, {@link #MODE_TRUNCATE}, + * {@link #MODE_WORLD_READABLE}, and + * {@link #MODE_WORLD_WRITEABLE}. + * @return a new ParcelFileDescriptor pointing to the given file. + * @throws FileNotFoundException if the given file does not exist or can not + * be opened with the requested mode. + */ + public static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException { + final FileDescriptor fd = openInternal(file, mode); + if (fd == null) return null; + + return new ParcelFileDescriptor(fd); + } + + /** + * Create a new ParcelFileDescriptor accessing a given file. * - * @throws FileNotFoundException Throws FileNotFoundException if the given - * file does not exist or can not be opened with the requested mode. + * @param file The file to be opened. + * @param mode The desired access mode, must be one of + * {@link #MODE_READ_ONLY}, {@link #MODE_WRITE_ONLY}, or + * {@link #MODE_READ_WRITE}; may also be any combination of + * {@link #MODE_CREATE}, {@link #MODE_TRUNCATE}, + * {@link #MODE_WORLD_READABLE}, and + * {@link #MODE_WORLD_WRITEABLE}. + * @param handler to call listener from; must not be null. + * @param listener to be invoked when the returned descriptor has been + * closed; must not be null. + * @return a new ParcelFileDescriptor pointing to the given file. + * @throws FileNotFoundException if the given file does not exist or can not + * be opened with the requested mode. */ - public static ParcelFileDescriptor open(File file, int mode) - throws FileNotFoundException { - String path = file.getPath(); + public static ParcelFileDescriptor open( + File file, int mode, Handler handler, OnCloseListener listener) throws IOException { + if (handler == null) { + throw new IllegalArgumentException("Handler must not be null"); + } + if (listener == null) { + throw new IllegalArgumentException("Listener must not be null"); + } + + final FileDescriptor fd = openInternal(file, mode); + if (fd == null) return null; + + final FileDescriptor[] comm = createCommSocketPair(true); + final ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd, comm[0]); + + // Kick off thread to watch for status updates + final ListenerBridge bridge = new ListenerBridge(comm[1], handler.getLooper(), listener); + bridge.start(); + + return pfd; + } - if ((mode&MODE_READ_WRITE) == 0) { + private static FileDescriptor openInternal(File file, int mode) throws FileNotFoundException { + if ((mode & MODE_READ_WRITE) == 0) { throw new IllegalArgumentException( "Must specify MODE_READ_ONLY, MODE_WRITE_ONLY, or MODE_READ_WRITE"); } - FileDescriptor fd = Parcel.openFileDescriptor(path, mode); - return fd != null ? new ParcelFileDescriptor(fd) : null; + final String path = file.getPath(); + return Parcel.openFileDescriptor(path, mode); } /** @@ -125,8 +256,12 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * original file descriptor. */ public static ParcelFileDescriptor dup(FileDescriptor orig) throws IOException { - FileDescriptor fd = Parcel.dupFileDescriptor(orig); - return fd != null ? new ParcelFileDescriptor(fd) : null; + try { + final FileDescriptor fd = Libcore.os.dup(orig); + return new ParcelFileDescriptor(fd); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } } /** @@ -136,7 +271,11 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * original file descriptor. */ public ParcelFileDescriptor dup() throws IOException { - return dup(getFileDescriptor()); + if (mWrapped != null) { + return mWrapped.dup(); + } else { + return dup(getFileDescriptor()); + } } /** @@ -150,12 +289,16 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * for a dup of the given fd. */ public static ParcelFileDescriptor fromFd(int fd) throws IOException { - FileDescriptor fdesc = getFileDescriptorFromFd(fd); - return new ParcelFileDescriptor(fdesc); - } + final FileDescriptor original = new FileDescriptor(); + original.setInt$(fd); - // Extracts the file descriptor from the specified socket and returns it untouched - private static native FileDescriptor getFileDescriptorFromFd(int fd) throws IOException; + try { + final FileDescriptor dup = Libcore.os.dup(original); + return new ParcelFileDescriptor(dup); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } /** * Take ownership of a raw native fd in to a new ParcelFileDescriptor. @@ -168,13 +311,12 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * for the given fd. */ public static ParcelFileDescriptor adoptFd(int fd) { - FileDescriptor fdesc = getFileDescriptorFromFdNoDup(fd); + final FileDescriptor fdesc = new FileDescriptor(); + fdesc.setInt$(fd); + return new ParcelFileDescriptor(fdesc); } - // Extracts the file descriptor from the specified socket and returns it untouched - private static native FileDescriptor getFileDescriptorFromFdNoDup(int fd); - /** * Create a new ParcelFileDescriptor from the specified Socket. The new * ParcelFileDescriptor holds a dup of the original FileDescriptor in @@ -212,15 +354,90 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * is the write side. */ public static ParcelFileDescriptor[] createPipe() throws IOException { - FileDescriptor[] fds = new FileDescriptor[2]; - createPipeNative(fds); - ParcelFileDescriptor[] pfds = new ParcelFileDescriptor[2]; - pfds[0] = new ParcelFileDescriptor(fds[0]); - pfds[1] = new ParcelFileDescriptor(fds[1]); - return pfds; + try { + final FileDescriptor[] fds = Libcore.os.pipe(); + return new ParcelFileDescriptor[] { + new ParcelFileDescriptor(fds[0]), + new ParcelFileDescriptor(fds[1]) }; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + /** + * Create two ParcelFileDescriptors structured as a data pipe. The first + * ParcelFileDescriptor in the returned array is the read side; the second + * is the write side. + * <p> + * The write end has the ability to deliver an error message through + * {@link #closeWithError(String)} which can be handled by the read end + * calling {@link #checkError(boolean)}, usually after detecting an EOF. + * This can also be used to detect remote crashes. + */ + public static ParcelFileDescriptor[] createReliablePipe() throws IOException { + try { + final FileDescriptor[] comm = createCommSocketPair(false); + final FileDescriptor[] fds = Libcore.os.pipe(); + return new ParcelFileDescriptor[] { + new ParcelFileDescriptor(fds[0], comm[0]), + new ParcelFileDescriptor(fds[1], comm[1]) }; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } } - private static native void createPipeNative(FileDescriptor[] outFds) throws IOException; + /** + * Create two ParcelFileDescriptors structured as a pair of sockets + * connected to each other. The two sockets are indistinguishable. + */ + public static ParcelFileDescriptor[] createSocketPair() throws IOException { + try { + final FileDescriptor fd0 = new FileDescriptor(); + final FileDescriptor fd1 = new FileDescriptor(); + Libcore.os.socketpair(AF_UNIX, SOCK_STREAM, 0, fd0, fd1); + return new ParcelFileDescriptor[] { + new ParcelFileDescriptor(fd0), + new ParcelFileDescriptor(fd1) }; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + /** + * Create two ParcelFileDescriptors structured as a pair of sockets + * connected to each other. The two sockets are indistinguishable. + * <p> + * Both ends have the ability to deliver an error message through + * {@link #closeWithError(String)} which can be detected by the other end + * calling {@link #checkError(boolean)}, usually after detecting an EOF. + * This can also be used to detect remote crashes. + */ + public static ParcelFileDescriptor[] createReliableSocketPair() throws IOException { + try { + final FileDescriptor[] comm = createCommSocketPair(false); + final FileDescriptor fd0 = new FileDescriptor(); + final FileDescriptor fd1 = new FileDescriptor(); + Libcore.os.socketpair(AF_UNIX, SOCK_STREAM, 0, fd0, fd1); + return new ParcelFileDescriptor[] { + new ParcelFileDescriptor(fd0, comm[0]), + new ParcelFileDescriptor(fd1, comm[1]) }; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + private static FileDescriptor[] createCommSocketPair(boolean blocking) throws IOException { + try { + final FileDescriptor comm1 = new FileDescriptor(); + final FileDescriptor comm2 = new FileDescriptor(); + Libcore.os.socketpair(AF_UNIX, SOCK_STREAM, 0, comm1, comm2); + IoUtils.setBlocking(comm1, blocking); + IoUtils.setBlocking(comm2, blocking); + return new FileDescriptor[] { comm1, comm2 }; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } /** * @hide Please use createPipe() or ContentProvider.openPipeHelper(). @@ -250,21 +467,51 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * @return Returns the FileDescriptor associated with this object. */ public FileDescriptor getFileDescriptor() { - return mFileDescriptor; + if (mWrapped != null) { + return mWrapped.getFileDescriptor(); + } else { + return mFd; + } } /** - * Return the total size of the file representing this fd, as determined - * by stat(). Returns -1 if the fd is not a file. + * Return the total size of the file representing this fd, as determined by + * {@code stat()}. Returns -1 if the fd is not a file. */ - public native long getStatSize(); + public long getStatSize() { + if (mWrapped != null) { + return mWrapped.getStatSize(); + } else { + try { + final StructStat st = Libcore.os.fstat(mFd); + if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) { + return st.st_size; + } else { + return -1; + } + } catch (ErrnoException e) { + Log.w(TAG, "fstat() failed: " + e); + return -1; + } + } + } /** * This is needed for implementing AssetFileDescriptor.AutoCloseOutputStream, * and I really don't think we want it to be public. * @hide */ - public native long seekTo(long pos); + public long seekTo(long pos) throws IOException { + if (mWrapped != null) { + return mWrapped.seekTo(pos); + } else { + try { + return Libcore.os.lseek(mFd, pos, SEEK_SET); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + } /** * Return the native fd int for this ParcelFileDescriptor. The @@ -272,34 +519,39 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * through this API. */ public int getFd() { - if (mClosed) { - throw new IllegalStateException("Already closed"); + if (mWrapped != null) { + return mWrapped.getFd(); + } else { + if (mClosed) { + throw new IllegalStateException("Already closed"); + } + return mFd.getInt$(); } - return getFdNative(); } - private native int getFdNative(); - /** - * Return the native fd int for this ParcelFileDescriptor and detach it - * from the object here. You are now responsible for closing the fd in - * native code. + * Return the native fd int for this ParcelFileDescriptor and detach it from + * the object here. You are now responsible for closing the fd in native + * code. + * <p> + * You should not detach when the original creator of the descriptor is + * expecting a reliable signal through {@link #close()} or + * {@link #closeWithError(String)}. + * + * @see #canDetectErrors() */ public int detachFd() { - if (mClosed) { - throw new IllegalStateException("Already closed"); - } if (mWrapped != null) { - int fd = mWrapped.detachFd(); - mClosed = true; - mGuard.close(); + return mWrapped.detachFd(); + } else { + if (mClosed) { + throw new IllegalStateException("Already closed"); + } + final int fd = getFd(); + Parcel.clearFileDescriptor(mFd); + writeCommStatusAndClose(Status.DETACHED, null); return fd; } - int fd = getFd(); - mClosed = true; - mGuard.close(); - Parcel.clearFileDescriptor(mFileDescriptor); - return fd; } /** @@ -311,16 +563,176 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { */ @Override public void close() throws IOException { - if (mClosed) return; - mClosed = true; - mGuard.close(); - if (mWrapped != null) { - // If this is a proxy to another file descriptor, just call through to its - // close method. mWrapped.close(); } else { - Parcel.closeFileDescriptor(mFileDescriptor); + closeWithStatus(Status.OK, null); + } + } + + /** + * Close the ParcelFileDescriptor, informing any peer that an error occurred + * while processing. If the creator of this descriptor is not observing + * errors, it will close normally. + * + * @param msg describing the error; must not be null. + */ + public void closeWithError(String msg) throws IOException { + if (mWrapped != null) { + mWrapped.closeWithError(msg); + } else { + if (msg == null) { + throw new IllegalArgumentException("Message must not be null"); + } + closeWithStatus(Status.ERROR, msg); + } + } + + private void closeWithStatus(int status, String msg) throws IOException { + if (mWrapped != null) { + mWrapped.closeWithStatus(status, msg); + } else { + if (mClosed) return; + mClosed = true; + mGuard.close(); + // Status MUST be sent before closing actual descriptor + writeCommStatusAndClose(status, msg); + IoUtils.closeQuietly(mFd); + } + } + + private byte[] getOrCreateStatusBuffer() { + if (mStatusBuf == null) { + mStatusBuf = new byte[MAX_STATUS]; + } + return mStatusBuf; + } + + private void writeCommStatusAndClose(int status, String msg) { + if (mCommFd == null) { + // Not reliable, or someone already sent status + if (msg != null) { + Log.w(TAG, "Unable to inform peer: " + msg); + } + return; + } + + if (status == Status.DETACHED) { + Log.w(TAG, "Peer expected signal when closed; unable to deliver after detach"); + } + + try { + try { + if (status != Status.SILENCE) { + final byte[] buf = getOrCreateStatusBuffer(); + int writePtr = 0; + + Memory.pokeInt(buf, writePtr, status, ByteOrder.BIG_ENDIAN); + writePtr += 4; + + if (msg != null) { + final byte[] rawMsg = msg.getBytes(); + final int len = Math.min(rawMsg.length, buf.length - writePtr); + System.arraycopy(rawMsg, 0, buf, writePtr, len); + writePtr += len; + } + + Libcore.os.write(mCommFd, buf, 0, writePtr); + } + } catch (ErrnoException e) { + // Reporting status is best-effort + Log.w(TAG, "Failed to report status: " + e); + } + + if (status != Status.SILENCE) { + // Since we're about to close, read off any remote status. It's + // okay to remember missing here. + mStatus = readCommStatus(mCommFd, getOrCreateStatusBuffer()); + } + + } finally { + IoUtils.closeQuietly(mCommFd); + mCommFd = null; + } + } + + private static Status readCommStatus(FileDescriptor comm, byte[] buf) { + try { + final int n = Libcore.os.read(comm, buf, 0, buf.length); + if (n == 0) { + // EOF means they're dead + return new Status(Status.DEAD); + } else { + final int status = Memory.peekInt(buf, 0, ByteOrder.BIG_ENDIAN); + if (status == Status.ERROR) { + final String msg = new String(buf, 4, n - 4); + return new Status(status, msg); + } + return new Status(status); + } + } catch (ErrnoException e) { + if (e.errno == OsConstants.EAGAIN) { + // Remote is still alive, but no status written yet + return null; + } else { + Log.d(TAG, "Failed to read status; assuming dead: " + e); + return new Status(Status.DEAD); + } + } + } + + /** + * Indicates if this ParcelFileDescriptor can communicate and detect remote + * errors/crashes. + * + * @see #checkError(boolean) + */ + public boolean canDetectErrors() { + if (mWrapped != null) { + return mWrapped.canDetectErrors(); + } else { + return mCommFd != null; + } + } + + /** + * Detect and throw if the other end of a pipe or socket pair encountered an + * error or crashed. This allows a reader to distinguish between a valid EOF + * and an error/crash. + * <p> + * If this ParcelFileDescriptor is unable to detect remote errors, it will + * return silently. + * + * @param throwIfDetached requests that an exception be thrown if the remote + * side called {@link #detachFd()}. Once detached, the remote + * side is unable to communicate any errors through + * {@link #closeWithError(String)}. An application may pass true + * if it needs a stronger guarantee that the stream was closed + * normally and was not merely detached. + * @see #canDetectErrors() + */ + public void checkError(boolean throwIfDetached) throws IOException { + if (mWrapped != null) { + mWrapped.checkError(throwIfDetached); + } else { + if (mStatus == null) { + if (mCommFd == null) { + Log.w(TAG, "Peer didn't provide a comm channel; unable to check for errors"); + return; + } + + // Try reading status; it might be null if nothing written yet. + // Either way, we keep comm open to write our status later. + mStatus = readCommStatus(mCommFd, getOrCreateStatusBuffer()); + } + + if (mStatus == null || mStatus.status == Status.OK + || (mStatus.status == Status.DETACHED && !throwIfDetached)) { + // No status yet, or everything is peachy! + return; + } else { + throw mStatus.asIOException(); + } } } @@ -330,17 +742,17 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * ParcelFileDescriptor.close()} for you when the stream is closed. */ public static class AutoCloseInputStream extends FileInputStream { - private final ParcelFileDescriptor mFd; + private final ParcelFileDescriptor mPfd; - public AutoCloseInputStream(ParcelFileDescriptor fd) { - super(fd.getFileDescriptor()); - mFd = fd; + public AutoCloseInputStream(ParcelFileDescriptor pfd) { + super(pfd.getFileDescriptor()); + mPfd = pfd; } @Override public void close() throws IOException { try { - mFd.close(); + mPfd.close(); } finally { super.close(); } @@ -353,17 +765,17 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { * ParcelFileDescriptor.close()} for you when the stream is closed. */ public static class AutoCloseOutputStream extends FileOutputStream { - private final ParcelFileDescriptor mFd; + private final ParcelFileDescriptor mPfd; - public AutoCloseOutputStream(ParcelFileDescriptor fd) { - super(fd.getFileDescriptor()); - mFd = fd; + public AutoCloseOutputStream(ParcelFileDescriptor pfd) { + super(pfd.getFileDescriptor()); + mPfd = pfd; } @Override public void close() throws IOException { try { - mFd.close(); + mPfd.close(); } finally { super.close(); } @@ -372,7 +784,11 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { @Override public String toString() { - return "{ParcelFileDescriptor: " + mFileDescriptor + "}"; + if (mWrapped != null) { + return mWrapped.toString(); + } else { + return "{ParcelFileDescriptor: " + mFd + "}"; + } } @Override @@ -382,32 +798,20 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { } try { if (!mClosed) { - close(); + closeWithStatus(Status.LEAKED, null); } } finally { super.finalize(); } } - public ParcelFileDescriptor(ParcelFileDescriptor descriptor) { - mWrapped = descriptor; - mFileDescriptor = mWrapped.mFileDescriptor; - mGuard.open("close"); - } - - /** {@hide} */ - public ParcelFileDescriptor(FileDescriptor descriptor) { - if (descriptor == null) { - throw new NullPointerException("descriptor must not be null"); - } - mWrapped = null; - mFileDescriptor = descriptor; - mGuard.open("close"); - } - @Override public int describeContents() { - return Parcelable.CONTENTS_FILE_DESCRIPTOR; + if (mWrapped != null) { + return mWrapped.describeContents(); + } else { + return Parcelable.CONTENTS_FILE_DESCRIPTOR; + } } /** @@ -417,12 +821,22 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { */ @Override public void writeToParcel(Parcel out, int flags) { - out.writeFileDescriptor(mFileDescriptor); - if ((flags&PARCELABLE_WRITE_RETURN_VALUE) != 0 && !mClosed) { - try { - close(); - } catch (IOException e) { - // Empty + if (mWrapped != null) { + mWrapped.writeToParcel(out, flags); + } else { + out.writeFileDescriptor(mFd); + if (mCommFd != null) { + out.writeInt(1); + out.writeFileDescriptor(mCommFd); + } else { + out.writeInt(0); + } + if ((flags & PARCELABLE_WRITE_RETURN_VALUE) != 0 && !mClosed) { + try { + // Not a real close, so emit no status + closeWithStatus(Status.SILENCE, null); + } catch (IOException e) { + } } } } @@ -431,7 +845,12 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { = new Parcelable.Creator<ParcelFileDescriptor>() { @Override public ParcelFileDescriptor createFromParcel(Parcel in) { - return in.readFileDescriptor(); + final FileDescriptor fd = in.readRawFileDescriptor(); + FileDescriptor commChannel = null; + if (in.readInt() != 0) { + commChannel = in.readRawFileDescriptor(); + } + return new ParcelFileDescriptor(fd, commChannel); } @Override @@ -439,4 +858,111 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { return new ParcelFileDescriptor[size]; } }; + + /** + * Callback indicating that a ParcelFileDescriptor has been closed. + */ + public interface OnCloseListener { + /** + * Event indicating the ParcelFileDescriptor to which this listener was + * attached has been closed. + * + * @param e error state, or {@code null} if closed cleanly. + * @param fromDetach indicates if close event was result of + * {@link ParcelFileDescriptor#detachFd()}. After detach the + * remote side may continue reading/writing to the underlying + * {@link FileDescriptor}, but they can no longer deliver + * reliable close/error events. + */ + public void onClose(IOException e, boolean fromDetach); + } + + /** + * Internal class representing a remote status read by + * {@link ParcelFileDescriptor#readCommStatus(FileDescriptor, byte[])}. + */ + private static class Status { + /** Special value indicating remote side died. */ + public static final int DEAD = -2; + /** Special value indicating no status should be written. */ + public static final int SILENCE = -1; + + /** Remote reported that everything went better than expected. */ + public static final int OK = 0; + /** Remote reported error; length and message follow. */ + public static final int ERROR = 1; + /** Remote reported {@link #detachFd()} and went rogue. */ + public static final int DETACHED = 2; + /** Remote reported their object was finalized. */ + public static final int LEAKED = 3; + + public final int status; + public final String msg; + + public Status(int status) { + this(status, null); + } + + public Status(int status, String msg) { + this.status = status; + this.msg = msg; + } + + public IOException asIOException() { + switch (status) { + case DEAD: + return new IOException("Remote side is dead"); + case OK: + return null; + case ERROR: + return new IOException("Remote error: " + msg); + case DETACHED: + return new IOException("Remote side is detached"); + case LEAKED: + return new IOException("Remote side was leaked"); + default: + return new IOException("Unknown status: " + status); + } + } + } + + /** + * Bridge to watch for remote status, and deliver to listener. Currently + * requires that communication socket is <em>blocking</em>. + */ + private static final class ListenerBridge extends Thread { + // TODO: switch to using Looper to avoid burning a thread + + private FileDescriptor mCommFd; + private final Handler mHandler; + + public ListenerBridge(FileDescriptor comm, Looper looper, final OnCloseListener listener) { + mCommFd = comm; + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + final Status s = (Status) msg.obj; + if (s.status == Status.DETACHED) { + listener.onClose(null, true); + } else if (s.status == Status.OK) { + listener.onClose(null, false); + } else { + listener.onClose(s.asIOException(), false); + } + } + }; + } + + @Override + public void run() { + try { + final byte[] buf = new byte[MAX_STATUS]; + final Status status = readCommStatus(mCommFd, buf); + mHandler.obtainMessage(0, status).sendToTarget(); + } finally { + IoUtils.closeQuietly(mCommFd); + mCommFd = null; + } + } + } } diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 0a16d73..91d349a 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -36,6 +36,7 @@ import com.google.android.collect.Lists; import libcore.io.IoUtils; +import java.io.FileDescriptor; import java.io.IOException; import java.io.InputStream; import java.util.List; @@ -53,80 +54,85 @@ public final class DocumentsContract { // content://com.example/roots/sdcard/docs/0/contents/ // content://com.example/roots/sdcard/docs/0/search/?query=pony - /** - * MIME type of a document which is a directory that may contain additional - * documents. - * - * @see #buildContentsUri(String, String, String) - */ - public static final String MIME_TYPE_DIRECTORY = "vnd.android.cursor.dir/doc"; - /** {@hide} */ public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER"; /** {@hide} */ - public static final String ACTION_ROOTS_CHANGED = "android.provider.action.ROOTS_CHANGED"; + public static final String ACTION_DOCUMENT_CHANGED = "android.provider.action.DOCUMENT_CHANGED"; - /** - * {@link DocumentColumns#DOC_ID} value representing the root directory of a - * storage root. - */ - public static final String ROOT_DOC_ID = "0"; + public static class Documents { + private Documents() { + } - /** - * Flag indicating that a document is a directory that supports creation of - * new files within it. - * - * @see DocumentColumns#FLAGS - * @see #createDocument(ContentResolver, Uri, String, String) - */ - public static final int FLAG_SUPPORTS_CREATE = 1; + /** + * MIME type of a document which is a directory that may contain additional + * documents. + * + * @see #buildContentsUri(String, String, String) + */ + public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/doc"; - /** - * Flag indicating that a document is renamable. - * - * @see DocumentColumns#FLAGS - * @see #renameDocument(ContentResolver, Uri, String) - */ - public static final int FLAG_SUPPORTS_RENAME = 1 << 1; + /** + * {@link DocumentColumns#DOC_ID} value representing the root directory of a + * storage root. + */ + public static final String DOC_ID_ROOT = "0"; - /** - * Flag indicating that a document is deletable. - * - * @see DocumentColumns#FLAGS - */ - public static final int FLAG_SUPPORTS_DELETE = 1 << 2; + /** + * Flag indicating that a document is a directory that supports creation of + * new files within it. + * + * @see DocumentColumns#FLAGS + * @see #createDocument(ContentResolver, Uri, String, String) + */ + public static final int FLAG_SUPPORTS_CREATE = 1; - /** - * Flag indicating that a document can be represented as a thumbnail. - * - * @see DocumentColumns#FLAGS - * @see #getThumbnail(ContentResolver, Uri, Point) - */ - public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3; + /** + * Flag indicating that a document is renamable. + * + * @see DocumentColumns#FLAGS + * @see #renameDocument(ContentResolver, Uri, String) + */ + public static final int FLAG_SUPPORTS_RENAME = 1 << 1; - /** - * Flag indicating that a document is a directory that supports search. - * - * @see DocumentColumns#FLAGS - */ - public static final int FLAG_SUPPORTS_SEARCH = 1 << 4; + /** + * Flag indicating that a document is deletable. + * + * @see DocumentColumns#FLAGS + */ + public static final int FLAG_SUPPORTS_DELETE = 1 << 2; - /** - * Flag indicating that a document is writable. - * - * @see DocumentColumns#FLAGS - */ - public static final int FLAG_SUPPORTS_WRITE = 1 << 5; + /** + * Flag indicating that a document can be represented as a thumbnail. + * + * @see DocumentColumns#FLAGS + * @see #getThumbnail(ContentResolver, Uri, Point) + */ + public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3; - /** - * Flag indicating that a document is a directory that prefers its contents - * be shown in a larger format grid. Usually suitable when a directory - * contains mostly pictures. - * - * @see DocumentColumns#FLAGS - */ - public static final int FLAG_PREFERS_GRID = 1 << 6; + /** + * Flag indicating that a document is a directory that supports search. + * + * @see DocumentColumns#FLAGS + */ + public static final int FLAG_SUPPORTS_SEARCH = 1 << 4; + + /** + * Flag indicating that a document is writable. + * + * @see DocumentColumns#FLAGS + */ + public static final int FLAG_SUPPORTS_WRITE = 1 << 5; + + /** + * Flag indicating that a document is a directory that prefers its contents + * be shown in a larger format grid. Usually suitable when a directory + * contains mostly pictures. + * + * @see DocumentColumns#FLAGS + */ + public static final int FLAG_PREFERS_GRID = 1 << 6; + } /** * Optimal dimensions for a document thumbnail request, stored as a @@ -189,7 +195,7 @@ public final class DocumentsContract { /** * Build URI representing the contents of the given directory in a storage - * backend. The given document must be {@link #MIME_TYPE_DIRECTORY}. + * backend. The given document must be {@link Documents#MIME_TYPE_DIR}. */ public static Uri buildContentsUri(String authority, String rootId, String docId) { return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) @@ -296,7 +302,7 @@ public final class DocumentsContract { * <p> * Type: STRING * - * @see DocumentsContract#MIME_TYPE_DIRECTORY + * @see Documents#MIME_TYPE_DIR */ public static final String MIME_TYPE = "mime_type"; @@ -327,35 +333,43 @@ public final class DocumentsContract { public static final String SUMMARY = "summary"; } - /** - * Root that represents a cloud-based storage service. - * - * @see RootColumns#ROOT_TYPE - */ - public static final int ROOT_TYPE_SERVICE = 1; + public static class Roots { + private Roots() { + } - /** - * Root that represents a shortcut to content that may be available - * elsewhere through another storage root. - * - * @see RootColumns#ROOT_TYPE - */ - public static final int ROOT_TYPE_SHORTCUT = 2; + public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/root"; + public static final String MIME_TYPE_ITEM = "vnd.android.cursor.item/root"; - /** - * Root that represents a physical storage device. - * - * @see RootColumns#ROOT_TYPE - */ - public static final int ROOT_TYPE_DEVICE = 3; + /** + * Root that represents a cloud-based storage service. + * + * @see RootColumns#ROOT_TYPE + */ + public static final int ROOT_TYPE_SERVICE = 1; - /** - * Root that represents a physical storage device that should only be - * displayed to advanced users. - * - * @see RootColumns#ROOT_TYPE - */ - public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; + /** + * Root that represents a shortcut to content that may be available + * elsewhere through another storage root. + * + * @see RootColumns#ROOT_TYPE + */ + public static final int ROOT_TYPE_SHORTCUT = 2; + + /** + * Root that represents a physical storage device. + * + * @see RootColumns#ROOT_TYPE + */ + public static final int ROOT_TYPE_DEVICE = 3; + + /** + * Root that represents a physical storage device that should only be + * displayed to advanced users. + * + * @see RootColumns#ROOT_TYPE + */ + public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; + } /** * These are standard columns for the roots URI. @@ -370,8 +384,8 @@ public final class DocumentsContract { * <p> * Type: INTEGER (int) * - * @see DocumentsContract#ROOT_TYPE_SERVICE - * @see DocumentsContract#ROOT_TYPE_DEVICE + * @see Roots#ROOT_TYPE_SERVICE + * @see Roots#ROOT_TYPE_DEVICE */ public static final String ROOT_TYPE = "root_type"; @@ -440,7 +454,7 @@ public final class DocumentsContract { /** * Return thumbnail representing the document at the given URI. Callers are * responsible for their own caching. Given document must have - * {@link #FLAG_SUPPORTS_THUMBNAIL} set. + * {@link Documents#FLAG_SUPPORTS_THUMBNAIL} set. * * @return decoded thumbnail, or {@code null} if problem was encountered. */ @@ -448,16 +462,27 @@ public final class DocumentsContract { final Bundle opts = new Bundle(); opts.putParcelable(EXTRA_THUMBNAIL_SIZE, size); - InputStream is = null; + AssetFileDescriptor afd = null; try { - is = new AssetFileDescriptor.AutoCloseInputStream( - resolver.openTypedAssetFileDescriptor(documentUri, "image/*", opts)); - return BitmapFactory.decodeStream(is); + afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", opts); + + final FileDescriptor fd = afd.getFileDescriptor(); + final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options(); + + bitmapOpts.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor(fd, null, bitmapOpts); + + final int widthSample = bitmapOpts.outWidth / size.x; + final int heightSample = bitmapOpts.outHeight / size.y; + + bitmapOpts.inJustDecodeBounds = false; + bitmapOpts.inSampleSize = Math.min(widthSample, heightSample); + return BitmapFactory.decodeFileDescriptor(fd, null, bitmapOpts); } catch (IOException e) { Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e); return null; } finally { - IoUtils.closeQuietly(is); + IoUtils.closeQuietly(afd); } } @@ -465,7 +490,8 @@ public final class DocumentsContract { * Create a new document under a specific parent document with the given * display name and MIME type. * - * @param parentDocumentUri document with {@link #FLAG_SUPPORTS_CREATE} + * @param parentDocumentUri document with + * {@link Documents#FLAG_SUPPORTS_CREATE} * @param displayName name for new document * @param mimeType MIME type for new document, which cannot be changed * @return newly created document Uri, or {@code null} if failed @@ -480,7 +506,7 @@ public final class DocumentsContract { /** * Rename the document at the given URI. Given document must have - * {@link #FLAG_SUPPORTS_RENAME} set. + * {@link Documents#FLAG_SUPPORTS_RENAME} set. * * @return if rename was successful. */ @@ -496,7 +522,7 @@ public final class DocumentsContract { * This signal is used to invalidate internal caches. */ public static void notifyRootsChanged(Context context, String authority) { - final Intent intent = new Intent(ACTION_ROOTS_CHANGED); + final Intent intent = new Intent(ACTION_DOCUMENT_CHANGED); intent.setData(buildRootsUri(authority)); context.sendBroadcast(intent); } diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index cb6300f..ad6839b 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -19,8 +19,8 @@ package android.provider; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ContentResolver; -import android.content.ContentValues; import android.content.ContentUris; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; @@ -532,7 +532,8 @@ public final class MediaStore { private static final Object sThumbBufLock = new Object(); private static byte[] sThumbBuf; - private static Bitmap getMiniThumbFromFile(Cursor c, Uri baseUri, ContentResolver cr, BitmapFactory.Options options) { + private static Bitmap getMiniThumbFromFile( + Cursor c, Uri baseUri, ContentResolver cr, BitmapFactory.Options options) { Bitmap bitmap = null; Uri thumbUri = null; try { @@ -577,6 +578,7 @@ public final class MediaStore { if (c != null) c.close(); } } + /** * This method ensure thumbnails associated with origId are generated and decode the byte * stream from database (MICRO_KIND) or file (MINI_KIND). diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java index 2b805a9..22675b4 100644 --- a/core/java/android/text/format/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -816,9 +816,10 @@ public class DateUtils */ public static Formatter formatDateRange(Context context, Formatter formatter, long startMillis, long endMillis, int flags, String timeZone) { - // icu4c will fall back to the locale's preferred 12/24 format, + // If we're being asked to format a time without being explicitly told whether to use + // the 12- or 24-hour clock, icu4c will fall back to the locale's preferred 12/24 format, // but we want to fall back to the user's preference. - if ((flags & (FORMAT_12HOUR | FORMAT_24HOUR)) == 0) { + if ((flags & (FORMAT_SHOW_TIME | FORMAT_12HOUR | FORMAT_24HOUR)) == FORMAT_SHOW_TIME) { flags |= DateFormat.is24HourFormat(context) ? FORMAT_24HOUR : FORMAT_12HOUR; } diff --git a/core/java/android/view/DisplayList.java b/core/java/android/view/DisplayList.java index 2d24c1e..43fd628 100644 --- a/core/java/android/view/DisplayList.java +++ b/core/java/android/view/DisplayList.java @@ -208,9 +208,22 @@ public abstract class DisplayList { * {@link #isValid()} will return false. * * @see #isValid() + * @see #reset() */ public abstract void clear(); + + /** + * Reset native resources. This is called when cleaning up the state of display lists + * during destruction of hardware resources, to ensure that we do not hold onto + * obsolete resources after related resources are gone. + * + * @see #clear() + * + * @hide + */ + public abstract void reset(); + /** * Sets the dirty flag. When a display list is dirty, {@link #clear()} should * be invoked whenever possible. @@ -670,13 +683,4 @@ public abstract class DisplayList { * @see View#offsetTopAndBottom(int) */ public abstract void offsetTopAndBottom(float offset); - - /** - * Reset native resources. This is called when cleaning up the state of display lists - * during destruction of hardware resources, to ensure that we do not hold onto - * obsolete resources after related resources are gone. - * - * @hide - */ - public abstract void reset(); } diff --git a/core/java/android/view/GLES20DisplayList.java b/core/java/android/view/GLES20DisplayList.java index 8b2a2ef..c652bac 100644 --- a/core/java/android/view/GLES20DisplayList.java +++ b/core/java/android/view/GLES20DisplayList.java @@ -94,6 +94,7 @@ class GLES20DisplayList extends DisplayList { if (hasNativeDisplayList()) { nReset(mFinalizer.mNativeDisplayList); } + clear(); } @Override diff --git a/core/java/android/view/GLES20Layer.java b/core/java/android/view/GLES20Layer.java index 7ee628b..0e3311c 100644 --- a/core/java/android/view/GLES20Layer.java +++ b/core/java/android/view/GLES20Layer.java @@ -59,6 +59,9 @@ abstract class GLES20Layer extends HardwareLayer { @Override public void destroy() { + if (mDisplayList != null) { + mDisplayList.reset(); + } if (mFinalizer != null) { mFinalizer.destroy(); mFinalizer = null; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 747e8ea..f05e372 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -12076,12 +12076,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ public void resolvePadding() { + final int resolvedLayoutDirection = getLayoutDirection(); + if (!isRtlCompatibilityMode()) { // Post Jelly Bean MR1 case: we need to take the resolved layout direction into account. // If start / end padding are defined, they will be resolved (hence overriding) to // left / right or right / left depending on the resolved layout direction. // If start / end padding are not defined, use the left / right ones. - int resolvedLayoutDirection = getLayoutDirection(); switch (resolvedLayoutDirection) { case LAYOUT_DIRECTION_RTL: if (mUserPaddingStart != UNDEFINED_PADDING) { @@ -12110,11 +12111,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } mUserPaddingBottom = (mUserPaddingBottom >= 0) ? mUserPaddingBottom : mPaddingBottom; - - onRtlPropertiesChanged(resolvedLayoutDirection); } internalSetPadding(mUserPaddingLeft, mPaddingTop, mUserPaddingRight, mUserPaddingBottom); + onRtlPropertiesChanged(resolvedLayoutDirection); mPrivateFlags2 |= PFLAG2_PADDING_RESOLVED; } @@ -12144,7 +12144,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, removeSendViewScrolledAccessibilityEventCallback(); destroyDrawingCache(); - destroyLayer(false); cleanupDraw(); @@ -12162,7 +12161,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mAttachInfo.mViewRootImpl.cancelInvalidate(this); } else { // Should never happen - clearDisplayList(); + resetDisplayList(); } } @@ -12773,9 +12772,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mHardwareLayer.destroy(); mHardwareLayer = null; - if (mDisplayList != null) { - mDisplayList.reset(); - } invalidate(true); invalidateParentCaches(); } @@ -12796,7 +12792,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ protected void destroyHardwareResources() { - clearDisplayList(); + resetDisplayList(); destroyLayer(true); } @@ -13044,6 +13040,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } + private void resetDisplayList() { + if (mDisplayList != null) { + mDisplayList.reset(); + } + } + /** * <p>Calling this method is equivalent to calling <code>getDrawingCache(false)</code>.</p> * diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index f574efa..c874c82 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -5357,6 +5357,18 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Returns whether layout calls on this container are currently being + * suppressed, due to an earlier call to {@link #suppressLayout(boolean)}. + * + * @return true if layout calls are currently suppressed, false otherwise. + * + * @hide + */ + public boolean isLayoutSuppressed() { + return mSuppressLayout; + } + + /** * {@inheritDoc} */ @Override diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index e90705c..f711e7a 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -614,15 +614,7 @@ public final class ViewRootImpl implements ViewParent, } void destroyHardwareResources() { - if (mAttachInfo.mHardwareRenderer != null) { - if (mAttachInfo.mHardwareRenderer.isEnabled()) { - mAttachInfo.mHardwareRenderer.destroyLayers(mView); - } - mAttachInfo.mHardwareRenderer.destroy(false); - } - } - - void terminateHardwareResources() { + invalidateDisplayLists(); if (mAttachInfo.mHardwareRenderer != null) { mAttachInfo.mHardwareRenderer.destroyHardwareResources(mView); mAttachInfo.mHardwareRenderer.destroy(false); @@ -636,6 +628,7 @@ public final class ViewRootImpl implements ViewParent, HardwareRenderer.trimMemory(ComponentCallbacks2.TRIM_MEMORY_MODERATE); } } else { + invalidateDisplayLists(); if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { mAttachInfo.mHardwareRenderer.destroyLayers(mView); @@ -2554,7 +2547,7 @@ public final class ViewRootImpl implements ViewParent, for (int i = 0; i < count; i++) { final DisplayList displayList = displayLists.get(i); if (displayList.isDirty()) { - displayList.clear(); + displayList.reset(); } } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index b183bb6..3dd96f5 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -21,7 +21,6 @@ import android.app.ActivityManager; import android.content.ComponentCallbacks2; import android.content.res.Configuration; import android.opengl.ManagedEGLContext; -import android.os.Debug; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; @@ -29,7 +28,6 @@ import android.os.SystemProperties; import android.util.AndroidRuntimeException; import android.util.ArraySet; import android.util.Log; -import android.util.Slog; import android.view.inputmethod.InputMethodManager; import com.android.internal.util.FastPrintWriter; @@ -385,7 +383,7 @@ public final class WindowManagerGlobal { // known windows synchronized (mLock) { for (int i = mRoots.size() - 1; i >= 0; --i) { - mRoots.get(i).terminateHardwareResources(); + mRoots.get(i).destroyHardwareResources(); } } // Force a full memory flush diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index feaab3e..c440c7b 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -82,11 +82,16 @@ public final class InputMethodInfo implements Parcelable { private final boolean mIsAuxIme; /** - * Cavert: mForceDefault must be false for production. This flag is only for test. + * Caveat: mForceDefault must be false for production. This flag is only for test. */ private final boolean mForceDefault; /** + * The flag whether this IME supports ways to switch to a next input method (e.g. globe key.) + */ + private final boolean mSupportsSwitchingToNextInputMethod; + + /** * Constructor. * * @param context The Context in which we are parsing the input method. @@ -114,6 +119,7 @@ public final class InputMethodInfo implements Parcelable { ServiceInfo si = service.serviceInfo; mId = new ComponentName(si.packageName, si.name).flattenToShortString(); boolean isAuxIme = true; + boolean supportsSwitchingToNextInputMethod = false; // false as default mForceDefault = false; PackageManager pm = context.getPackageManager(); @@ -149,6 +155,9 @@ public final class InputMethodInfo implements Parcelable { com.android.internal.R.styleable.InputMethod_settingsActivity); isDefaultResId = sa.getResourceId( com.android.internal.R.styleable.InputMethod_isDefault, 0); + supportsSwitchingToNextInputMethod = sa.getBoolean( + com.android.internal.R.styleable.InputMethod_supportsSwitchingToNextInputMethod, + false); sa.recycle(); final int depth = parser.getDepth(); @@ -216,6 +225,7 @@ public final class InputMethodInfo implements Parcelable { mSettingsActivityName = settingsActivityComponent; mIsDefaultResId = isDefaultResId; mIsAuxIme = isAuxIme; + mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod; } InputMethodInfo(Parcel source) { @@ -223,6 +233,7 @@ public final class InputMethodInfo implements Parcelable { mSettingsActivityName = source.readString(); mIsDefaultResId = source.readInt(); mIsAuxIme = source.readInt() == 1; + mSupportsSwitchingToNextInputMethod = source.readInt() == 1; mService = ResolveInfo.CREATOR.createFromParcel(source); source.readTypedList(mSubtypes, InputMethodSubtype.CREATOR); mForceDefault = false; @@ -254,6 +265,7 @@ public final class InputMethodInfo implements Parcelable { mSubtypes.addAll(subtypes); } mForceDefault = forceDefault; + mSupportsSwitchingToNextInputMethod = true; } private static ResolveInfo buildDummyResolveInfo(String packageName, String className, @@ -435,6 +447,14 @@ public final class InputMethodInfo implements Parcelable { } /** + * @return true if this input method supports ways to switch to a next input method. + * @hide + */ + public boolean supportsSwitchingToNextInputMethod() { + return mSupportsSwitchingToNextInputMethod; + } + + /** * Used to package this object into a {@link Parcel}. * * @param dest The {@link Parcel} to be written. @@ -446,6 +466,7 @@ public final class InputMethodInfo implements Parcelable { dest.writeString(mSettingsActivityName); dest.writeInt(mIsDefaultResId); dest.writeInt(mIsAuxIme ? 1 : 0); + dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0); mService.writeToParcel(dest, flags); dest.writeTypedList(mSubtypes); } diff --git a/core/java/android/view/transition/Fade.java b/core/java/android/view/transition/Fade.java index 4fd60c1..45c21d8 100644 --- a/core/java/android/view/transition/Fade.java +++ b/core/java/android/view/transition/Fade.java @@ -19,6 +19,7 @@ package android.view.transition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -35,6 +36,7 @@ public class Fade extends Visibility { private static boolean DBG = Transition.DBG && false; private static final String LOG_TAG = "Fade"; + private static final String PROPNAME_ALPHA = "android:fade:alpha"; private static final String PROPNAME_SCREEN_X = "android:fade:screenX"; private static final String PROPNAME_SCREEN_Y = "android:fade:screenY"; @@ -74,20 +76,28 @@ public class Fade extends Visibility { /** * Utility method to handle creating and running the Animator. */ - private Animator runAnimation(View view, float startAlpha, float endAlpha, - Animator.AnimatorListener listener) { + private Animator createAnimation(View view, float startAlpha, float endAlpha, + AnimatorListenerAdapter listener) { + if (startAlpha == endAlpha) { + // run listener if we're noop'ing the animation, to get the end-state results now + if (listener != null) { + listener.onAnimationEnd(null); + } + return null; + } final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", startAlpha, endAlpha); if (listener != null) { anim.addListener(listener); + anim.addPauseListener(listener); } - // TODO: Maybe extract a method into Transition to run an animation that handles the - // duration/startDelay stuff for all subclasses. return anim; } @Override protected void captureValues(TransitionValues values, boolean start) { super.captureValues(values, start); + float alpha = values.view.getAlpha(); + values.values.put(PROPNAME_ALPHA, alpha); int[] loc = new int[2]; values.view.getLocationOnScreen(loc); values.values.put(PROPNAME_SCREEN_X, loc[0]); @@ -95,6 +105,23 @@ public class Fade extends Visibility { } @Override + protected Animator play(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + Animator animator = super.play(sceneRoot, startValues, endValues); + if (animator == null && startValues != null && endValues != null) { + boolean endVisible = isVisible(endValues); + final View endView = endValues.view; + float endAlpha = endView.getAlpha(); + float startAlpha = (Float) startValues.values.get(PROPNAME_ALPHA); + if ((endVisible && startAlpha < endAlpha && (mFadingMode & Fade.IN) != 0) || + (!endVisible && startAlpha > endAlpha && (mFadingMode & Fade.OUT) != 0)) { + animator = createAnimation(endView, startAlpha, endAlpha, null); + } + } + return animator; + } + + @Override protected Animator appear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility) { @@ -102,15 +129,11 @@ public class Fade extends Visibility { return null; } final View endView = endValues.view; - endView.setAlpha(0); - final Animator.AnimatorListener endListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // Always end animation with full alpha, in case it's canceled mid-stream - endView.setAlpha(1); - } - }; - return runAnimation(endView, 0, 1, endListener); + // if alpha < 1, just fade it in from the current value + if (endView.getAlpha() == 1.0f) { + endView.setAlpha(0); + } + return createAnimation(endView, endView.getAlpha(), 1, null); } @Override @@ -129,7 +152,7 @@ public class Fade extends Visibility { } View overlayView = null; View viewToKeep = null; - if (endView == null) { + if (endView == null || endView.getParent() == null) { // view was removed: add the start view to the Overlay view = startView; overlayView = view; @@ -167,7 +190,7 @@ public class Fade extends Visibility { final View finalOverlayView = overlayView; final View finalViewToKeep = viewToKeep; final ViewGroup finalSceneRoot = sceneRoot; - final Animator.AnimatorListener endListener = new AnimatorListenerAdapter() { + final AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { finalView.setAlpha(startAlpha); @@ -179,8 +202,22 @@ public class Fade extends Visibility { finalSceneRoot.getOverlay().remove(finalOverlayView); } } + + @Override + public void onAnimationPause(Animator animation) { + if (finalOverlayView != null) { + finalSceneRoot.getOverlay().remove(finalOverlayView); + } + } + + @Override + public void onAnimationResume(Animator animation) { + if (finalOverlayView != null) { + finalSceneRoot.getOverlay().add(finalOverlayView); + } + } }; - return runAnimation(view, startAlpha, endAlpha, endListener); + return createAnimation(view, startAlpha, endAlpha, endListener); } if (viewToKeep != null) { // TODO: find a different way to do this, like just changing the view to be @@ -193,12 +230,42 @@ public class Fade extends Visibility { final View finalOverlayView = overlayView; final View finalViewToKeep = viewToKeep; final ViewGroup finalSceneRoot = sceneRoot; - final Animator.AnimatorListener endListener = new AnimatorListenerAdapter() { + final AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { + boolean mCanceled = false; + float mPausedAlpha = -1; + @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationPause(Animator animation) { + if (finalViewToKeep != null && !mCanceled) { + finalViewToKeep.setVisibility(finalVisibility); + } + mPausedAlpha = finalView.getAlpha(); finalView.setAlpha(startAlpha); + } + + @Override + public void onAnimationResume(Animator animation) { + if (finalViewToKeep != null && !mCanceled) { + finalViewToKeep.setVisibility(View.VISIBLE); + } + finalView.setAlpha(mPausedAlpha); + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + if (mPausedAlpha >= 0) { + finalView.setAlpha(mPausedAlpha); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCanceled) { + finalView.setAlpha(startAlpha); + } // TODO: restore view offset from overlay repositioning - if (finalViewToKeep != null) { + if (finalViewToKeep != null && !mCanceled) { finalViewToKeep.setVisibility(finalVisibility); } if (finalOverlayView != null) { @@ -206,7 +273,7 @@ public class Fade extends Visibility { } } }; - return runAnimation(view, startAlpha, endAlpha, endListener); + return createAnimation(view, startAlpha, endAlpha, endListener); } return null; } diff --git a/core/java/android/view/transition/Move.java b/core/java/android/view/transition/Move.java index ceda5a5..ae7d759 100644 --- a/core/java/android/view/transition/Move.java +++ b/core/java/android/view/transition/Move.java @@ -25,8 +25,6 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; -import android.util.ArrayMap; -import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -42,6 +40,13 @@ public class Move extends Transition { private static final String PROPNAME_PARENT = "android:move:parent"; private static final String PROPNAME_WINDOW_X = "android:move:windowX"; private static final String PROPNAME_WINDOW_Y = "android:move:windowY"; + private static String[] sTransitionProperties = { + PROPNAME_BOUNDS, + PROPNAME_PARENT, + PROPNAME_WINDOW_X, + PROPNAME_WINDOW_Y + }; + int[] tempLocation = new int[2]; boolean mResizeClip = false; boolean mReparent = false; @@ -49,6 +54,11 @@ public class Move extends Transition { private static RectEvaluator sRectEvaluator = new RectEvaluator(); + @Override + public String[] getTransitionProperties() { + return sTransitionProperties; + } + public void setResizeClip(boolean resizeClip) { mResizeClip = resizeClip; } @@ -146,12 +156,33 @@ public class Move extends Transition { if (view.getParent() instanceof ViewGroup) { final ViewGroup parent = (ViewGroup) view.getParent(); parent.suppressLayout(true); - anim.addListener(new AnimatorListenerAdapter() { + TransitionListener transitionListener = new TransitionListenerAdapter() { + boolean mCanceled = false; + @Override - public void onAnimationEnd(Animator animation) { + public void onTransitionCancel(Transition transition) { parent.suppressLayout(false); + mCanceled = true; + } + + @Override + public void onTransitionEnd(Transition transition) { + if (!mCanceled) { + parent.suppressLayout(false); + } + } + + @Override + public void onTransitionPause(Transition transition) { + parent.suppressLayout(false); + } + + @Override + public void onTransitionResume(Transition transition) { + parent.suppressLayout(true); } - }); + }; + addListener(transitionListener); } return anim; } else { @@ -191,12 +222,33 @@ public class Move extends Transition { if (view.getParent() instanceof ViewGroup) { final ViewGroup parent = (ViewGroup) view.getParent(); parent.suppressLayout(true); - anim.addListener(new AnimatorListenerAdapter() { + TransitionListener transitionListener = new TransitionListenerAdapter() { + boolean mCanceled = false; + @Override - public void onAnimationEnd(Animator animation) { + public void onTransitionCancel(Transition transition) { parent.suppressLayout(false); + mCanceled = true; + } + + @Override + public void onTransitionEnd(Transition transition) { + if (!mCanceled) { + parent.suppressLayout(false); + } + } + + @Override + public void onTransitionPause(Transition transition) { + parent.suppressLayout(false); + } + + @Override + public void onTransitionResume(Transition transition) { + parent.suppressLayout(true); } - }); + }; + addListener(transitionListener); } anim.addListener(new AnimatorListenerAdapter() { @Override diff --git a/core/java/android/view/transition/Transition.java b/core/java/android/view/transition/Transition.java index f99ddc0..0444843 100644 --- a/core/java/android/view/transition/Transition.java +++ b/core/java/android/view/transition/Transition.java @@ -22,7 +22,6 @@ import android.animation.TimeInterpolator; import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; -import android.util.Pair; import android.util.SparseArray; import android.view.SurfaceView; import android.view.TextureView; @@ -60,6 +59,8 @@ public abstract class Transition implements Cloneable { private static final String LOG_TAG = "Transition"; static final boolean DBG = false; + private String mName = getClass().getName(); + long mStartDelay = -1; long mDuration = -1; TimeInterpolator mInterpolator = null; @@ -69,29 +70,29 @@ public abstract class Transition implements Cloneable { private TransitionValuesMaps mEndValues = new TransitionValuesMaps(); TransitionGroup mParent = null; + // Per-animator information used for later canceling when future transitions overlap + private static ThreadLocal<ArrayMap<Animator, AnimationInfo>> sRunningAnimators = + new ThreadLocal<ArrayMap<Animator, AnimationInfo>>(); + // Scene Root is set at play() time in the cloned Transition ViewGroup mSceneRoot = null; - // Used to carry data between setup() and play(), cleared before every scene transition - private ArrayList<TransitionValues> mPlayStartValuesList = new ArrayList<TransitionValues>(); - private ArrayList<TransitionValues> mPlayEndValuesList = new ArrayList<TransitionValues>(); - // Track all animators in use in case the transition gets canceled and needs to // cancel running animators private ArrayList<Animator> mCurrentAnimators = new ArrayList<Animator>(); // Number of per-target instances of this Transition currently running. This count is - // determined by calls to startTransition() and endTransition() + // determined by calls to start() and end() int mNumInstances = 0; - + // Whether this transition is currently paused, due to a call to pause() + boolean mPaused = false; // The set of listeners to be sent transition lifecycle events. ArrayList<TransitionListener> mListeners = null; // The set of animators collected from calls to play(), to be run in runAnimations() - ArrayMap<Pair<TransitionValues, TransitionValues>, Animator> mAnimatorMap = - new ArrayMap<Pair<TransitionValues, TransitionValues>, Animator>(); + ArrayList<Animator> mAnimators = new ArrayList<Animator>(); /** * Constructs a Transition object with no target objects. A transition with @@ -115,6 +116,14 @@ public abstract class Transition implements Cloneable { return this; } + /** + * Returns the duration set on this transition. If no duration has been set, + * the returned value will be negative, indicating that resulting animators will + * retain their own durations. + * + * @return The duration set on this transition, if one has been set, otherwise + * returns a negative number. + */ public long getDuration() { return mDuration; } @@ -131,6 +140,14 @@ public abstract class Transition implements Cloneable { mStartDelay = startDelay; } + /** + * Returns the startDelay set on this transition. If no startDelay has been set, + * the returned value will be negative, indicating that resulting animators will + * retain their own startDelays. + * + * @return The startDealy set on this transition, if one has been set, otherwise + * returns a negative number. + */ public long getStartDelay() { return mStartDelay; } @@ -147,11 +164,44 @@ public abstract class Transition implements Cloneable { mInterpolator = interpolator; } + /** + * Returns the interpolator set on this transition. If no interpolator has been set, + * the returned value will be null, indicating that resulting animators will + * retain their own interpolators. + * + * @return The interpolator set on this transition, if one has been set, otherwise + * returns null. + */ public TimeInterpolator getInterpolator() { return mInterpolator; } /** + * Returns the set of property names used stored in the {@link TransitionValues} + * object passed into {@link #captureValues(TransitionValues, boolean)} that + * this transition cares about for the purposes of canceling overlapping animations. + * When any transition is started on a given scene root, all transitions + * currently running on that same scene root are checked to see whether the + * properties on which they based their animations agree with the end values of + * the same properties in the new transition. If the end values are not equal, + * then the old animation is canceled since the new transition will start a new + * animation to these new values. If the values are equal, the old animation is + * allowed to continue and no new animation is started for that transition. + * + * <p>A transition does not need to override this method. However, not doing so + * will mean that the cancellation logic outlined in the previous paragraph + * will be skipped for that transition, possibly leading to artifacts as + * old transitions and new transitions on the same targets run in parallel, + * animating views toward potentially different end values.</p> + * + * @return An array of property names as described in the class documentation for + * {@link TransitionValues}. The default implementation returns <code>null</code>. + */ + public String[] getTransitionProperties() { + return null; + } + + /** * This method is called by the transition's parent (all the way up to the * topmost Transition in the hierarchy) with the sceneRoot and start/end * values that the transition may need to set up initial target values @@ -210,8 +260,6 @@ public abstract class Transition implements Cloneable { if (DBG) { Log.d(LOG_TAG, "play() for " + this); } - mPlayStartValuesList.clear(); - mPlayEndValuesList.clear(); ArrayMap<View, TransitionValues> endCopy = new ArrayMap<View, TransitionValues>(endValues.viewValues); SparseArray<TransitionValues> endIdCopy = @@ -316,6 +364,7 @@ public abstract class Transition implements Cloneable { startValuesList.add(start); endValuesList.add(end); } + ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); for (int i = 0; i < startValuesList.size(); ++i) { TransitionValues start = startValuesList.get(i); TransitionValues end = endValuesList.get(i); @@ -345,14 +394,46 @@ public abstract class Transition implements Cloneable { // TODO: what to do about targetIds and itemIds? Animator animator = play(sceneRoot, start, end); if (animator != null) { - mAnimatorMap.put(new Pair(start, end), animator); - // Note: we've already done the check against targetIDs in these lists - mPlayStartValuesList.add(start); - mPlayEndValuesList.add(end); + // Save animation info for future cancellation purposes + View view = null; + TransitionValues infoValues = null; + if (end != null) { + view = end.view; + String[] properties = getTransitionProperties(); + if (view != null && properties != null && properties.length > 0) { + infoValues = new TransitionValues(); + infoValues.view = view; + TransitionValues newValues = endValues.viewValues.get(view); + if (newValues != null) { + for (int j = 0; j < properties.length; ++j) { + infoValues.values.put(properties[j], + newValues.values.get(properties[j])); + } + } + int numExistingAnims = runningAnimators.size(); + for (int j = 0; j < numExistingAnims; ++j) { + Animator anim = runningAnimators.keyAt(j); + AnimationInfo info = runningAnimators.get(anim); + if (info.values != null && info.view == view && + ((info.name == null && getName() == null) || + info.name.equals(getName()))) { + if (info.values.equals(infoValues)) { + // Favor the old animator + animator = null; + break; + } + } + } + } + } else { + view = (start != null) ? start.view : null; + } + if (animator != null) { + AnimationInfo info = new AnimationInfo(view, getName(), infoValues); + runningAnimators.put(animator, info); + mAnimators.add(animator); + } } - } else if (DBG) { - View view = (end != null) ? end.view : start.view; - Log.d(LOG_TAG, " No change for view " + view); } } } @@ -389,6 +470,15 @@ public abstract class Transition implements Cloneable { return false; } + private static ArrayMap<Animator, AnimationInfo> getRunningAnimators() { + ArrayMap<Animator, AnimationInfo> runningAnimators = sRunningAnimators.get(); + if (runningAnimators == null) { + runningAnimators = new ArrayMap<Animator, AnimationInfo>(); + sRunningAnimators.set(runningAnimators); + } + return runningAnimators; + } + /** * This is called internally once all animations have been set up by the * transition hierarchy. \ @@ -396,28 +486,27 @@ public abstract class Transition implements Cloneable { * @hide */ protected void runAnimations() { - if (DBG && mPlayStartValuesList.size() > 0) { - Log.d(LOG_TAG, "runAnimations (" + mPlayStartValuesList.size() + ") on " + this); - } - startTransition(); - // Now walk the list of TransitionValues, calling play for each pair - for (int i = 0; i < mPlayStartValuesList.size(); ++i) { - TransitionValues start = mPlayStartValuesList.get(i); - TransitionValues end = mPlayEndValuesList.get(i); - Animator anim = mAnimatorMap.get(new Pair(start, end)); + if (DBG) { + Log.d(LOG_TAG, "runAnimations() on " + this); + } + start(); + ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); + // Now start every Animator that was previously created for this transition in play() + for (Animator anim : mAnimators) { if (DBG) { Log.d(LOG_TAG, " anim: " + anim); } - startTransition(); - runAnimator(anim); + if (runningAnimators.containsKey(anim)) { + start(); + runAnimator(anim, runningAnimators); + } } - mPlayStartValuesList.clear(); - mPlayEndValuesList.clear(); - mAnimatorMap.clear(); - endTransition(); + mAnimators.clear(); + end(); } - private void runAnimator(Animator animator) { + private void runAnimator(Animator animator, + final ArrayMap<Animator, AnimationInfo> runningAnimators) { if (animator != null) { // TODO: could be a single listener instance for all of them since it uses the param animator.addListener(new AnimatorListenerAdapter() { @@ -427,6 +516,7 @@ public abstract class Transition implements Cloneable { } @Override public void onAnimationEnd(Animator animation) { + runningAnimators.remove(animation); mCurrentAnimators.remove(animation); } }); @@ -691,11 +781,112 @@ public abstract class Transition implements Cloneable { } /** + * Pauses this transition, sending out calls to {@link + * TransitionListener#onTransitionPause(Transition)} to all listeners + * and pausing all running animators started by this transition. + * + * @hide + */ + public void pause() { + ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); + int numOldAnims = runningAnimators.size(); + for (int i = numOldAnims - 1; i >= 0; i--) { + Animator anim = runningAnimators.keyAt(i); + anim.pause(); + } + if (mListeners != null && mListeners.size() > 0) { + ArrayList<TransitionListener> tmpListeners = + (ArrayList<TransitionListener>) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onTransitionPause(this); + } + } + mPaused = true; + } + + /** + * Resumes this transition, sending out calls to {@link + * TransitionListener#onTransitionPause(Transition)} to all listeners + * and pausing all running animators started by this transition. + * + * @hide + */ + public void resume() { + if (mPaused) { + ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); + int numOldAnims = runningAnimators.size(); + for (int i = numOldAnims - 1; i >= 0; i--) { + Animator anim = runningAnimators.keyAt(i); + anim.resume(); + } + if (mListeners != null && mListeners.size() > 0) { + ArrayList<TransitionListener> tmpListeners = + (ArrayList<TransitionListener>) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onTransitionResume(this); + } + } + mPaused = false; + } + } + + /** * Called by TransitionManager to play the transition. This calls * play() to set things up and create all of the animations and then * runAnimations() to actually start the animations. */ void playTransition(ViewGroup sceneRoot) { + ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); + int numOldAnims = runningAnimators.size(); + for (int i = numOldAnims - 1; i >= 0; i--) { + Animator anim = runningAnimators.keyAt(i); + if (anim != null) { + anim.resume(); + AnimationInfo oldInfo = runningAnimators.get(anim); + if (oldInfo != null) { + boolean cancel = false; + TransitionValues oldValues = oldInfo.values; + View oldView = oldInfo.view; + TransitionValues newValues = mEndValues.viewValues != null ? + mEndValues.viewValues.get(oldView) : null; + if (oldValues == null || newValues == null) { + if (oldValues != null || newValues != null) { + cancel = true; + } + } else { + for (String key : oldValues.values.keySet()) { + Object oldValue = oldValues.values.get(key); + Object newValue = newValues.values.get(key); + if ((oldValue == null && newValue != null) || + (oldValue != null && !oldValue.equals(newValue))) { + cancel = true; + if (DBG) { + Log.d(LOG_TAG, "Transition.play: oldValue != newValue for " + + key + ": old, new = " + oldValue + ", " + newValue); + } + break; + } + } + } + if (cancel) { + if (anim.isRunning() || anim.isStarted()) { + if (DBG) { + Log.d(LOG_TAG, "Canceling anim " + anim); + } + anim.cancel(); + } else { + if (DBG) { + Log.d(LOG_TAG, "removing anim from info list: " + anim); + } + runningAnimators.remove(anim); + } + } + } + } + } + // setup() must be called on entire transition hierarchy and set of views // before calling play() on anything; every transition needs a chance to set up // target views appropriately before transitions begin running @@ -707,7 +898,7 @@ public abstract class Transition implements Cloneable { * This is a utility method used by subclasses to handle standard parts of * setting up and running an Animator: it sets the {@link #getDuration() * duration} and the {@link #getStartDelay() startDelay}, starts the - * animation, and, when the animator ends, calls {@link #endTransition()}. + * animation, and, when the animator ends, calls {@link #end()}. * * @param animator The Animator to be run during this transition. * @@ -716,7 +907,7 @@ public abstract class Transition implements Cloneable { protected void animate(Animator animator) { // TODO: maybe pass auto-end as a boolean parameter? if (animator == null) { - endTransition(); + end(); } else { if (getDuration() >= 0) { animator.setDuration(getDuration()); @@ -730,7 +921,7 @@ public abstract class Transition implements Cloneable { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - endTransition(); + end(); animation.removeListener(this); } }); @@ -739,39 +930,14 @@ public abstract class Transition implements Cloneable { } /** - * Subclasses may override to receive notice of when the transition starts. - * This is equivalent to listening for the - * {@link TransitionListener#onTransitionStart(Transition)} callback. - */ - protected void onTransitionStart() { - } - - /** - * Subclasses may override to receive notice of when the transition is - * canceled. This is equivalent to listening for the - * {@link TransitionListener#onTransitionCancel(Transition)} callback. - */ - protected void onTransitionCancel() { - } - - /** - * Subclasses may override to receive notice of when the transition ends. - * This is equivalent to listening for the - * {@link TransitionListener#onTransitionEnd(Transition)} callback. - */ - protected void onTransitionEnd() { - } - - /** * This method is called automatically by the transition and * TransitionGroup classes prior to a Transition subclass starting; * subclasses should not need to call it directly. * * @hide */ - protected void startTransition() { + protected void start() { if (mNumInstances == 0) { - onTransitionStart(); if (mListeners != null && mListeners.size() > 0) { ArrayList<TransitionListener> tmpListeners = (ArrayList<TransitionListener>) mListeners.clone(); @@ -790,15 +956,14 @@ public abstract class Transition implements Cloneable { * a transition did nothing (returned a null Animator from * {@link Transition#play(ViewGroup, TransitionValues, * TransitionValues)}) or because the transition returned a valid - * Animator and endTransition() was called in the onAnimationEnd() + * Animator and end() was called in the onAnimationEnd() * callback of the AnimatorListener. * * @hide */ - protected void endTransition() { + protected void end() { --mNumInstances; if (mNumInstances == 0) { - onTransitionEnd(); if (mListeners != null && mListeners.size() > 0) { ArrayList<TransitionListener> tmpListeners = (ArrayList<TransitionListener>) mListeners.clone(); @@ -828,7 +993,7 @@ public abstract class Transition implements Cloneable { * This method cancels a transition that is currently running. * Implementation TBD. */ - protected void cancelTransition() { + protected void cancel() { // TODO: how does this work with instances? // TODO: this doesn't actually do *anything* yet int numAnimators = mCurrentAnimators.size(); @@ -836,7 +1001,6 @@ public abstract class Transition implements Cloneable { Animator animator = mCurrentAnimators.get(i); animator.cancel(); } - onTransitionCancel(); if (mListeners != null && mListeners.size() > 0) { ArrayList<TransitionListener> tmpListeners = (ArrayList<TransitionListener>) mListeners.clone(); @@ -901,11 +1065,28 @@ public abstract class Transition implements Cloneable { Transition clone = null; try { clone = (Transition) super.clone(); + clone.mAnimators = new ArrayList<Animator>(); } catch (CloneNotSupportedException e) {} return clone; } + /** + * Returns the name of this Transition. This name is used internally to distinguish + * between different transitions to determine when interrupting transitions overlap. + * For example, a Move running on the same target view as another Move should determine + * whether the old transition is animating to different end values and should be + * canceled in favor of the new transition. + * + * <p>By default, a Transition's name is simply the value of {@link Class#getName()}, + * but subclasses are free to override and return something different.</p> + * + * @return The name of this transition. + */ + public String getName() { + return mName; + } + String toString(String indent) { String result = indent + getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + ": "; @@ -943,8 +1124,7 @@ public abstract class Transition implements Cloneable { /** * A transition listener receives notifications from a transition. - * Notifications indicate transition lifecycle events: when the transition - * begins, ends, or is canceled. + * Notifications indicate transition lifecycle events. */ public static interface TransitionListener { /** @@ -957,7 +1137,7 @@ public abstract class Transition implements Cloneable { /** * Notification about the end of the transition. Canceled transitions * will always notify listeners of both the cancellation and end - * events. That is, {@link #onTransitionEnd()} is always called, + * events. That is, {@link #onTransitionEnd(Transition)} is always called, * regardless of whether the transition was canceled or played * through to completion. * @@ -967,10 +1147,38 @@ public abstract class Transition implements Cloneable { /** * Notification about the cancellation of the transition. + * Note that cancel() may be called by a parent {@link TransitionGroup} on + * a child transition which has not yet started. This allows the child + * transition to restore state on target objects which was set at + * {@link #play(android.view.ViewGroup, TransitionValues, TransitionValues) + * play()} time. * * @param transition The transition which was canceled. */ void onTransitionCancel(Transition transition); + + /** + * Notification when a transition is paused. + * Note that play() may be called by a parent {@link TransitionGroup} on + * a child transition which has not yet started. This allows the child + * transition to restore state on target objects which was set at + * {@link #play(android.view.ViewGroup, TransitionValues, TransitionValues) + * play()} time. + * + * @param transition The transition which was paused. + */ + void onTransitionPause(Transition transition); + + /** + * Notification when a transition is resumed. + * Note that resume() may be called by a parent {@link TransitionGroup} on + * a child transition which has not yet started. This allows the child + * transition to restore state which may have changed in an earlier call + * to {@link #onTransitionPause(Transition)}. + * + * @param transition The transition which was resumed. + */ + void onTransitionResume(Transition transition); } /** @@ -991,6 +1199,32 @@ public abstract class Transition implements Cloneable { @Override public void onTransitionCancel(Transition transition) { } + + @Override + public void onTransitionPause(Transition transition) { + } + + @Override + public void onTransitionResume(Transition transition) { + } } + /** + * Holds information about each animator used when a new transition starts + * while other transitions are still running to determine whether a running + * animation should be canceled or a new animation noop'd. The structure holds + * information about the state that an animation is going to, to be compared to + * end state of a new animation. + */ + private static class AnimationInfo { + View view; + String name; + TransitionValues values; + + AnimationInfo(View view, String name, TransitionValues values) { + this.view = view; + this.name = name; + this.values = values; + } + } } diff --git a/core/java/android/view/transition/TransitionGroup.java b/core/java/android/view/transition/TransitionGroup.java index 313e33e..b3bacde 100644 --- a/core/java/android/view/transition/TransitionGroup.java +++ b/core/java/android/view/transition/TransitionGroup.java @@ -164,7 +164,7 @@ public class TransitionGroup extends Transition { @Override public void onTransitionStart(Transition transition) { if (!mTransitionGroup.mStarted) { - mTransitionGroup.startTransition(); + mTransitionGroup.start(); mTransitionGroup.mStarted = true; } } @@ -175,7 +175,7 @@ public class TransitionGroup extends Transition { if (mTransitionGroup.mCurrentListeners == 0) { // All child trans mTransitionGroup.mStarted = false; - mTransitionGroup.endTransition(); + mTransitionGroup.end(); } transition.removeListener(this); } @@ -233,12 +233,32 @@ public class TransitionGroup extends Transition { } } + /** @hide */ @Override - protected void cancelTransition() { - super.cancelTransition(); + public void pause() { + super.pause(); int numTransitions = mTransitions.size(); for (int i = 0; i < numTransitions; ++i) { - mTransitions.get(i).cancelTransition(); + mTransitions.get(i).pause(); + } + } + + /** @hide */ + @Override + public void resume() { + super.resume(); + int numTransitions = mTransitions.size(); + for (int i = 0; i < numTransitions; ++i) { + mTransitions.get(i).resume(); + } + } + + @Override + protected void cancel() { + super.cancel(); + int numTransitions = mTransitions.size(); + for (int i = 0; i < numTransitions; ++i) { + mTransitions.get(i).cancel(); } } diff --git a/core/java/android/view/transition/TransitionManager.java b/core/java/android/view/transition/TransitionManager.java index 7836268..3cb6f68 100644 --- a/core/java/android/view/transition/TransitionManager.java +++ b/core/java/android/view/transition/TransitionManager.java @@ -18,6 +18,7 @@ package android.view.transition; import android.util.ArrayMap; import android.util.Log; +import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; @@ -45,8 +46,8 @@ public class TransitionManager { ArrayMap<Scene, Transition> mSceneTransitions = new ArrayMap<Scene, Transition>(); ArrayMap<Scene, ArrayMap<Scene, Transition>> mScenePairTransitions = new ArrayMap<Scene, ArrayMap<Scene, Transition>>(); - static ArrayMap<ViewGroup, Transition> sRunningTransitions = - new ArrayMap<ViewGroup, Transition>(); + private static ThreadLocal<ArrayMap<ViewGroup, ArrayList<Transition>>> sRunningTransitions = + new ThreadLocal<ArrayMap<ViewGroup, ArrayList<Transition>>>(); private static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<ViewGroup>(); @@ -160,6 +161,16 @@ public class TransitionManager { sceneChangeRunTransition(sceneRoot, transitionClone); } + private static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() { + ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions = + sRunningTransitions.get(); + if (runningTransitions == null) { + runningTransitions = new ArrayMap<ViewGroup, ArrayList<Transition>>(); + sRunningTransitions.set(runningTransitions); + } + return runningTransitions; + } + private static void sceneChangeRunTransition(final ViewGroup sceneRoot, final Transition transition) { if (transition != null) { @@ -169,16 +180,31 @@ public class TransitionManager { sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); sPendingTransitions.remove(sceneRoot); // Add to running list, handle end to remove it - sRunningTransitions.put(sceneRoot, transition); + final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions = + getRunningTransitions(); + ArrayList<Transition> currentTransitions = runningTransitions.get(sceneRoot); + if (currentTransitions == null) { + currentTransitions = new ArrayList<Transition>(); + runningTransitions.put(sceneRoot, currentTransitions); + } + currentTransitions.add(transition); transition.addListener(new Transition.TransitionListenerAdapter() { @Override public void onTransitionEnd(Transition transition) { - sRunningTransitions.remove(sceneRoot); + ArrayList<Transition> currentTransitions = + runningTransitions.get(sceneRoot); + currentTransitions.remove(transition); } }); transition.captureValues(sceneRoot, false); transition.playTransition(sceneRoot); - return true; + + // Returning false from onPreDraw() skips the current frame. This is + // necessary to avoid artifacts caused by resetting target views + // to their proper end states for capturing. Waiting until the next + // frame to draw allows these views to have their mid-transition + // values set on them again and avoid artifacts. + return false; } }); } @@ -187,14 +213,16 @@ public class TransitionManager { private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) { // Capture current values - Transition runningTransition = sRunningTransitions.get(sceneRoot); + ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot); - if (transition != null) { - transition.captureValues(sceneRoot, true); + if (runningTransitions != null && runningTransitions.size() > 0) { + for (Transition runningTransition : runningTransitions) { + runningTransition.pause(); + } } - if (runningTransition != null) { - runningTransition.cancelTransition(); + if (transition != null) { + transition.captureValues(sceneRoot, true); } // Notify previous scene that it is being exited diff --git a/core/java/android/view/transition/Visibility.java b/core/java/android/view/transition/Visibility.java index 6d39ab6..96ea044 100644 --- a/core/java/android/view/transition/Visibility.java +++ b/core/java/android/view/transition/Visibility.java @@ -19,6 +19,7 @@ package android.view.transition; import android.animation.Animator; import android.view.View; import android.view.ViewGroup; +import android.view.ViewOverlay; import android.view.ViewParent; /** @@ -38,6 +39,10 @@ public abstract class Visibility extends Transition { private static final String PROPNAME_VISIBILITY = "android:visibility:visibility"; private static final String PROPNAME_PARENT = "android:visibility:parent"; + private static String[] sTransitionProperties = { + PROPNAME_VISIBILITY, + PROPNAME_PARENT, + }; private static class VisibilityInfo { boolean visibilityChange; @@ -52,12 +57,42 @@ public abstract class Visibility extends Transition { private VisibilityInfo mTmpVisibilityInfo = new VisibilityInfo(); @Override + public String[] getTransitionProperties() { + return sTransitionProperties; + } + + @Override protected void captureValues(TransitionValues values, boolean start) { int visibility = values.view.getVisibility(); values.values.put(PROPNAME_VISIBILITY, visibility); values.values.put(PROPNAME_PARENT, values.view.getParent()); } + /** + * Returns whether the view is 'visible' according to the given values + * object. This is determined by testing the same properties in the values + * object that are used to determine whether the object is appearing or + * disappearing in the {@link + * #play(android.view.ViewGroup, TransitionValues, TransitionValues)} + * method. This method can be called by, for example, subclasses that want + * to know whether the object is visible in the same way that Visibility + * determines it for the actual animation. + * + * @param values The TransitionValues object that holds the information by + * which visibility is determined. + * @return True if the view reference by <code>values</code> is visible, + * false otherwise. + */ + public boolean isVisible(TransitionValues values) { + if (values == null) { + return false; + } + int visibility = (Integer) values.values.get(PROPNAME_VISIBILITY); + View parent = (View) values.values.get(PROPNAME_PARENT); + + return visibility == View.VISIBLE && parent != null; + } + private boolean isHierarchyVisibilityChanging(ViewGroup sceneRoot, ViewGroup view) { if (view == sceneRoot) { @@ -197,5 +232,4 @@ public abstract class Visibility extends Transition { TransitionValues endValues, int endVisibility) { return null; } - } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 0224fbe..03bde70 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -780,7 +780,8 @@ public class WebView extends AbsoluteLayout * {@link #loadUrl(String)} instead. * * @param url the URL of the resource to load - * @param postData the data will be passed to "POST" request + * @param postData the data will be passed to "POST" request, which must be + * be "application/x-www-form-urlencoded" encoded. */ public void postUrl(String url, byte[] postData) { checkThread(); diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 07198c7..285e6f2 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -1211,13 +1211,19 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** - * Enables fast scrolling by letting the user quickly scroll through lists by - * dragging the fast scroll thumb. The adapter attached to the list may want - * to implement {@link SectionIndexer} if it wishes to display alphabet preview and - * jump between sections of the list. + * Specifies whether fast scrolling is enabled or disabled. + * <p> + * When fast scrolling is enabled, the user can quickly scroll through lists + * by dragging the fast scroll thumb. + * <p> + * If the adapter backing this list implements {@link SectionIndexer}, the + * fast scroller will display section header previews as the user scrolls. + * Additionally, the user will be able to quickly jump between sections by + * tapping along the length of the scroll bar. + * * @see SectionIndexer * @see #isFastScrollEnabled() - * @param enabled whether or not to enable fast scrolling + * @param enabled true to enable fast scrolling, false otherwise */ public void setFastScrollEnabled(final boolean enabled) { if (mFastScrollEnabled != enabled) { @@ -1252,13 +1258,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** - * Set whether or not the fast scroller should always be shown in place of the - * standard scrollbars. Fast scrollers shown in this way will not fade out and will - * be a permanent fixture within the list. Best combined with an inset scroll bar style - * that will ensure enough padding. This will enable fast scrolling if it is not + * Set whether or not the fast scroller should always be shown in place of + * the standard scroll bars. This will enable fast scrolling if it is not * already enabled. + * <p> + * Fast scrollers shown in this way will not fade out and will be a + * permanent fixture within the list. This is best combined with an inset + * scroll bar style to ensure the scroll bar does not overlap content. * - * @param alwaysShow true if the fast scroller should always be displayed. + * @param alwaysShow true if the fast scroller should always be displayed, + * false otherwise * @see #setScrollBarStyle(int) * @see #setFastScrollEnabled(boolean) */ @@ -1297,10 +1306,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** - * Returns true if the fast scroller is set to always show on this view rather than - * fade out when not in use. + * Returns true if the fast scroller is set to always show on this view. * - * @return true if the fast scroller will always show. + * @return true if the fast scroller will always show * @see #setFastScrollAlwaysVisible(boolean) */ public boolean isFastScrollAlwaysVisible() { @@ -1316,7 +1324,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** - * Returns the current state of the fast scroll feature. + * Returns true if the fast scroller is enabled. + * * @see #setFastScrollEnabled(boolean) * @return true if fast scroll is enabled, false otherwise */ diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java index 414c318..2b4e520 100644 --- a/core/java/android/widget/ListPopupWindow.java +++ b/core/java/android/widget/ListPopupWindow.java @@ -16,6 +16,9 @@ package android.widget; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; import android.content.Context; import android.database.DataSetObserver; import android.graphics.Rect; @@ -23,6 +26,7 @@ import android.graphics.drawable.Drawable; import android.os.Handler; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.IntProperty; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; @@ -31,6 +35,7 @@ import android.view.View.MeasureSpec; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.animation.AccelerateDecelerateInterpolator; import java.util.Locale; @@ -956,6 +961,33 @@ public class ListPopupWindow { } /** + * Receives motion events forwarded from a source view. This is used + * internally to implement support for drag-to-open. + * + * @param src view from which the event was forwarded + * @param srcEvent forwarded motion event in source-local coordinates + * @param activePointerId id of the pointer that activated forwarding + * @return whether the event was handled + * @hide + */ + public boolean onForwardedEvent(View src, MotionEvent srcEvent, int activePointerId) { + final DropDownListView dst = mDropDownList; + if (dst == null || !dst.isShown()) { + return false; + } + + // Convert event to local coordinates. + final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent); + src.toGlobalMotionEvent(dstEvent); + dst.toLocalMotionEvent(dstEvent); + + // Forward converted event, then recycle it. + final boolean handled = dst.onForwardedEvent(dstEvent, activePointerId); + dstEvent.recycle(); + return handled; + } + + /** * <p>Builds the popup window's content and returns the height the popup * should have. Returns -1 when the content already exists.</p> * @@ -1130,6 +1162,27 @@ public class ListPopupWindow { */ private static class DropDownListView extends ListView { private static final String TAG = ListPopupWindow.TAG + ".DropDownListView"; + + /** Duration in milliseconds of the drag-to-open click animation. */ + private static final long CLICK_ANIM_DURATION = 150; + + /** Target alpha value for drag-to-open click animation. */ + private static final int CLICK_ANIM_ALPHA = 0x80; + + /** Wrapper around Drawable's <code>alpha</code> property. */ + private static final IntProperty<Drawable> DRAWABLE_ALPHA = + new IntProperty<Drawable>("alpha") { + @Override + public void setValue(Drawable object, int value) { + object.setAlpha(value); + } + + @Override + public Integer get(Drawable object) { + return object.getAlpha(); + } + }; + /* * WARNING: This is a workaround for a touch mode issue. * @@ -1165,6 +1218,12 @@ public class ListPopupWindow { */ private boolean mHijackFocus; + /** Whether to force drawing of the pressed state selector. */ + private boolean mDrawsInPressedState; + + /** Current drag-to-open click animation, if any. */ + private Animator mClickAnimation; + /** * <p>Creates a new list view wrapper.</p> * @@ -1178,6 +1237,119 @@ public class ListPopupWindow { } /** + * Handles forwarded events. + * + * @param activePointerId id of the pointer that activated forwarding + * @return whether the event was handled + */ + public boolean onForwardedEvent(MotionEvent event, int activePointerId) { + boolean handledEvent = true; + boolean clearPressedItem = false; + + final int actionMasked = event.getActionMasked(); + switch (actionMasked) { + case MotionEvent.ACTION_CANCEL: + handledEvent = false; + break; + case MotionEvent.ACTION_UP: + handledEvent = false; + // $FALL-THROUGH$ + case MotionEvent.ACTION_MOVE: + final int activeIndex = event.findPointerIndex(activePointerId); + if (activeIndex < 0) { + handledEvent = false; + break; + } + + final int x = (int) event.getX(activeIndex); + final int y = (int) event.getY(activeIndex); + final int position = pointToPosition(x, y); + if (position == INVALID_POSITION) { + clearPressedItem = true; + break; + } + + final View child = getChildAt(position - getFirstVisiblePosition()); + setPressedItem(child, position); + handledEvent = true; + + if (actionMasked == MotionEvent.ACTION_UP) { + clickPressedItem(child, position); + } + break; + } + + // Failure to handle the event cancels forwarding. + if (!handledEvent || clearPressedItem) { + clearPressedItem(); + } + + return handledEvent; + } + + /** + * Starts an alpha animation on the selector. When the animation ends, + * the list performs a click on the item. + */ + private void clickPressedItem(final View child, final int position) { + final long id = getItemIdAtPosition(position); + final Animator anim = ObjectAnimator.ofInt( + mSelector, DRAWABLE_ALPHA, 0xFF, CLICK_ANIM_ALPHA, 0xFF); + anim.setDuration(CLICK_ANIM_DURATION); + anim.setInterpolator(new AccelerateDecelerateInterpolator()); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + performItemClick(child, position, id); + } + }); + anim.start(); + + if (mClickAnimation != null) { + mClickAnimation.cancel(); + } + mClickAnimation = anim; + } + + private void clearPressedItem() { + mDrawsInPressedState = false; + setPressed(false); + updateSelectorState(); + + if (mClickAnimation != null) { + mClickAnimation.cancel(); + mClickAnimation = null; + } + } + + private void setPressedItem(View child, int position) { + mDrawsInPressedState = true; + + // Ordering is essential. First update the pressed state and layout + // the children. This will ensure the selector actually gets drawn. + setPressed(true); + layoutChildren(); + + // Ensure that keyboard focus starts from the last touched position. + setSelectedPositionInt(position); + positionSelector(position, child); + + // Refresh the drawable state to reflect the new pressed state, + // which will also update the selector state. + refreshDrawableState(); + + if (mClickAnimation != null) { + mClickAnimation.cancel(); + mClickAnimation = null; + } + } + + @Override + boolean touchModeDrawsInPressedState() { + return mDrawsInPressedState || super.touchModeDrawsInPressedState(); + } + + /** * <p>Avoids jarring scrolling effect by ensuring that list elements * made of a text view fit on a single line.</p> * diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index 4a98f66..19cc3c2 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -1425,6 +1425,7 @@ public class NumberPicker extends LinearLayout { @Override protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); removeAllCallbacks(); } diff --git a/core/java/android/widget/SectionIndexer.java b/core/java/android/widget/SectionIndexer.java index a1c71f4..f6333d1 100644 --- a/core/java/android/widget/SectionIndexer.java +++ b/core/java/android/widget/SectionIndexer.java @@ -17,38 +17,62 @@ package android.widget; /** - * Interface that should be implemented on Adapters to enable fast scrolling - * in an {@link AbsListView} between sections of the list. A section is a group of list items - * to jump to that have something in common. For example, they may begin with the - * same letter or they may be songs from the same artist. ExpandableListAdapters that - * consider groups and sections as synonymous should account for collapsed groups and return - * an appropriate section/position. + * Interface that may implemented on {@link Adapter}s to enable fast scrolling + * between sections of an {@link AbsListView}. + * <p> + * A section is a group of list items that have something in common. For + * example, they may begin with the same letter or they may be songs from the + * same artist. + * <p> + * {@link ExpandableListAdapter}s that consider groups and sections as + * synonymous should account for collapsed groups and return an appropriate + * section/position. + * + * @see AbsListView#setFastScrollEnabled(boolean) */ public interface SectionIndexer { /** - * This provides the list view with an array of section objects. In the simplest - * case these are Strings, each containing one letter of the alphabet. - * They could be more complex objects that indicate the grouping for the adapter's - * consumption. The list view will call toString() on the objects to get the - * preview letter to display while scrolling. - * @return the array of objects that indicate the different sections of the list. + * Returns an array of objects representing sections of the list. The + * returned array and its contents should be non-null. + * <p> + * The list view will call toString() on the objects to get the preview text + * to display while scrolling. For example, an adapter may return an array + * of Strings representing letters of the alphabet. Or, it may return an + * array of objects whose toString() methods return their section titles. + * + * @return the array of section objects */ Object[] getSections(); - + /** - * Provides the starting index in the list for a given section. - * @param section the index of the section to jump to. - * @return the starting position of that section. If the section is out of bounds, the - * position must be clipped to fall within the size of the list. + * Given the index of a section within the array of section objects, returns + * the starting position of that section within the adapter. + * <p> + * If the section's starting position is outside of the adapter bounds, the + * position must be clipped to fall within the size of the adapter. + * + * @param sectionIndex the index of the section within the array of section + * objects + * @return the starting position of that section within the adapter, + * constrained to fall within the adapter bounds */ - int getPositionForSection(int section); - + int getPositionForSection(int sectionIndex); + /** - * This is a reverse mapping to fetch the section index for a given position - * in the list. - * @param position the position for which to return the section - * @return the section index. If the position is out of bounds, the section index + * Given a position within the adapter, returns the index of the + * corresponding section within the array of section objects. + * <p> + * If the section index is outside of the section array bounds, the index * must be clipped to fall within the size of the section array. + * <p> + * For example, consider an indexer where the section at array index 0 + * starts at adapter position 100. Calling this method with position 10, + * which is before the first section, must return index 0. + * + * @param position the position within the adapter for which to return the + * corresponding section index + * @return the index of the corresponding section within the array of + * section objects, constrained to fall within the array bounds */ - int getSectionForPosition(int position); + int getSectionForPosition(int position); } diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index e33c4d4..1c1d77a 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -22,10 +22,12 @@ import android.content.res.Configuration; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; +import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.EditorInfo; @@ -105,6 +107,9 @@ public class TimePicker extends FrameLayout { private Locale mCurrentLocale; + private boolean mHourWithTwoDigit; + private char mHourFormat; + /** * The callback interface used to indicate the time has been adjusted. */ @@ -164,7 +169,7 @@ public class TimePicker extends FrameLayout { // divider (only for the new widget style) mDivider = (TextView) findViewById(R.id.divider); if (mDivider != null) { - mDivider.setText(R.string.time_picker_separator); + setDividerText(); } // minute @@ -235,6 +240,24 @@ public class TimePicker extends FrameLayout { mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); } + if (isAmPmAtStart()) { + // Move the am/pm view to the beginning + ViewGroup amPmParent = (ViewGroup) findViewById(R.id.timePickerLayout); + amPmParent.removeView(amPmView); + amPmParent.addView(amPmView, 0); + // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme for + // example and not for Holo Theme) + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); + final int startMargin = lp.getMarginStart(); + final int endMargin = lp.getMarginEnd(); + if (startMargin != endMargin) { + lp.setMarginStart(endMargin); + lp.setMarginEnd(startMargin); + } + } + + getHourFormatData(); + // update controls to initial state updateHourControl(); updateMinuteControl(); @@ -259,6 +282,35 @@ public class TimePicker extends FrameLayout { } } + private void getHourFormatData() { + final Locale defaultLocale = Locale.getDefault(); + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, + (mIs24HourView) ? "Hm" : "hm"); + final int lengthPattern = bestDateTimePattern.length(); + mHourWithTwoDigit = false; + char hourFormat = '\0'; + // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save + // the hour format that we found. + for (int i = 0; i < lengthPattern; i++) { + final char c = bestDateTimePattern.charAt(i); + if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { + mHourFormat = c; + if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { + mHourWithTwoDigit = true; + } + break; + } + } + } + + private boolean isAmPmAtStart() { + final Locale defaultLocale = Locale.getDefault(); + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, + "hm" /* skeleton */); + + return bestDateTimePattern.startsWith("a"); + } + @Override public void setEnabled(boolean enabled) { if (mIsEnabled == enabled) { @@ -423,9 +475,11 @@ public class TimePicker extends FrameLayout { if (mIs24HourView == is24HourView) { return; } - mIs24HourView = is24HourView; - // cache the current hour since spinner range changes + // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! int currentHour = getCurrentHour(); + // Order is important here. + mIs24HourView = is24HourView; + getHourFormatData(); updateHourControl(); // set value after spinner range is updated setCurrentHour(currentHour); @@ -458,6 +512,38 @@ public class TimePicker extends FrameLayout { onTimeChanged(); } + /** + * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". + * + * See http://unicode.org/cldr/trac/browser/trunk/common/main + * + * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the + * separator as the character which is just after the hour marker in the returned pattern. + */ + private void setDividerText() { + final Locale defaultLocale = Locale.getDefault(); + final String skeleton = (mIs24HourView) ? "Hm" : "hm"; + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, + skeleton); + final String separatorText; + int hourIndex = bestDateTimePattern.lastIndexOf('H'); + if (hourIndex == -1) { + hourIndex = bestDateTimePattern.lastIndexOf('h'); + } + if (hourIndex == -1) { + // Default case + separatorText = ":"; + } else { + int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); + if (minuteIndex == -1) { + separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); + } else { + separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); + } + } + mDivider.setText(separatorText); + } + @Override public int getBaseline() { return mHourSpinner.getBaseline(); @@ -500,14 +586,25 @@ public class TimePicker extends FrameLayout { private void updateHourControl() { if (is24HourView()) { - mHourSpinner.setMinValue(0); - mHourSpinner.setMaxValue(23); - mHourSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); + // 'k' means 1-24 hour + if (mHourFormat == 'k') { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(24); + } else { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(23); + } } else { - mHourSpinner.setMinValue(1); - mHourSpinner.setMaxValue(12); - mHourSpinner.setFormatter(null); + // 'K' means 0-11 hour + if (mHourFormat == 'K') { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(11); + } else { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(12); + } } + mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); } private void updateMinuteControl() { |
