summaryrefslogtreecommitdiffstats
path: root/core/java/android
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android')
-rw-r--r--core/java/android/animation/TypeEvaluator.java4
-rw-r--r--core/java/android/app/ActivityThread.java2
-rw-r--r--core/java/android/app/NativeActivity.java12
-rw-r--r--core/java/android/bluetooth/BluetoothA2dp.java60
-rw-r--r--core/java/android/bluetooth/BluetoothUuid.java2
-rw-r--r--core/java/android/bluetooth/IBluetoothA2dp.aidl3
-rw-r--r--core/java/android/content/ContentResolver.java15
-rw-r--r--core/java/android/content/Intent.java4
-rw-r--r--core/java/android/database/MatrixCursor.java47
-rw-r--r--core/java/android/gesture/GestureOverlayView.java1
-rw-r--r--core/java/android/hardware/ICameraService.aidl19
-rw-r--r--core/java/android/hardware/camera2/CameraAccessException.java9
-rw-r--r--core/java/android/hardware/camera2/CameraManager.java27
-rw-r--r--core/java/android/hardware/camera2/CaptureRequest.java37
-rw-r--r--core/java/android/hardware/camera2/utils/BinderHolder.aidl20
-rw-r--r--core/java/android/hardware/camera2/utils/BinderHolder.java74
-rw-r--r--core/java/android/hardware/camera2/utils/CameraBinderDecorator.java8
-rw-r--r--core/java/android/os/Parcel.java5
-rw-r--r--core/java/android/os/ParcelFileDescriptor.java744
-rw-r--r--core/java/android/provider/DocumentsContract.java228
-rw-r--r--core/java/android/provider/MediaStore.java6
-rw-r--r--core/java/android/text/format/DateUtils.java5
-rw-r--r--core/java/android/view/DisplayList.java22
-rw-r--r--core/java/android/view/GLES20DisplayList.java1
-rw-r--r--core/java/android/view/GLES20Layer.java3
-rw-r--r--core/java/android/view/View.java20
-rw-r--r--core/java/android/view/ViewGroup.java12
-rw-r--r--core/java/android/view/ViewRootImpl.java13
-rw-r--r--core/java/android/view/WindowManagerGlobal.java4
-rw-r--r--core/java/android/view/inputmethod/InputMethodInfo.java23
-rw-r--r--core/java/android/view/transition/Fade.java107
-rw-r--r--core/java/android/view/transition/Move.java68
-rw-r--r--core/java/android/view/transition/Transition.java376
-rw-r--r--core/java/android/view/transition/TransitionGroup.java30
-rw-r--r--core/java/android/view/transition/TransitionManager.java48
-rw-r--r--core/java/android/view/transition/Visibility.java36
-rw-r--r--core/java/android/webkit/WebView.java3
-rw-r--r--core/java/android/widget/AbsListView.java37
-rw-r--r--core/java/android/widget/ListPopupWindow.java172
-rw-r--r--core/java/android/widget/NumberPicker.java1
-rw-r--r--core/java/android/widget/SectionIndexer.java72
-rw-r--r--core/java/android/widget/TimePicker.java115
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() {