diff options
56 files changed, 5673 insertions, 2719 deletions
diff --git a/api/current.txt b/api/current.txt index b22dd1c12906..612d16de8332 100644 --- a/api/current.txt +++ b/api/current.txt @@ -10973,6 +10973,8 @@ package android.graphics { public class ImageFormat { ctor public ImageFormat(); method public static int getBitsPerPixel(int); + field public static final int DEPTH16 = 1144402265; // 0x44363159 + field public static final int DEPTH_POINT_CLOUD = 257; // 0x101 field public static final int JPEG = 256; // 0x100 field public static final int NV16 = 16; // 0x10 field public static final int NV21 = 17; // 0x11 diff --git a/api/system-current.txt b/api/system-current.txt index 2b8ce989ca24..ff9809ffb14b 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -11248,6 +11248,8 @@ package android.graphics { public class ImageFormat { ctor public ImageFormat(); method public static int getBitsPerPixel(int); + field public static final int DEPTH16 = 1144402265; // 0x44363159 + field public static final int DEPTH_POINT_CLOUD = 257; // 0x101 field public static final int JPEG = 256; // 0x100 field public static final int NV16 = 16; // 0x10 field public static final int NV21 = 17; // 0x11 @@ -15139,6 +15141,7 @@ package android.media { public final class AudioAttributes implements android.os.Parcelable { method public int describeContents(); + method public int getAllFlags(); method public int getCapturePreset(); method public int getContentType(); method public int getFlags(); @@ -15180,6 +15183,7 @@ package android.media { method public android.media.AudioAttributes.Builder setCapturePreset(int); method public android.media.AudioAttributes.Builder setContentType(int); method public android.media.AudioAttributes.Builder setFlags(int); + method public android.media.AudioAttributes.Builder setInternalCapturePreset(int); method public android.media.AudioAttributes.Builder setLegacyStreamType(int); method public android.media.AudioAttributes.Builder setUsage(int); } @@ -15412,6 +15416,7 @@ package android.media { public class AudioRecord { ctor public AudioRecord(int, int, int, int, int) throws java.lang.IllegalArgumentException; + ctor public AudioRecord(android.media.AudioAttributes, android.media.AudioFormat, int, int) throws java.lang.IllegalArgumentException; method public int getAudioFormat(); method public int getAudioSessionId(); method public int getAudioSource(); @@ -16550,7 +16555,9 @@ package android.media { public final class MediaRecorder.AudioSource { field public static final int CAMCORDER = 5; // 0x5 field public static final int DEFAULT = 0; // 0x0 + field public static final int HOTWORD = 1999; // 0x7cf field public static final int MIC = 1; // 0x1 + field public static final int RADIO_TUNER = 1998; // 0x7ce field public static final int REMOTE_SUBMIX = 8; // 0x8 field public static final int VOICE_CALL = 4; // 0x4 field public static final int VOICE_COMMUNICATION = 7; // 0x7 diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 4a3650fc4a6a..e23ffe412d4a 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -3681,9 +3681,14 @@ public class DevicePolicyManager { /** * Check whether the current user has been blocked by device policy from uninstalling a package. * Requires the caller to be the profile owner if checking a specific admin's policy. + * <p> + * <strong>Note:</strong> Starting from {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}, the + * behavior of this API is changed such that passing <code>null</code> as the <code>admin</code> + * parameter will return if any admin has blocked the uninstallation. Before L MR1, passing + * <code>null</code> will cause a NullPointerException to be raised. * * @param admin The name of the admin component whose blocking policy will be checked, or null - * to check if any admin has blocked the uninstallation. + * to check if any admin has blocked the uninstallation. * @param packageName package to check. * @return true if uninstallation is blocked. */ diff --git a/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java b/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java index ce87329ee43d..161c339e78a8 100644 --- a/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java +++ b/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java @@ -26,32 +26,32 @@ import android.os.Parcelable; * @hide */ public final class BluetoothActivityEnergyInfo implements Parcelable { + private final long mTimestamp; private final int mBluetoothStackState; private final int mControllerTxTimeMs; private final int mControllerRxTimeMs; private final int mControllerIdleTimeMs; private final int mControllerEnergyUsed; - private final long timestamp; public static final int BT_STACK_STATE_INVALID = 0; public static final int BT_STACK_STATE_STATE_ACTIVE = 1; public static final int BT_STACK_STATE_STATE_SCANNING = 2; public static final int BT_STACK_STATE_STATE_IDLE = 3; - public BluetoothActivityEnergyInfo(int stackState, int txTime, int rxTime, - int idleTime, int energyUsed) { + public BluetoothActivityEnergyInfo(long timestamp, int stackState, + int txTime, int rxTime, int idleTime, int energyUsed) { + mTimestamp = timestamp; mBluetoothStackState = stackState; mControllerTxTimeMs = txTime; mControllerRxTimeMs = rxTime; mControllerIdleTimeMs = idleTime; mControllerEnergyUsed = energyUsed; - timestamp = System.currentTimeMillis(); } @Override public String toString() { return "BluetoothActivityEnergyInfo{" - + " timestamp=" + timestamp + + " mTimestamp=" + mTimestamp + " mBluetoothStackState=" + mBluetoothStackState + " mControllerTxTimeMs=" + mControllerTxTimeMs + " mControllerRxTimeMs=" + mControllerRxTimeMs @@ -63,13 +63,14 @@ public final class BluetoothActivityEnergyInfo implements Parcelable { public static final Parcelable.Creator<BluetoothActivityEnergyInfo> CREATOR = new Parcelable.Creator<BluetoothActivityEnergyInfo>() { public BluetoothActivityEnergyInfo createFromParcel(Parcel in) { + long timestamp = in.readLong(); int stackState = in.readInt(); int txTime = in.readInt(); int rxTime = in.readInt(); int idleTime = in.readInt(); int energyUsed = in.readInt(); - return new BluetoothActivityEnergyInfo(stackState, txTime, rxTime, - idleTime, energyUsed); + return new BluetoothActivityEnergyInfo(timestamp, stackState, + txTime, rxTime, idleTime, energyUsed); } public BluetoothActivityEnergyInfo[] newArray(int size) { return new BluetoothActivityEnergyInfo[size]; @@ -77,6 +78,7 @@ public final class BluetoothActivityEnergyInfo implements Parcelable { }; public void writeToParcel(Parcel out, int flags) { + out.writeLong(mTimestamp); out.writeInt(mBluetoothStackState); out.writeInt(mControllerTxTimeMs); out.writeInt(mControllerRxTimeMs); @@ -123,11 +125,12 @@ public final class BluetoothActivityEnergyInfo implements Parcelable { public int getControllerEnergyUsed() { return mControllerEnergyUsed; } + /** - * @return timestamp(wall clock) of record creation + * @return timestamp(real time elapsed in milliseconds since boot) of record creation. */ public long getTimeStamp() { - return timestamp; + return mTimestamp; } /** diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index 410849a35877..1fca9204fe42 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -444,8 +444,10 @@ public class TypedArray { } return defValue; } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); throw new UnsupportedOperationException( - "Failed to resolve attribute at index " + index); + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to color: type=0x" @@ -480,7 +482,7 @@ public class TypedArray { if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { if (value.type == TypedValue.TYPE_ATTRIBUTE) { throw new UnsupportedOperationException( - "Failed to resolve attribute at index " + index); + "Failed to resolve attribute at index " + index + ": " + value); } return mResources.loadColorStateList(value, value.resourceId, mTheme); } @@ -516,8 +518,10 @@ public class TypedArray { && type <= TypedValue.TYPE_LAST_INT) { return data[index+AssetManager.STYLE_DATA]; } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); throw new UnsupportedOperationException( - "Failed to resolve attribute at index " + index); + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to integer: type=0x" @@ -560,8 +564,10 @@ public class TypedArray { return TypedValue.complexToDimension( data[index + AssetManager.STYLE_DATA], mMetrics); } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); throw new UnsupportedOperationException( - "Failed to resolve attribute at index " + index); + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" @@ -605,8 +611,10 @@ public class TypedArray { return TypedValue.complexToDimensionPixelOffset( data[index + AssetManager.STYLE_DATA], mMetrics); } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); throw new UnsupportedOperationException( - "Failed to resolve attribute at index " + index); + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" @@ -651,8 +659,10 @@ public class TypedArray { return TypedValue.complexToDimensionPixelSize( data[index+AssetManager.STYLE_DATA], mMetrics); } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); throw new UnsupportedOperationException( - "Failed to resolve attribute at index " + index); + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" @@ -692,8 +702,10 @@ public class TypedArray { return TypedValue.complexToDimensionPixelSize( data[index+AssetManager.STYLE_DATA], mMetrics); } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); throw new UnsupportedOperationException( - "Failed to resolve attribute at index " + index); + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException(getPositionDescription() @@ -765,8 +777,10 @@ public class TypedArray { return TypedValue.complexToFraction( data[index+AssetManager.STYLE_DATA], base, pbase); } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value); throw new UnsupportedOperationException( - "Failed to resolve attribute at index " + index); + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to fraction: type=0x" @@ -853,7 +867,7 @@ public class TypedArray { if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { if (value.type == TypedValue.TYPE_ATTRIBUTE) { throw new UnsupportedOperationException( - "Failed to resolve attribute at index " + index); + "Failed to resolve attribute at index " + index + ": " + value); } return mResources.loadDrawable(value, value.resourceId, mTheme); } diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index 87ec06a5b237..a0217c2171b4 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -2493,6 +2493,83 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri public static final Key<Integer> SYNC_MAX_LATENCY = new Key<Integer>("android.sync.maxLatency", int.class); + /** + * <p>The available depth dataspace stream + * configurations that this camera device supports + * (i.e. format, width, height, output/input stream).</p> + * <p>These are output stream configurations for use with + * dataSpace HAL_DATASPACE_DEPTH. The configurations are + * listed as <code>(format, width, height, input?)</code> tuples.</p> + * <p>Only devices that support depth output for at least + * the HAL_PIXEL_FORMAT_Y16 dense depth map may include + * this entry.</p> + * <p>A device that also supports the HAL_PIXEL_FORMAT_BLOB + * sparse depth point cloud must report a single entry for + * the format in this list as <code>(HAL_PIXEL_FORMAT_BLOB, + * android.depth.maxDepthSamples, 1, OUTPUT)</code> in addition to + * the entries for HAL_PIXEL_FORMAT_Y16.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @hide + */ + public static final Key<android.hardware.camera2.params.StreamConfiguration[]> DEPTH_AVAILABLE_DEPTH_STREAM_CONFIGURATIONS = + new Key<android.hardware.camera2.params.StreamConfiguration[]>("android.depth.availableDepthStreamConfigurations", android.hardware.camera2.params.StreamConfiguration[].class); + + /** + * <p>This lists the minimum frame duration for each + * format/size combination for depth output formats.</p> + * <p>This should correspond to the frame duration when only that + * stream is active, with all processing (typically in android.*.mode) + * set to either OFF or FAST.</p> + * <p>When multiple streams are used in a request, the minimum frame + * duration will be max(individual stream min durations).</p> + * <p>The minimum frame duration of a stream (of a particular format, size) + * is the same regardless of whether the stream is input or output.</p> + * <p>See {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} and + * android.scaler.availableStallDurations for more details about + * calculating the max frame rate.</p> + * <p>(Keep in sync with + * StreamConfigurationMap#getOutputMinFrameDuration)</p> + * <p><b>Units</b>: (format, width, height, ns) x n</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @hide + */ + public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> DEPTH_AVAILABLE_DEPTH_MIN_FRAME_DURATIONS = + new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.depth.availableDepthMinFrameDurations", android.hardware.camera2.params.StreamConfigurationDuration[].class); + + /** + * <p>This lists the maximum stall duration for each + * format/size combination for depth streams.</p> + * <p>A stall duration is how much extra time would get added + * to the normal minimum frame duration for a repeating request + * that has streams with non-zero stall.</p> + * <p>This functions similarly to + * android.scaler.availableStallDurations for depth + * streams.</p> + * <p>All depth output stream formats may have a nonzero stall + * duration.</p> + * <p><b>Units</b>: (format, width, height, ns) x n</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @hide + */ + public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> DEPTH_AVAILABLE_DEPTH_STALL_DURATIONS = + new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.depth.availableDepthStallDurations", android.hardware.camera2.params.StreamConfigurationDuration[].class); + /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * End generated code *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/ diff --git a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl index 50a58edda35b..d286d381f6e8 100644 --- a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl +++ b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl @@ -66,7 +66,7 @@ interface ICameraDeviceUser int deleteStream(int streamId); // non-negative value is the stream ID. negative value is status_t - int createStream(int width, int height, int format, in Surface surface); + int createStream(in Surface surface); int createDefaultRequest(int templateId, out CameraMetadataNative request); diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index ec450bd14cc8..78aefa5675f3 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -373,9 +373,7 @@ public class CameraDeviceImpl extends CameraDevice { // Add all new streams for (Surface s : addSet) { - // TODO: remove width,height,format since we are ignoring - // it. - int streamId = mRemoteDevice.createStream(0, 0, 0, s); + int streamId = mRemoteDevice.createStream(s); mConfiguredOutputs.put(streamId, s); } diff --git a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java index fcf172c47ec4..26cd498751d0 100644 --- a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java +++ b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java @@ -504,7 +504,7 @@ public class CameraDeviceUserShim implements ICameraDeviceUser { } @Override - public int createStream(int width, int height, int format, Surface surface) { + public int createStream(Surface surface) { if (DEBUG) { Log.d(TAG, "createStream called."); } diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index d03fe4153588..d96a0e955068 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -441,14 +441,14 @@ public abstract class BatteryStats implements Parcelable { public abstract boolean isActive(); /** - * Returns the total time (in 1/100 sec) spent executing in user code. + * Returns the total time (in milliseconds) spent executing in user code. * * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract long getUserTime(int which); /** - * Returns the total time (in 1/100 sec) spent executing in system code. + * Returns the total time (in milliseconds) spent executing in system code. * * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ @@ -476,14 +476,14 @@ public abstract class BatteryStats implements Parcelable { public abstract int getNumAnrs(int which); /** - * Returns the cpu time spent in microseconds while the process was in the foreground. + * Returns the cpu time (milliseconds) spent while the process was in the foreground. * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. * @return foreground cpu time in microseconds */ public abstract long getForegroundTime(int which); /** - * Returns the approximate cpu time spent in microseconds, at a certain CPU speed. + * Returns the approximate cpu time (in milliseconds) spent at a certain CPU speed. * @param speedStep the index of the CPU speed. This is not the actual speed of the * CPU. * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. @@ -1858,6 +1858,15 @@ public abstract class BatteryStats implements Parcelable { public abstract long getNetworkActivityBytes(int type, int which); public abstract long getNetworkActivityPackets(int type, int which); + public static final int CONTROLLER_IDLE_TIME = 0; + public static final int CONTROLLER_RX_TIME = 1; + public static final int CONTROLLER_TX_TIME = 2; + public static final int CONTROLLER_ENERGY = 3; + public static final int NUM_CONTROLLER_ACTIVITY_TYPES = CONTROLLER_ENERGY + 1; + + public abstract long getBluetoothControllerActivity(int type, int which); + public abstract long getWifiControllerActivity(int type, int which); + /** * Return the wall clock time when battery stats data collection started. */ @@ -3142,6 +3151,35 @@ public abstract class BatteryStats implements Parcelable { if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); + final long wifiIdleTimeMs = getBluetoothControllerActivity(CONTROLLER_IDLE_TIME, which); + final long wifiRxTimeMs = getBluetoothControllerActivity(CONTROLLER_RX_TIME, which); + final long wifiTxTimeMs = getBluetoothControllerActivity(CONTROLLER_TX_TIME, which); + final long wifiTotalTimeMs = wifiIdleTimeMs + wifiRxTimeMs + wifiTxTimeMs; + + sb.setLength(0); + sb.append(prefix); + sb.append(" WiFi Idle time: "); formatTimeMs(sb, wifiIdleTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(wifiIdleTimeMs, wifiTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" WiFi Rx time: "); formatTimeMs(sb, wifiRxTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(wifiRxTimeMs, wifiTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" WiFi Tx time: "); formatTimeMs(sb, wifiTxTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(wifiTxTimeMs, wifiTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + sb.setLength(0); sb.append(prefix); sb.append(" Bluetooth on: "); formatTimeMs(sb, bluetoothOnTime / 1000); @@ -3169,9 +3207,41 @@ public abstract class BatteryStats implements Parcelable { sb.append(getPhoneDataConnectionCount(i, which)); sb.append("x"); } + if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); + final long bluetoothIdleTimeMs = + getBluetoothControllerActivity(CONTROLLER_IDLE_TIME, which); + final long bluetoothRxTimeMs = getBluetoothControllerActivity(CONTROLLER_RX_TIME, which); + final long bluetoothTxTimeMs = getBluetoothControllerActivity(CONTROLLER_TX_TIME, which); + final long bluetoothTotalTimeMs = bluetoothIdleTimeMs + bluetoothRxTimeMs + + bluetoothTxTimeMs; + + sb.setLength(0); + sb.append(prefix); + sb.append(" Bluetooth Idle time: "); formatTimeMs(sb, bluetoothIdleTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(bluetoothIdleTimeMs, bluetoothTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Bluetooth Rx time: "); formatTimeMs(sb, bluetoothRxTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(bluetoothRxTimeMs, bluetoothTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Bluetooth Tx time: "); formatTimeMs(sb, bluetoothTxTimeMs); + sb.append(" ("); + sb.append(formatRatioLocked(bluetoothTxTimeMs, bluetoothTotalTimeMs)); + sb.append(")"); + pw.println(sb.toString()); + pw.println(); if (which == STATS_SINCE_UNPLUGGED) { diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index 101457320b25..1a07aee5d482 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -532,10 +532,10 @@ public abstract class LayoutInflater { InflateException ex = new InflateException(e.getMessage()); ex.initCause(e); throw ex; - } catch (IOException e) { + } catch (Exception e) { InflateException ex = new InflateException( parser.getPositionDescription() - + ": " + e.getMessage()); + + ": " + e.getMessage()); ex.initCause(e); throw ex; } finally { diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 8197b3d4c710..0f99e8865ea9 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -3326,6 +3326,8 @@ public class Editor { mContainer.setSplitTouchEnabled(true); mContainer.setClippingEnabled(false); mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); + mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); mContainer.setContentView(this); mDrawableLtr = drawableLtr; diff --git a/core/java/com/android/internal/os/BatterySipper.java b/core/java/com/android/internal/os/BatterySipper.java index cfeca086b2b4..4cd959f11f20 100644 --- a/core/java/com/android/internal/os/BatterySipper.java +++ b/core/java/com/android/internal/os/BatterySipper.java @@ -26,12 +26,15 @@ public class BatterySipper implements Comparable<BatterySipper> { public double value; public double[] values; public DrainType drainType; + + // Measured in milliseconds. public long usageTime; public long cpuTime; public long gpsTime; public long wifiRunningTime; public long cpuFgTime; public long wakeLockTime; + public long mobileRxPackets; public long mobileTxPackets; public long mobileActive; @@ -48,6 +51,14 @@ public class BatterySipper implements Comparable<BatterySipper> { public String[] mPackages; public String packageWithHighestDrain; + // Measured in mAh (milli-ampere per hour). + public double wifiPower; + public double cpuPower; + public double wakeLockPower; + public double mobileRadioPower; + public double gpsPower; + public double sensorPower; + public enum DrainType { IDLE, CELL, @@ -107,4 +118,31 @@ public class BatterySipper implements Comparable<BatterySipper> { } return uidObj.getUid(); } + + /** + * Add stats from other to this BatterySipper. + */ + public void add(BatterySipper other) { + cpuTime += other.cpuTime; + gpsTime += other.gpsTime; + wifiRunningTime += other.wifiRunningTime; + cpuFgTime += other.cpuFgTime; + wakeLockTime += other.wakeLockTime; + mobileRxPackets += other.mobileRxPackets; + mobileTxPackets += other.mobileTxPackets; + mobileActive += other.mobileActive; + mobileActiveCount += other.mobileActiveCount; + wifiRxPackets += other.wifiRxPackets; + wifiTxPackets += other.wifiTxPackets; + mobileRxBytes += other.mobileRxBytes; + mobileTxBytes += other.mobileTxBytes; + wifiRxBytes += other.wifiRxBytes; + wifiTxBytes += other.wifiTxBytes; + wifiPower += other.wifiPower; + gpsPower += other.gpsPower; + cpuPower += other.cpuPower; + sensorPower += other.sensorPower; + mobileRadioPower += other.mobileRadioPower; + wakeLockPower += other.wakeLockPower; + } } diff --git a/core/java/com/android/internal/os/BatteryStatsHelper.java b/core/java/com/android/internal/os/BatteryStatsHelper.java index 5d73be0de3dc..d3611bf629c2 100644 --- a/core/java/com/android/internal/os/BatteryStatsHelper.java +++ b/core/java/com/android/internal/os/BatteryStatsHelper.java @@ -344,6 +344,7 @@ public final class BatteryStatsHelper { mMobilemsppList.add(bs); } } + for (int i=0; i<mUserSippers.size(); i++) { List<BatterySipper> user = mUserSippers.valueAt(i); for (int j=0; j<user.size(); j++) { @@ -389,8 +390,8 @@ public final class BatteryStatsHelper { private void processAppUsage(SparseArray<UserHandle> asUsers) { final boolean forAllUsers = (asUsers.get(UserHandle.USER_ALL) != null); - SensorManager sensorManager = (SensorManager) mContext.getSystemService( - Context.SENSOR_SERVICE); + final SensorManager sensorManager = + (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); final int which = mStatsType; final int speedSteps = mPowerProfile.getNumSpeedSteps(); final double[] powerCpuNormal = new double[speedSteps]; @@ -401,238 +402,317 @@ public final class BatteryStatsHelper { final double mobilePowerPerPacket = getMobilePowerPerPacket(); final double mobilePowerPerMs = getMobilePowerPerMs(); final double wifiPowerPerPacket = getWifiPowerPerPacket(); - long appWakelockTimeUs = 0; + long totalAppWakelockTimeUs = 0; BatterySipper osApp = null; mStatsPeriod = mTypeBatteryRealtime; - SparseArray<? extends Uid> uidStats = mStats.getUidStats(); + + final ArrayList<BatterySipper> appList = new ArrayList<>(); + + // Max values used to normalize later. + double maxWifiPower = 0; + double maxCpuPower = 0; + double maxWakeLockPower = 0; + double maxMobileRadioPower = 0; + double maxGpsPower = 0; + double maxSensorPower = 0; + + final SparseArray<? extends Uid> uidStats = mStats.getUidStats(); final int NU = uidStats.size(); for (int iu = 0; iu < NU; iu++) { - Uid u = uidStats.valueAt(iu); - double p; // in mAs - double power = 0; // in mAs - double highestDrain = 0; - String packageWithHighestDrain = null; - Map<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats(); - long cpuTime = 0; - long cpuFgTime = 0; - long wakelockTime = 0; - long gpsTime = 0; + final Uid u = uidStats.valueAt(iu); + final BatterySipper app = new BatterySipper( + BatterySipper.DrainType.APP, u, new double[]{0}); + + final Map<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats(); if (processStats.size() > 0) { - // Process CPU time + // Process CPU time. + + // Keep track of the package with highest drain. + double highestDrain = 0; + for (Map.Entry<String, ? extends BatteryStats.Uid.Proc> ent : processStats.entrySet()) { Uid.Proc ps = ent.getValue(); - final long userTime = ps.getUserTime(which); - final long systemTime = ps.getSystemTime(which); - final long foregroundTime = ps.getForegroundTime(which); - cpuFgTime += foregroundTime; - final long tmpCpuTime = userTime + systemTime; - int totalTimeAtSpeeds = 0; - // Get the total first + app.cpuFgTime += ps.getForegroundTime(which); + final long totalCpuTime = ps.getUserTime(which) + ps.getSystemTime(which); + app.cpuTime += totalCpuTime; + + // Calculate the total CPU time spent at the various speed steps. + long totalTimeAtSpeeds = 0; for (int step = 0; step < speedSteps; step++) { cpuSpeedStepTimes[step] = ps.getTimeAtCpuSpeedStep(step, which); totalTimeAtSpeeds += cpuSpeedStepTimes[step]; } - if (totalTimeAtSpeeds == 0) totalTimeAtSpeeds = 1; - // Then compute the ratio of time spent at each speed - double processPower = 0; + totalTimeAtSpeeds = Math.max(totalTimeAtSpeeds, 1); + + // Then compute the ratio of time spent at each speed and figure out + // the total power consumption. + double cpuPower = 0; for (int step = 0; step < speedSteps; step++) { - double ratio = (double) cpuSpeedStepTimes[step] / totalTimeAtSpeeds; - if (DEBUG && ratio != 0) Log.d(TAG, "UID " + u.getUid() + ": CPU step #" - + step + " ratio=" + makemAh(ratio) + " power=" - + makemAh(ratio*tmpCpuTime*powerCpuNormal[step] / (60*60*1000))); - processPower += ratio * tmpCpuTime * powerCpuNormal[step]; + final double ratio = (double) cpuSpeedStepTimes[step] / totalTimeAtSpeeds; + final double cpuSpeedStepPower = + ratio * totalCpuTime * powerCpuNormal[step]; + if (DEBUG && ratio != 0) { + Log.d(TAG, "UID " + u.getUid() + ": CPU step #" + + step + " ratio=" + makemAh(ratio) + " power=" + + makemAh(cpuSpeedStepPower / (60 * 60 * 1000))); + } + cpuPower += cpuSpeedStepPower; } - cpuTime += tmpCpuTime; - if (DEBUG && processPower != 0) { + + if (DEBUG && cpuPower != 0) { Log.d(TAG, String.format("process %s, cpu power=%s", - ent.getKey(), makemAh(processPower / (60*60*1000)))); + ent.getKey(), makemAh(cpuPower / (60 * 60 * 1000)))); } - power += processPower; - if (packageWithHighestDrain == null - || packageWithHighestDrain.startsWith("*")) { - highestDrain = processPower; - packageWithHighestDrain = ent.getKey(); - } else if (highestDrain < processPower - && !ent.getKey().startsWith("*")) { - highestDrain = processPower; - packageWithHighestDrain = ent.getKey(); + app.cpuPower += cpuPower; + + // Each App can have multiple packages and with multiple running processes. + // Keep track of the package who's process has the highest drain. + if (app.packageWithHighestDrain == null || + app.packageWithHighestDrain.startsWith("*")) { + highestDrain = cpuPower; + app.packageWithHighestDrain = ent.getKey(); + } else if (highestDrain < cpuPower && !ent.getKey().startsWith("*")) { + highestDrain = cpuPower; + app.packageWithHighestDrain = ent.getKey(); } } } - if (cpuFgTime > cpuTime) { - if (DEBUG && cpuFgTime > cpuTime + 10000) { + + // Ensure that the CPU times make sense. + if (app.cpuFgTime > app.cpuTime) { + if (DEBUG && app.cpuFgTime > app.cpuTime + 10000) { Log.d(TAG, "WARNING! Cputime is more than 10 seconds behind Foreground time"); } - cpuTime = cpuFgTime; // Statistics may not have been gathered yet. + + // Statistics may not have been gathered yet. + app.cpuTime = app.cpuFgTime; } - power /= (60*60*1000); + + // Convert the CPU power to mAh + app.cpuPower /= (60 * 60 * 1000); + maxCpuPower = Math.max(maxCpuPower, app.cpuPower); // Process wake lock usage - Map<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats = u.getWakelockStats(); + final Map<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats = + u.getWakelockStats(); + long wakeLockTimeUs = 0; for (Map.Entry<String, ? extends BatteryStats.Uid.Wakelock> wakelockEntry : wakelockStats.entrySet()) { - Uid.Wakelock wakelock = wakelockEntry.getValue(); + final Uid.Wakelock wakelock = wakelockEntry.getValue(); + // Only care about partial wake locks since full wake locks // are canceled when the user turns the screen off. BatteryStats.Timer timer = wakelock.getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL); if (timer != null) { - wakelockTime += timer.getTotalTimeLocked(mRawRealtime, which); + wakeLockTimeUs += timer.getTotalTimeLocked(mRawRealtime, which); } } - appWakelockTimeUs += wakelockTime; - wakelockTime /= 1000; // convert to millis - - // Add cost of holding a wake lock - p = (wakelockTime - * mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE)) / (60*60*1000); - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wake " - + wakelockTime + " power=" + makemAh(p)); - power += p; + app.wakeLockTime = wakeLockTimeUs / 1000; // convert to millis + totalAppWakelockTimeUs += wakeLockTimeUs; + + // Add cost of holding a wake lock. + app.wakeLockPower = (app.wakeLockTime * + mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE)) / (60 * 60 * 1000); + if (DEBUG && app.wakeLockPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": wake " + + app.wakeLockTime + " power=" + makemAh(app.wakeLockPower)); + } + maxWakeLockPower = Math.max(maxWakeLockPower, app.wakeLockPower); - // Add cost of mobile traffic - final long mobileRx = u.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, mStatsType); - final long mobileTx = u.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, mStatsType); - final long mobileRxB = u.getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, mStatsType); - final long mobileTxB = u.getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, mStatsType); + // Add cost of mobile traffic. final long mobileActive = u.getMobileRadioActiveTime(mStatsType); + app.mobileRxPackets = u.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, mStatsType); + app.mobileTxPackets = u.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, mStatsType); + app.mobileActive = mobileActive / 1000; + app.mobileActiveCount = u.getMobileRadioActiveCount(mStatsType); + app.mobileRxBytes = u.getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, mStatsType); + app.mobileTxBytes = u.getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, mStatsType); + if (mobileActive > 0) { // We are tracking when the radio is up, so can use the active time to // determine power use. mAppMobileActive += mobileActive; - p = (mobilePowerPerMs * mobileActive) / 1000; + app.mobileRadioPower = (mobilePowerPerMs * mobileActive) / 1000; } else { // We are not tracking when the radio is up, so must approximate power use // based on the number of packets. - p = (mobileRx + mobileTx) * mobilePowerPerPacket; + app.mobileRadioPower = (app.mobileRxPackets + app.mobileTxPackets) + * mobilePowerPerPacket; } - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": mobile packets " - + (mobileRx+mobileTx) + " active time " + mobileActive - + " power=" + makemAh(p)); - power += p; + if (DEBUG && app.mobileRadioPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": mobile packets " + + (app.mobileRxPackets + app.mobileTxPackets) + + " active time " + mobileActive + + " power=" + makemAh(app.mobileRadioPower)); + } + maxMobileRadioPower = Math.max(maxMobileRadioPower, app.mobileRadioPower); // Add cost of wifi traffic - final long wifiRx = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, mStatsType); - final long wifiTx = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, mStatsType); - final long wifiRxB = u.getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, mStatsType); - final long wifiTxB = u.getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, mStatsType); - p = (wifiRx + wifiTx) * wifiPowerPerPacket; - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wifi packets " - + (mobileRx+mobileTx) + " power=" + makemAh(p)); - power += p; + app.wifiRxPackets = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, mStatsType); + app.wifiTxPackets = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, mStatsType); + app.wifiRxBytes = u.getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, mStatsType); + app.wifiTxBytes = u.getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, mStatsType); + + final double wifiPacketPower = (app.wifiRxPackets + app.wifiTxPackets) + * wifiPowerPerPacket; + if (DEBUG && wifiPacketPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": wifi packets " + + (app.wifiRxPackets + app.wifiTxPackets) + + " power=" + makemAh(wifiPacketPower)); + } // Add cost of keeping WIFI running. - long wifiRunningTimeMs = u.getWifiRunningTime(mRawRealtime, which) / 1000; - mAppWifiRunning += wifiRunningTimeMs; - p = (wifiRunningTimeMs - * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) / (60*60*1000); - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wifi running " - + wifiRunningTimeMs + " power=" + makemAh(p)); - power += p; + app.wifiRunningTime = u.getWifiRunningTime(mRawRealtime, which) / 1000; + mAppWifiRunning += app.wifiRunningTime; + + final double wifiLockPower = (app.wifiRunningTime + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) / (60 * 60 * 1000); + if (DEBUG && wifiLockPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": wifi running " + + app.wifiRunningTime + " power=" + makemAh(wifiLockPower)); + } // Add cost of WIFI scans - long wifiScanTimeMs = u.getWifiScanTime(mRawRealtime, which) / 1000; - p = (wifiScanTimeMs - * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_SCAN)) / (60*60*1000); - if (DEBUG) Log.d(TAG, "UID " + u.getUid() + ": wifi scan " + wifiScanTimeMs - + " power=" + makemAh(p)); - power += p; + final long wifiScanTimeMs = u.getWifiScanTime(mRawRealtime, which) / 1000; + final double wifiScanPower = (wifiScanTimeMs + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_SCAN)) + / (60 * 60 * 1000); + if (DEBUG && wifiScanPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": wifi scan " + wifiScanTimeMs + + " power=" + makemAh(wifiScanPower)); + } + + // Add cost of WIFI batch scans. + double wifiBatchScanPower = 0; for (int bin = 0; bin < BatteryStats.Uid.NUM_WIFI_BATCHED_SCAN_BINS; bin++) { - long batchScanTimeMs = u.getWifiBatchedScanTime(bin, mRawRealtime, which) / 1000; - p = ((batchScanTimeMs + final long batchScanTimeMs = + u.getWifiBatchedScanTime(bin, mRawRealtime, which) / 1000; + final double batchScanPower = ((batchScanTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_BATCHED_SCAN, bin)) - ) / (60*60*1000); - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wifi batched scan # " + bin - + " time=" + batchScanTimeMs + " power=" + makemAh(p)); - power += p; + ) / (60 * 60 * 1000); + if (DEBUG && batchScanPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": wifi batched scan # " + bin + + " time=" + batchScanTimeMs + " power=" + makemAh(batchScanPower)); + } + wifiBatchScanPower += batchScanPower; } + // Add up all the WiFi costs. + app.wifiPower = wifiPacketPower + wifiLockPower + wifiScanPower + wifiBatchScanPower; + maxWifiPower = Math.max(maxWifiPower, app.wifiPower); + // Process Sensor usage - SparseArray<? extends BatteryStats.Uid.Sensor> sensorStats = u.getSensorStats(); - int NSE = sensorStats.size(); - for (int ise=0; ise<NSE; ise++) { - Uid.Sensor sensor = sensorStats.valueAt(ise); - int sensorHandle = sensorStats.keyAt(ise); - BatteryStats.Timer timer = sensor.getSensorTime(); - long sensorTime = timer.getTotalTimeLocked(mRawRealtime, which) / 1000; - double multiplier = 0; + final SparseArray<? extends BatteryStats.Uid.Sensor> sensorStats = u.getSensorStats(); + final int NSE = sensorStats.size(); + for (int ise = 0; ise < NSE; ise++) { + final Uid.Sensor sensor = sensorStats.valueAt(ise); + final int sensorHandle = sensorStats.keyAt(ise); + final BatteryStats.Timer timer = sensor.getSensorTime(); + final long sensorTime = timer.getTotalTimeLocked(mRawRealtime, which) / 1000; + double sensorPower = 0; switch (sensorHandle) { case Uid.Sensor.GPS: - multiplier = mPowerProfile.getAveragePower(PowerProfile.POWER_GPS_ON); - gpsTime = sensorTime; + app.gpsTime = sensorTime; + app.gpsPower = (app.gpsTime + * mPowerProfile.getAveragePower(PowerProfile.POWER_GPS_ON)) + / (60 * 60 * 1000); + sensorPower = app.gpsPower; + maxGpsPower = Math.max(maxGpsPower, app.gpsPower); break; default: List<Sensor> sensorList = sensorManager.getSensorList( android.hardware.Sensor.TYPE_ALL); for (android.hardware.Sensor s : sensorList) { if (s.getHandle() == sensorHandle) { - multiplier = s.getPower(); + sensorPower = (sensorTime * s.getPower()) / (60 * 60 * 1000); + app.sensorPower += sensorPower; break; } } } - p = (multiplier * sensorTime) / (60*60*1000); - if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": sensor #" + sensorHandle - + " time=" + sensorTime + " power=" + makemAh(p)); - power += p; + if (DEBUG && sensorPower != 0) { + Log.d(TAG, "UID " + u.getUid() + ": sensor #" + sensorHandle + + " time=" + sensorTime + " power=" + makemAh(sensorPower)); + } } + maxSensorPower = Math.max(maxSensorPower, app.sensorPower); - if (DEBUG && power != 0) Log.d(TAG, String.format("UID %d: total power=%s", - u.getUid(), makemAh(power))); - - // Add the app to the list if it is consuming power - final int userId = UserHandle.getUserId(u.getUid()); - if (power != 0 || u.getUid() == 0) { - BatterySipper app = new BatterySipper(BatterySipper.DrainType.APP, u, - new double[] {power}); - app.cpuTime = cpuTime; - app.gpsTime = gpsTime; - app.wifiRunningTime = wifiRunningTimeMs; - app.cpuFgTime = cpuFgTime; - app.wakeLockTime = wakelockTime; - app.mobileRxPackets = mobileRx; - app.mobileTxPackets = mobileTx; - app.mobileActive = mobileActive / 1000; - app.mobileActiveCount = u.getMobileRadioActiveCount(mStatsType); - app.wifiRxPackets = wifiRx; - app.wifiTxPackets = wifiTx; - app.mobileRxBytes = mobileRxB; - app.mobileTxBytes = mobileTxB; - app.wifiRxBytes = wifiRxB; - app.wifiTxBytes = wifiTxB; - app.packageWithHighestDrain = packageWithHighestDrain; - if (u.getUid() == Process.WIFI_UID) { - mWifiSippers.add(app); - mWifiPower += power; - } else if (u.getUid() == Process.BLUETOOTH_UID) { - mBluetoothSippers.add(app); - mBluetoothPower += power; - } else if (!forAllUsers && asUsers.get(userId) == null - && UserHandle.getAppId(u.getUid()) >= Process.FIRST_APPLICATION_UID) { - List<BatterySipper> list = mUserSippers.get(userId); - if (list == null) { - list = new ArrayList<BatterySipper>(); - mUserSippers.put(userId, list); - } - list.add(app); - if (power != 0) { - Double userPower = mUserPower.get(userId); - if (userPower == null) { - userPower = power; - } else { - userPower += power; - } - mUserPower.put(userId, userPower); - } - } else { - mUsageList.add(app); - if (power > mMaxPower) mMaxPower = power; - if (power > mMaxRealPower) mMaxRealPower = power; - mComputedPower += power; + final double totalUnnormalizedPower = app.cpuPower + app.wifiPower + app.wakeLockPower + + app.mobileRadioPower + app.gpsPower + app.sensorPower; + if (DEBUG && totalUnnormalizedPower != 0) { + Log.d(TAG, String.format("UID %d: total power=%s", + u.getUid(), makemAh(totalUnnormalizedPower))); + } + + // Add the app to the list if it is consuming power. + if (totalUnnormalizedPower != 0 || u.getUid() == 0) { + appList.add(app); + } + } + + // Fetch real power consumption from hardware. + double actualTotalWifiPower = 0.0; + if (mStats.getWifiControllerActivity(BatteryStats.CONTROLLER_ENERGY, mStatsType) != 0) { + final double kDefaultVoltage = 3.36; + final long energy = mStats.getWifiControllerActivity( + BatteryStats.CONTROLLER_ENERGY, mStatsType); + final double voltage = mPowerProfile.getAveragePowerOrDefault( + PowerProfile.OPERATING_VOLTAGE_WIFI, kDefaultVoltage); + actualTotalWifiPower = energy / (voltage * 1000*60*60); + } + + final int appCount = appList.size(); + for (int i = 0; i < appCount; i++) { + // Normalize power where possible. + final BatterySipper app = appList.get(i); + if (actualTotalWifiPower != 0) { + app.wifiPower = (app.wifiPower / maxWifiPower) * actualTotalWifiPower; + } + + // Assign the final power consumption here. + final double power = app.wifiPower + app.cpuPower + app.wakeLockPower + + app.mobileRadioPower + app.gpsPower + app.sensorPower; + app.values[0] = app.value = power; + + // + // Add the app to the app list, WiFi, Bluetooth, etc, or into "Other Users" list. + // + + final int uid = app.getUid(); + final int userId = UserHandle.getUserId(uid); + if (uid == Process.WIFI_UID) { + mWifiSippers.add(app); + mWifiPower += power; + } else if (uid == Process.BLUETOOTH_UID) { + mBluetoothSippers.add(app); + mBluetoothPower += power; + } else if (!forAllUsers && asUsers.get(userId) == null + && UserHandle.getAppId(uid) >= Process.FIRST_APPLICATION_UID) { + // We are told to just report this user's apps as one large entry. + List<BatterySipper> list = mUserSippers.get(userId); + if (list == null) { + list = new ArrayList<>(); + mUserSippers.put(userId, list); } - if (u.getUid() == 0) { - osApp = app; + list.add(app); + + Double userPower = mUserPower.get(userId); + if (userPower == null) { + userPower = power; + } else { + userPower += power; } + mUserPower.put(userId, userPower); + } else { + mUsageList.add(app); + if (power > mMaxPower) mMaxPower = power; + if (power > mMaxRealPower) mMaxRealPower = power; + mComputedPower += power; + } + + if (uid == 0) { + osApp = app; } } @@ -641,7 +721,7 @@ public final class BatteryStatsHelper { // this remainder to the OS, if possible. if (osApp != null) { long wakeTimeMillis = mBatteryUptime / 1000; - wakeTimeMillis -= (appWakelockTimeUs / 1000) + wakeTimeMillis -= (totalAppWakelockTimeUs / 1000) + (mStats.getScreenOnTime(mRawRealtime, which) / 1000); if (wakeTimeMillis > 0) { double power = (wakeTimeMillis @@ -741,46 +821,11 @@ public final class BatteryStatsHelper { for (int i=0; i<from.size(); i++) { BatterySipper wbs = from.get(i); if (DEBUG) Log.d(TAG, tag + " adding sipper " + wbs + ": cpu=" + wbs.cpuTime); - bs.cpuTime += wbs.cpuTime; - bs.gpsTime += wbs.gpsTime; - bs.wifiRunningTime += wbs.wifiRunningTime; - bs.cpuFgTime += wbs.cpuFgTime; - bs.wakeLockTime += wbs.wakeLockTime; - bs.mobileRxPackets += wbs.mobileRxPackets; - bs.mobileTxPackets += wbs.mobileTxPackets; - bs.mobileActive += wbs.mobileActive; - bs.mobileActiveCount += wbs.mobileActiveCount; - bs.wifiRxPackets += wbs.wifiRxPackets; - bs.wifiTxPackets += wbs.wifiTxPackets; - bs.mobileRxBytes += wbs.mobileRxBytes; - bs.mobileTxBytes += wbs.mobileTxBytes; - bs.wifiRxBytes += wbs.wifiRxBytes; - bs.wifiTxBytes += wbs.wifiTxBytes; + bs.add(wbs); } bs.computeMobilemspp(); } - private void addWiFiUsage() { - long onTimeMs = mStats.getWifiOnTime(mRawRealtime, mStatsType) / 1000; - long runningTimeMs = mStats.getGlobalWifiRunningTime(mRawRealtime, mStatsType) / 1000; - if (DEBUG) Log.d(TAG, "WIFI runningTime=" + runningTimeMs - + " app runningTime=" + mAppWifiRunning); - runningTimeMs -= mAppWifiRunning; - if (runningTimeMs < 0) runningTimeMs = 0; - double wifiPower = (onTimeMs * 0 /* TODO */ - * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON) - + runningTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) - / (60*60*1000); - if (DEBUG && wifiPower != 0) { - Log.d(TAG, "Wifi: time=" + runningTimeMs + " power=" + makemAh(wifiPower)); - } - if ((wifiPower+mWifiPower) != 0) { - BatterySipper bs = addEntry(BatterySipper.DrainType.WIFI, runningTimeMs, - wifiPower + mWifiPower); - aggregateSippers(bs, mWifiSippers, "WIFI"); - } - } - private void addIdleUsage() { long idleTimeMs = (mTypeBatteryRealtime - mStats.getScreenOnTime(mRawRealtime, mStatsType)) / 1000; @@ -794,24 +839,81 @@ public final class BatteryStatsHelper { } } - private void addBluetoothUsage() { - long btOnTimeMs = mStats.getBluetoothOnTime(mRawRealtime, mStatsType) / 1000; - double btPower = btOnTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_ON) - / (60*60*1000); - if (DEBUG && btPower != 0) { - Log.d(TAG, "Bluetooth: time=" + btOnTimeMs + " power=" + makemAh(btPower)); + /** + * We do per-app blaming of WiFi activity. If energy info is reported from the controller, + * then only the WiFi process gets blamed here since we normalize power calculations and + * assign all the power drain to apps. If energy info is not reported, we attribute the + * difference between total running time of WiFi for all apps and the actual running time + * of WiFi to the WiFi subsystem. + */ + private void addWiFiUsage() { + final long idleTimeMs = mStats.getWifiControllerActivity( + BatteryStats.CONTROLLER_IDLE_TIME, mStatsType); + final long txTimeMs = mStats.getWifiControllerActivity( + BatteryStats.CONTROLLER_TX_TIME, mStatsType); + final long rxTimeMs = mStats.getWifiControllerActivity( + BatteryStats.CONTROLLER_RX_TIME, mStatsType); + final long energy = mStats.getWifiControllerActivity( + BatteryStats.CONTROLLER_ENERGY, mStatsType); + final long totalTimeRunning = idleTimeMs + txTimeMs + rxTimeMs; + + double powerDrain = 0; + if (energy == 0 && totalTimeRunning > 0) { + // Energy is not reported, which means we may have left over power drain not attributed + // to any app. Assign this power to the WiFi app. + // TODO(adamlesinski): This mimics the old behavior. However, mAppWifiRunningTime + // is the accumulation of the time each app kept the WiFi chip on. Multiple apps + // can do this at the same time, so these times do not add up to the total time + // the WiFi chip was on. Consider normalizing the time spent running and calculating + // power from that? Normalizing the times will assign a weight to each app which + // should better represent power usage. + powerDrain = ((totalTimeRunning - mAppWifiRunning) + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) / (60*60*1000); } - int btPingCount = mStats.getBluetoothPingCount(); - double pingPower = (btPingCount - * mPowerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_AT_CMD)) - / (60*60*1000); - if (DEBUG && pingPower != 0) { - Log.d(TAG, "Bluetooth ping: count=" + btPingCount + " power=" + makemAh(pingPower)); + + if (DEBUG && powerDrain != 0) { + Log.d(TAG, "Wifi active: time=" + (txTimeMs + rxTimeMs) + + " power=" + makemAh(powerDrain)); + } + + // TODO(adamlesinski): mWifiPower is already added as a BatterySipper... + // Are we double counting here? + final double power = mWifiPower + powerDrain; + if (power > 0) { + BatterySipper bs = addEntry(BatterySipper.DrainType.WIFI, totalTimeRunning, power); + aggregateSippers(bs, mWifiSippers, "WIFI"); } - btPower += pingPower; - if ((btPower+mBluetoothPower) != 0) { - BatterySipper bs = addEntry(BatterySipper.DrainType.BLUETOOTH, btOnTimeMs, - btPower + mBluetoothPower); + } + + /** + * Bluetooth usage is not attributed to any apps yet, so the entire blame goes to the + * Bluetooth Category. + */ + private void addBluetoothUsage() { + final double kDefaultVoltage = 3.36; + final long idleTimeMs = mStats.getBluetoothControllerActivity( + BatteryStats.CONTROLLER_IDLE_TIME, mStatsType); + final long txTimeMs = mStats.getBluetoothControllerActivity( + BatteryStats.CONTROLLER_TX_TIME, mStatsType); + final long rxTimeMs = mStats.getBluetoothControllerActivity( + BatteryStats.CONTROLLER_RX_TIME, mStatsType); + final long energy = mStats.getBluetoothControllerActivity( + BatteryStats.CONTROLLER_ENERGY, mStatsType); + final double voltage = mPowerProfile.getAveragePowerOrDefault( + PowerProfile.OPERATING_VOLTAGE_BLUETOOTH, kDefaultVoltage); + + // energy is measured in mA * V * ms, and we are interested in mAh + final double powerDrain = energy / (voltage * 60*60*1000); + + if (DEBUG && powerDrain != 0) { + Log.d(TAG, "Bluetooth active: time=" + (txTimeMs + rxTimeMs) + + " power=" + makemAh(powerDrain)); + } + + final long totalTime = idleTimeMs + txTimeMs + rxTimeMs; + final double power = mBluetoothPower + powerDrain; + if (power > 0) { + BatterySipper bs = addEntry(BatterySipper.DrainType.BLUETOOTH, totalTime, power); aggregateSippers(bs, mBluetoothSippers, "Bluetooth"); } } diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index e02b9596f9b6..f9b1ca11922c 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -20,11 +20,15 @@ import static android.net.NetworkStats.UID_ALL; import static com.android.server.NetworkManagementSocketTagger.PROP_QTAGUID_ENABLED; import android.app.ActivityManager; +import android.bluetooth.BluetoothActivityEnergyInfo; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkStats; +import android.net.wifi.IWifiManager; +import android.net.wifi.WifiActivityEnergyInfo; import android.net.wifi.WifiManager; import android.os.BadParcelableException; import android.os.BatteryManager; @@ -38,6 +42,8 @@ import android.os.Parcel; import android.os.ParcelFormatException; import android.os.Parcelable; import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; import android.os.WorkSource; @@ -332,6 +338,12 @@ public final class BatteryStatsImpl extends BatteryStats { final LongSamplingCounter[] mNetworkPacketActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES]; + final LongSamplingCounter[] mBluetoothActivityCounters = + new LongSamplingCounter[NUM_CONTROLLER_ACTIVITY_TYPES]; + + final LongSamplingCounter[] mWifiActivityCounters = + new LongSamplingCounter[NUM_CONTROLLER_ACTIVITY_TYPES]; + boolean mWifiOn; StopwatchTimer mWifiOnTimer; @@ -4307,6 +4319,20 @@ public final class BatteryStatsImpl extends BatteryStats { return mBluetoothStateTimer[bluetoothState].getCountLocked(which); } + @Override public long getBluetoothControllerActivity(int type, int which) { + if (type >= 0 && type < mBluetoothActivityCounters.length) { + return mBluetoothActivityCounters[type].getCountLocked(which); + } + return 0; + } + + @Override public long getWifiControllerActivity(int type, int which) { + if (type >= 0 && type < mWifiActivityCounters.length) { + return mWifiActivityCounters[type].getCountLocked(which); + } + return 0; + } + @Override public long getFlashlightOnTime(long elapsedRealtimeUs, int which) { return mFlashlightOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @@ -6652,6 +6678,10 @@ public final class BatteryStatsImpl extends BatteryStats { mNetworkByteActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); mNetworkPacketActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); } + for (int i = 0; i < NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); + mWifiActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); + } mMobileRadioActiveTimer = new StopwatchTimer(null, -400, null, mOnBatteryTimeBase); mMobileRadioActivePerAppTimer = new StopwatchTimer(null, -401, null, mOnBatteryTimeBase); mMobileRadioActiveAdjustedTime = new LongSamplingCounter(mOnBatteryTimeBase); @@ -7239,6 +7269,10 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { mBluetoothStateTimer[i].reset(false); } + for (int i=0; i< NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i].reset(false); + mWifiActivityCounters[i].reset(false); + } mNumConnectivityChange = mLoadedNumConnectivityChange = mUnpluggedNumConnectivityChange = 0; for (int i=0; i<mUidStats.size(); i++) { @@ -7325,6 +7359,9 @@ public final class BatteryStatsImpl extends BatteryStats { public void pullPendingStateUpdatesLocked() { updateKernelWakelocksLocked(); updateNetworkActivityLocked(NET_UPDATE_ALL, SystemClock.elapsedRealtime()); + // TODO(adamlesinski): enable when bluedroid stops deadlocking. b/19248786 + // updateBluetoothControllerActivityLocked(); + updateWifiControllerActivityLocked(); if (mOnBatteryInternal) { final boolean screenOn = mScreenState == Display.STATE_ON; updateDischargeScreenLevelsLocked(screenOn, screenOn); @@ -7761,6 +7798,65 @@ public final class BatteryStatsImpl extends BatteryStats { } } + private void updateBluetoothControllerActivityLocked() { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter == null) { + return; + } + + // We read the data even if we are not on battery. Each read clears + // the previous data, so we must always read to make sure the + // data is for the current interval. + BluetoothActivityEnergyInfo info = adapter.getControllerActivityEnergyInfo( + BluetoothAdapter.ACTIVITY_ENERGY_INFO_REFRESHED); + if (info == null || !info.isValid() || !mOnBatteryInternal) { + // Bad info or we are not on battery. + return; + } + + mBluetoothActivityCounters[CONTROLLER_RX_TIME].addCountLocked( + info.getControllerRxTimeMillis()); + mBluetoothActivityCounters[CONTROLLER_TX_TIME].addCountLocked( + info.getControllerTxTimeMillis()); + mBluetoothActivityCounters[CONTROLLER_IDLE_TIME].addCountLocked( + info.getControllerIdleTimeMillis()); + mBluetoothActivityCounters[CONTROLLER_ENERGY].addCountLocked( + info.getControllerEnergyUsed()); + } + + private void updateWifiControllerActivityLocked() { + IWifiManager wifiManager = IWifiManager.Stub.asInterface( + ServiceManager.getService(Context.WIFI_SERVICE)); + if (wifiManager == null) { + return; + } + + WifiActivityEnergyInfo info; + try { + // We read the data even if we are not on battery. Each read clears + // the previous data, so we must always read to make sure the + // data is for the current interval. + info = wifiManager.reportActivityInfo(); + } catch (RemoteException e) { + // Nothing to report, WiFi is dead. + return; + } + + if (info == null || !info.isValid() || !mOnBatteryInternal) { + // Bad info or we are not on battery. + return; + } + + mWifiActivityCounters[CONTROLLER_RX_TIME].addCountLocked( + info.getControllerRxTimeMillis()); + mWifiActivityCounters[CONTROLLER_TX_TIME].addCountLocked( + info.getControllerTxTimeMillis()); + mWifiActivityCounters[CONTROLLER_IDLE_TIME].addCountLocked( + info.getControllerIdleTimeMillis()); + mWifiActivityCounters[CONTROLLER_ENERGY].addCountLocked( + info.getControllerEnergyUsed()); + } + public long getAwakeTimeBattery() { return computeBatteryUptime(getBatteryUptimeLocked(), STATS_CURRENT); } @@ -8475,6 +8571,15 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { mBluetoothStateTimer[i].readSummaryFromParcelLocked(in); } + + for (int i = 0; i < NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i].readSummaryFromParcelLocked(in); + } + + for (int i = 0; i < NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mWifiActivityCounters[i].readSummaryFromParcelLocked(in); + } + mNumConnectivityChange = mLoadedNumConnectivityChange = in.readInt(); mFlashlightOn = false; mFlashlightOnTimer.readSummaryFromParcelLocked(in); @@ -8763,6 +8868,12 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { mBluetoothStateTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS); } + for (int i=0; i< NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i].writeSummaryFromParcelLocked(out); + } + for (int i=0; i< NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mWifiActivityCounters[i].writeSummaryFromParcelLocked(out); + } out.writeInt(mNumConnectivityChange); mFlashlightOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); @@ -9067,6 +9178,15 @@ public final class BatteryStatsImpl extends BatteryStats { mBluetoothStateTimer[i] = new StopwatchTimer(null, -500-i, null, mOnBatteryTimeBase, in); } + + for (int i = 0; i < NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase, in); + } + + for (int i = 0; i < NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mWifiActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase, in); + } + mNumConnectivityChange = in.readInt(); mLoadedNumConnectivityChange = in.readInt(); mUnpluggedNumConnectivityChange = in.readInt(); @@ -9212,6 +9332,12 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { mBluetoothStateTimer[i].writeToParcel(out, uSecRealtime); } + for (int i=0; i< NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mBluetoothActivityCounters[i].writeToParcel(out); + } + for (int i=0; i< NUM_CONTROLLER_ACTIVITY_TYPES; i++) { + mWifiActivityCounters[i].writeToParcel(out); + } out.writeInt(mNumConnectivityChange); out.writeInt(mLoadedNumConnectivityChange); out.writeInt(mUnpluggedNumConnectivityChange); diff --git a/core/java/com/android/internal/os/PowerProfile.java b/core/java/com/android/internal/os/PowerProfile.java index b3bafa1007bd..944eb5aca9e3 100644 --- a/core/java/com/android/internal/os/PowerProfile.java +++ b/core/java/com/android/internal/os/PowerProfile.java @@ -76,6 +76,11 @@ public class PowerProfile { public static final String POWER_WIFI_ACTIVE = "wifi.active"; /** + * Operating voltage of the WiFi controller. + */ + public static final String OPERATING_VOLTAGE_WIFI = "wifi.voltage"; + + /** * Power consumption when GPS is on. */ public static final String POWER_GPS_ON = "gps.on"; @@ -96,6 +101,11 @@ public class PowerProfile { public static final String POWER_BLUETOOTH_AT_CMD = "bluetooth.at"; /** + * Operating voltage of the Bluetooth controller. + */ + public static final String OPERATING_VOLTAGE_BLUETOOTH = "bluetooth.voltage"; + + /** * Power consumption when screen is on, not including the backlight power. */ public static final String POWER_SCREEN_ON = "screen.on"; @@ -224,11 +234,13 @@ public class PowerProfile { } /** - * Returns the average current in mA consumed by the subsystem + * Returns the average current in mA consumed by the subsystem, or the given + * default value if the subsystem has no recorded value. * @param type the subsystem type + * @param defaultValue the value to return if the subsystem has no recorded value. * @return the average current in milliAmps. */ - public double getAveragePower(String type) { + public double getAveragePowerOrDefault(String type, double defaultValue) { if (sPowerMap.containsKey(type)) { Object data = sPowerMap.get(type); if (data instanceof Double[]) { @@ -237,9 +249,18 @@ public class PowerProfile { return (Double) sPowerMap.get(type); } } else { - return 0; + return defaultValue; } } + + /** + * Returns the average current in mA consumed by the subsystem + * @param type the subsystem type + * @return the average current in milliAmps. + */ + public double getAveragePower(String type) { + return getAveragePowerOrDefault(type, 0); + } /** * Returns the average current in mA consumed by the subsystem for the given level. diff --git a/core/java/com/android/internal/widget/PagerAdapter.java b/core/java/com/android/internal/widget/PagerAdapter.java new file mode 100644 index 000000000000..910a720eda03 --- /dev/null +++ b/core/java/com/android/internal/widget/PagerAdapter.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; + +/** + * Base class providing the adapter to populate pages inside of + * a {@link android.support.v4.view.ViewPager}. You will most likely want to use a more + * specific implementation of this, such as + * {@link android.support.v4.app.FragmentPagerAdapter} or + * {@link android.support.v4.app.FragmentStatePagerAdapter}. + * + * <p>When you implement a PagerAdapter, you must override the following methods + * at minimum:</p> + * <ul> + * <li>{@link #instantiateItem(android.view.ViewGroup, int)}</li> + * <li>{@link #destroyItem(android.view.ViewGroup, int, Object)}</li> + * <li>{@link #getCount()}</li> + * <li>{@link #isViewFromObject(android.view.View, Object)}</li> + * </ul> + * + * <p>PagerAdapter is more general than the adapters used for + * {@link android.widget.AdapterView AdapterViews}. Instead of providing a + * View recycling mechanism directly ViewPager uses callbacks to indicate the + * steps taken during an update. A PagerAdapter may implement a form of View + * recycling if desired or use a more sophisticated method of managing page + * Views such as Fragment transactions where each page is represented by its + * own Fragment.</p> + * + * <p>ViewPager associates each page with a key Object instead of working with + * Views directly. This key is used to track and uniquely identify a given page + * independent of its position in the adapter. A call to the PagerAdapter method + * {@link #startUpdate(android.view.ViewGroup)} indicates that the contents of the ViewPager + * are about to change. One or more calls to {@link #instantiateItem(android.view.ViewGroup, int)} + * and/or {@link #destroyItem(android.view.ViewGroup, int, Object)} will follow, and the end + * of an update will be signaled by a call to {@link #finishUpdate(android.view.ViewGroup)}. + * By the time {@link #finishUpdate(android.view.ViewGroup) finishUpdate} returns the views + * associated with the key objects returned by + * {@link #instantiateItem(android.view.ViewGroup, int) instantiateItem} should be added to + * the parent ViewGroup passed to these methods and the views associated with + * the keys passed to {@link #destroyItem(android.view.ViewGroup, int, Object) destroyItem} + * should be removed. The method {@link #isViewFromObject(android.view.View, Object)} identifies + * whether a page View is associated with a given key object.</p> + * + * <p>A very simple PagerAdapter may choose to use the page Views themselves + * as key objects, returning them from {@link #instantiateItem(android.view.ViewGroup, int)} + * after creation and adding them to the parent ViewGroup. A matching + * {@link #destroyItem(android.view.ViewGroup, int, Object)} implementation would remove the + * View from the parent ViewGroup and {@link #isViewFromObject(android.view.View, Object)} + * could be implemented as <code>return view == object;</code>.</p> + * + * <p>PagerAdapter supports data set changes. Data set changes must occur on the + * main thread and must end with a call to {@link #notifyDataSetChanged()} similar + * to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data + * set change may involve pages being added, removed, or changing position. The + * ViewPager will keep the current page active provided the adapter implements + * the method {@link #getItemPosition(Object)}.</p> + */ +public abstract class PagerAdapter { + private DataSetObservable mObservable = new DataSetObservable(); + + public static final int POSITION_UNCHANGED = -1; + public static final int POSITION_NONE = -2; + + /** + * Return the number of views available. + */ + public abstract int getCount(); + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void startUpdate(ViewGroup container) { + startUpdate((View) container); + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(android.view.ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + */ + public Object instantiateItem(ViewGroup container, int position) { + return instantiateItem((View) container, position); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(android.view.ViewGroup)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(android.view.View, int)}. + */ + public void destroyItem(ViewGroup container, int position, Object object) { + destroyItem((View) container, position, object); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(android.view.View, int)}. + */ + public void setPrimaryItem(ViewGroup container, int position, Object object) { + setPrimaryItem((View) container, position, object); + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void finishUpdate(ViewGroup container) { + finishUpdate((View) container); + } + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #startUpdate(android.view.ViewGroup)} + */ + public void startUpdate(View container) { + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(android.view.ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + * + * @deprecated Use {@link #instantiateItem(android.view.ViewGroup, int)} + */ + public Object instantiateItem(View container, int position) { + throw new UnsupportedOperationException( + "Required method instantiateItem was not overridden"); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(android.view.View)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(android.view.View, int)}. + * + * @deprecated Use {@link #destroyItem(android.view.ViewGroup, int, Object)} + */ + public void destroyItem(View container, int position, Object object) { + throw new UnsupportedOperationException("Required method destroyItem was not overridden"); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(android.view.View, int)}. + * + * @deprecated Use {@link #setPrimaryItem(android.view.ViewGroup, int, Object)} + */ + public void setPrimaryItem(View container, int position, Object object) { + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #finishUpdate(android.view.ViewGroup)} + */ + public void finishUpdate(View container) { + } + + /** + * Determines whether a page View is associated with a specific key object + * as returned by {@link #instantiateItem(android.view.ViewGroup, int)}. This method is + * required for a PagerAdapter to function properly. + * + * @param view Page View to check for association with <code>object</code> + * @param object Object to check for association with <code>view</code> + * @return true if <code>view</code> is associated with the key object <code>object</code> + */ + public abstract boolean isViewFromObject(View view, Object object); + + /** + * Save any instance state associated with this adapter and its pages that should be + * restored if the current UI state needs to be reconstructed. + * + * @return Saved state for this adapter + */ + public Parcelable saveState() { + return null; + } + + /** + * Restore any instance state associated with this adapter and its pages + * that was previously saved by {@link #saveState()}. + * + * @param state State previously saved by a call to {@link #saveState()} + * @param loader A ClassLoader that should be used to instantiate any restored objects + */ + public void restoreState(Parcelable state, ClassLoader loader) { + } + + /** + * Called when the host view is attempting to determine if an item's position + * has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given + * item has not changed or {@link #POSITION_NONE} if the item is no longer present + * in the adapter. + * + * <p>The default implementation assumes that items will never + * change position and always returns {@link #POSITION_UNCHANGED}. + * + * @param object Object representing an item, previously returned by a call to + * {@link #instantiateItem(android.view.View, int)}. + * @return object's new position index from [0, {@link #getCount()}), + * {@link #POSITION_UNCHANGED} if the object's position has not changed, + * or {@link #POSITION_NONE} if the item is no longer present. + */ + public int getItemPosition(Object object) { + return POSITION_UNCHANGED; + } + + /** + * This method should be called by the application if the data backing this adapter has changed + * and associated views should update. + */ + public void notifyDataSetChanged() { + mObservable.notifyChanged(); + } + + /** + * Register an observer to receive callbacks related to the adapter's data changing. + * + * @param observer The {@link android.database.DataSetObserver} which will receive callbacks. + */ + public void registerDataSetObserver(DataSetObserver observer) { + mObservable.registerObserver(observer); + } + + /** + * Unregister an observer from callbacks related to the adapter's data changing. + * + * @param observer The {@link android.database.DataSetObserver} which will be unregistered. + */ + public void unregisterDataSetObserver(DataSetObserver observer) { + mObservable.unregisterObserver(observer); + } + + /** + * This method may be called by the ViewPager to obtain a title string + * to describe the specified page. This method may return null + * indicating no title for this page. The default implementation returns + * null. + * + * @param position The position of the title requested + * @return A title for the requested page + */ + public CharSequence getPageTitle(int position) { + return null; + } + + /** + * Returns the proportional width of a given page as a percentage of the + * ViewPager's measured width from (0.f-1.f] + * + * @param position The position of the page requested + * @return Proportional width for the given page position + */ + public float getPageWidth(int position) { + return 1.f; + } +} diff --git a/core/java/com/android/internal/widget/ViewPager.java b/core/java/com/android/internal/widget/ViewPager.java new file mode 100644 index 000000000000..f916e6f784e7 --- /dev/null +++ b/core/java/com/android/internal/widget/ViewPager.java @@ -0,0 +1,2866 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.annotation.DrawableRes; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.view.FocusFinder; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityRecord; +import android.view.animation.Interpolator; +import android.widget.EdgeEffect; +import android.widget.Scroller; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +/** + * Layout manager that allows the user to flip left and right + * through pages of data. You supply an implementation of a + * {@link android.support.v4.view.PagerAdapter} to generate the pages that the view shows. + * + * <p>Note this class is currently under early design and + * development. The API will likely change in later updates of + * the compatibility library, requiring changes to the source code + * of apps when they are compiled against the newer version.</p> + * + * <p>ViewPager is most often used in conjunction with {@link android.app.Fragment}, + * which is a convenient way to supply and manage the lifecycle of each page. + * There are standard adapters implemented for using fragments with the ViewPager, + * which cover the most common use cases. These are + * {@link android.support.v4.app.FragmentPagerAdapter} and + * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these + * classes have simple code showing how to build a full user interface + * with them. + * + * <p>For more information about how to use ViewPager, read <a + * href="{@docRoot}training/implementing-navigation/lateral.html">Creating Swipe Views with + * Tabs</a>.</p> + * + * <p>Below is a more complicated example of ViewPager, using it in conjunction + * with {@link android.app.ActionBar} tabs. You can find other examples of using + * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code. + * + * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java + * complete} + */ +public class ViewPager extends ViewGroup { + private static final String TAG = "ViewPager"; + private static final boolean DEBUG = false; + + private static final boolean USE_CACHE = false; + + private static final int DEFAULT_OFFSCREEN_PAGES = 1; + private static final int MAX_SETTLE_DURATION = 600; // ms + private static final int MIN_DISTANCE_FOR_FLING = 25; // dips + + private static final int DEFAULT_GUTTER_SIZE = 16; // dips + + private static final int MIN_FLING_VELOCITY = 400; // dips + + private static final int[] LAYOUT_ATTRS = new int[] { + com.android.internal.R.attr.layout_gravity + }; + + /** + * Used to track what the expected number of items in the adapter should be. + * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. + */ + private int mExpectedAdapterCount; + + static class ItemInfo { + Object object; + int position; + boolean scrolling; + float widthFactor; + float offset; + } + + private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){ + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + return lhs.position - rhs.position; + } + }; + + private static final Interpolator sInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>(); + private final ItemInfo mTempItem = new ItemInfo(); + + private final Rect mTempRect = new Rect(); + + private PagerAdapter mAdapter; + private int mCurItem; // Index of currently displayed page. + private int mRestoredCurItem = -1; + private Parcelable mRestoredAdapterState = null; + private ClassLoader mRestoredClassLoader = null; + private Scroller mScroller; + private PagerObserver mObserver; + + private int mPageMargin; + private Drawable mMarginDrawable; + private int mTopPageBounds; + private int mBottomPageBounds; + + // Offsets of the first and last items, if known. + // Set during population, used to determine if we are at the beginning + // or end of the pager data set during touch scrolling. + private float mFirstOffset = -Float.MAX_VALUE; + private float mLastOffset = Float.MAX_VALUE; + + private int mChildWidthMeasureSpec; + private int mChildHeightMeasureSpec; + private boolean mInLayout; + + private boolean mScrollingCacheEnabled; + + private boolean mPopulatePending; + private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; + + private boolean mIsBeingDragged; + private boolean mIsUnableToDrag; + private int mDefaultGutterSize; + private int mGutterSize; + private int mTouchSlop; + /** + * Position of the last motion event. + */ + private float mLastMotionX; + private float mLastMotionY; + private float mInitialMotionX; + private float mInitialMotionY; + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + private int mMinimumVelocity; + private int mMaximumVelocity; + private int mFlingDistance; + private int mCloseEnough; + + // If the pager is at least this close to its final position, complete the scroll + // on touch down and let the user interact with the content inside instead of + // "catching" the flinging pager. + private static final int CLOSE_ENOUGH = 2; // dp + + private boolean mFakeDragging; + private long mFakeDragBeginTime; + + private EdgeEffect mLeftEdge; + private EdgeEffect mRightEdge; + + private boolean mFirstLayout = true; + private boolean mNeedCalculatePageOffsets = false; + private boolean mCalledSuper; + private int mDecorChildCount; + + private OnPageChangeListener mOnPageChangeListener; + private OnPageChangeListener mInternalPageChangeListener; + private OnAdapterChangeListener mAdapterChangeListener; + private PageTransformer mPageTransformer; + + private static final int DRAW_ORDER_DEFAULT = 0; + private static final int DRAW_ORDER_FORWARD = 1; + private static final int DRAW_ORDER_REVERSE = 2; + private int mDrawingOrder; + private ArrayList<View> mDrawingOrderedChildren; + private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); + + /** + * Indicates that the pager is in an idle, settled state. The current page + * is fully in view and no animation is in progress. + */ + public static final int SCROLL_STATE_IDLE = 0; + + /** + * Indicates that the pager is currently being dragged by the user. + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * Indicates that the pager is in the process of settling to a final position. + */ + public static final int SCROLL_STATE_SETTLING = 2; + + private final Runnable mEndScrollRunnable = new Runnable() { + public void run() { + setScrollState(SCROLL_STATE_IDLE); + populate(); + } + }; + + private int mScrollState = SCROLL_STATE_IDLE; + + /** + * Callback interface for responding to changing state of the selected page. + */ + public interface OnPageChangeListener { + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param positionOffset Value from [0, 1) indicating the offset from the page at position. + * @param positionOffsetPixels Value in pixels indicating the offset from position. + */ + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); + + /** + * This method will be invoked when a new page becomes selected. Animation is not + * necessarily complete. + * + * @param position Position index of the new selected page. + */ + public void onPageSelected(int position); + + /** + * Called when the scroll state changes. Useful for discovering when the user + * begins dragging, when the pager is automatically settling to the current page, + * or when it is fully stopped/idle. + * + * @param state The new scroll state. + * @see com.android.internal.widget.ViewPager#SCROLL_STATE_IDLE + * @see com.android.internal.widget.ViewPager#SCROLL_STATE_DRAGGING + * @see com.android.internal.widget.ViewPager#SCROLL_STATE_SETTLING + */ + public void onPageScrollStateChanged(int state); + } + + /** + * Simple implementation of the {@link OnPageChangeListener} interface with stub + * implementations of each method. Extend this if you do not intend to override + * every method of {@link OnPageChangeListener}. + */ + public static class SimpleOnPageChangeListener implements OnPageChangeListener { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // This space for rent + } + + @Override + public void onPageSelected(int position) { + // This space for rent + } + + @Override + public void onPageScrollStateChanged(int state) { + // This space for rent + } + } + + /** + * A PageTransformer is invoked whenever a visible/attached page is scrolled. + * This offers an opportunity for the application to apply a custom transformation + * to the page views using animation properties. + * + * <p>As property animation is only supported as of Android 3.0 and forward, + * setting a PageTransformer on a ViewPager on earlier platform versions will + * be ignored.</p> + */ + public interface PageTransformer { + /** + * Apply a property transformation to the given page. + * + * @param page Apply the transformation to this page + * @param position Position of page relative to the current front-and-center + * position of the pager. 0 is front and center. 1 is one full + * page position to the right, and -1 is one page position to the left. + */ + public void transformPage(View page, float position); + } + + /** + * Used internally to monitor when adapters are switched. + */ + interface OnAdapterChangeListener { + public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); + } + + /** + * Used internally to tag special types of child views that should be added as + * pager decorations by default. + */ + interface Decor {} + + public ViewPager(Context context) { + super(context); + initViewPager(); + } + + public ViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + initViewPager(); + } + + void initViewPager() { + setWillNotDraw(false); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setFocusable(true); + final Context context = getContext(); + mScroller = new Scroller(context, sInterpolator); + final ViewConfiguration configuration = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + + mTouchSlop = configuration.getScaledPagingTouchSlop(); + mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mLeftEdge = new EdgeEffect(context); + mRightEdge = new EdgeEffect(context); + + mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); + mCloseEnough = (int) (CLOSE_ENOUGH * density); + mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); + + setAccessibilityDelegate(new MyAccessibilityDelegate()); + + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(mEndScrollRunnable); + super.onDetachedFromWindow(); + } + + private void setScrollState(int newState) { + if (mScrollState == newState) { + return; + } + + mScrollState = newState; + if (mPageTransformer != null) { + // PageTransformers can do complex things that benefit from hardware layers. + enableLayers(newState != SCROLL_STATE_IDLE); + } + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrollStateChanged(newState); + } + } + + /** + * Set a PagerAdapter that will supply views for this pager as needed. + * + * @param adapter Adapter to use + */ + public void setAdapter(PagerAdapter adapter) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + mAdapter.startUpdate(this); + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + mAdapter.destroyItem(this, ii.position, ii.object); + } + mAdapter.finishUpdate(this); + mItems.clear(); + removeNonDecorViews(); + mCurItem = 0; + scrollTo(0, 0); + } + + final PagerAdapter oldAdapter = mAdapter; + mAdapter = adapter; + mExpectedAdapterCount = 0; + + if (mAdapter != null) { + if (mObserver == null) { + mObserver = new PagerObserver(); + } + mAdapter.registerDataSetObserver(mObserver); + mPopulatePending = false; + final boolean wasFirstLayout = mFirstLayout; + mFirstLayout = true; + mExpectedAdapterCount = mAdapter.getCount(); + if (mRestoredCurItem >= 0) { + mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); + setCurrentItemInternal(mRestoredCurItem, false, true); + mRestoredCurItem = -1; + mRestoredAdapterState = null; + mRestoredClassLoader = null; + } else if (!wasFirstLayout) { + populate(); + } else { + requestLayout(); + } + } + + if (mAdapterChangeListener != null && oldAdapter != adapter) { + mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); + } + } + + private void removeNonDecorViews() { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + removeViewAt(i); + i--; + } + } + } + + /** + * Retrieve the current adapter supplying pages. + * + * @return The currently registered PagerAdapter + */ + public PagerAdapter getAdapter() { + return mAdapter; + } + + void setOnAdapterChangeListener(OnAdapterChangeListener listener) { + mAdapterChangeListener = listener; + } + + private int getClientWidth() { + return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + } + + /** + * Set the currently selected page. If the ViewPager has already been through its first + * layout with its current adapter there will be a smooth animated transition between + * the current item and the specified item. + * + * @param item Item index to select + */ + public void setCurrentItem(int item) { + mPopulatePending = false; + setCurrentItemInternal(item, !mFirstLayout, false); + } + + /** + * Set the currently selected page. + * + * @param item Item index to select + * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately + */ + public void setCurrentItem(int item, boolean smoothScroll) { + mPopulatePending = false; + setCurrentItemInternal(item, smoothScroll, false); + } + + public int getCurrentItem() { + return mCurItem; + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { + setCurrentItemInternal(item, smoothScroll, always, 0); + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { + if (mAdapter == null || mAdapter.getCount() <= 0) { + setScrollingCacheEnabled(false); + return; + } + if (!always && mCurItem == item && mItems.size() != 0) { + setScrollingCacheEnabled(false); + return; + } + + if (item < 0) { + item = 0; + } else if (item >= mAdapter.getCount()) { + item = mAdapter.getCount() - 1; + } + final int pageLimit = mOffscreenPageLimit; + if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { + // We are doing a jump by more than one page. To avoid + // glitches, we want to keep all current pages in the view + // until the scroll ends. + for (int i=0; i<mItems.size(); i++) { + mItems.get(i).scrolling = true; + } + } + final boolean dispatchSelected = mCurItem != item; + + if (mFirstLayout) { + // We don't have any idea how big we are yet and shouldn't have any pages either. + // Just set things up and let the pending layout handle things. + mCurItem = item; + if (dispatchSelected && mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(item); + } + if (dispatchSelected && mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(item); + } + requestLayout(); + } else { + populate(item); + scrollToItem(item, smoothScroll, velocity, dispatchSelected); + } + } + + private void scrollToItem(int item, boolean smoothScroll, int velocity, + boolean dispatchSelected) { + final ItemInfo curInfo = infoForPosition(item); + int destX = 0; + if (curInfo != null) { + final int width = getClientWidth(); + destX = (int) (width * Math.max(mFirstOffset, + Math.min(curInfo.offset, mLastOffset))); + } + if (smoothScroll) { + smoothScrollTo(destX, 0, velocity); + if (dispatchSelected && mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(item); + } + if (dispatchSelected && mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(item); + } + } else { + if (dispatchSelected && mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(item); + } + if (dispatchSelected && mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(item); + } + completeScroll(false); + scrollTo(destX, 0); + pageScrolled(destX); + } + } + + /** + * Set a listener that will be invoked whenever the page changes or is incrementally + * scrolled. See {@link OnPageChangeListener}. + * + * @param listener Listener to set + */ + public void setOnPageChangeListener(OnPageChangeListener listener) { + mOnPageChangeListener = listener; + } + + /** + * Set a {@link PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding look and feel. + * + * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist. + * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p> + * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + */ + public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) { + final boolean hasTransformer = transformer != null; + final boolean needsPopulate = hasTransformer != (mPageTransformer != null); + mPageTransformer = transformer; + setChildrenDrawingOrderEnabled(hasTransformer); + if (hasTransformer) { + mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; + } else { + mDrawingOrder = DRAW_ORDER_DEFAULT; + } + if (needsPopulate) populate(); + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; + final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; + return result; + } + + /** + * Set a separate OnPageChangeListener for internal use by the support library. + * + * @param listener Listener to set + * @return The old listener that was set, if any. + */ + OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { + OnPageChangeListener oldListener = mInternalPageChangeListener; + mInternalPageChangeListener = listener; + return oldListener; + } + + /** + * Returns the number of pages that will be retained to either side of the + * current page in the view hierarchy in an idle state. Defaults to 1. + * + * @return How many pages will be kept offscreen on either side + * @see #setOffscreenPageLimit(int) + */ + public int getOffscreenPageLimit() { + return mOffscreenPageLimit; + } + + /** + * Set the number of pages that should be retained to either side of the + * current page in the view hierarchy in an idle state. Pages beyond this + * limit will be recreated from the adapter when needed. + * + * <p>This is offered as an optimization. If you know in advance the number + * of pages you will need to support or have lazy-loading mechanisms in place + * on your pages, tweaking this setting can have benefits in perceived smoothness + * of paging animations and interaction. If you have a small number of pages (3-4) + * that you can keep active all at once, less time will be spent in layout for + * newly created view subtrees as the user pages back and forth.</p> + * + * <p>You should keep this limit low, especially if your pages have complex layouts. + * This setting defaults to 1.</p> + * + * @param limit How many pages will be kept offscreen in an idle state. + */ + public void setOffscreenPageLimit(int limit) { + if (limit < DEFAULT_OFFSCREEN_PAGES) { + Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + + DEFAULT_OFFSCREEN_PAGES); + limit = DEFAULT_OFFSCREEN_PAGES; + } + if (limit != mOffscreenPageLimit) { + mOffscreenPageLimit = limit; + populate(); + } + } + + /** + * Set the margin between pages. + * + * @param marginPixels Distance between adjacent pages in pixels + * @see #getPageMargin() + * @see #setPageMarginDrawable(android.graphics.drawable.Drawable) + * @see #setPageMarginDrawable(int) + */ + public void setPageMargin(int marginPixels) { + final int oldMargin = mPageMargin; + mPageMargin = marginPixels; + + final int width = getWidth(); + recomputeScrollPosition(width, width, marginPixels, oldMargin); + + requestLayout(); + } + + /** + * Return the margin between pages. + * + * @return The size of the margin in pixels + */ + public int getPageMargin() { + return mPageMargin; + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param d Drawable to display between pages + */ + public void setPageMarginDrawable(Drawable d) { + mMarginDrawable = d; + if (d != null) refreshDrawableState(); + setWillNotDraw(d == null); + invalidate(); + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param resId Resource ID of a drawable to display between pages + */ + public void setPageMarginDrawable(@DrawableRes int resId) { + setPageMarginDrawable(getContext().getDrawable(resId)); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mMarginDrawable; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + final Drawable d = mMarginDrawable; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + */ + void smoothScrollTo(int x, int y) { + smoothScrollTo(x, y, 0); + } + + /** + * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) + */ + void smoothScrollTo(int x, int y, int velocity) { + if (getChildCount() == 0) { + // Nothing to do. + setScrollingCacheEnabled(false); + return; + } + int sx = getScrollX(); + int sy = getScrollY(); + int dx = x - sx; + int dy = y - sy; + if (dx == 0 && dy == 0) { + completeScroll(false); + populate(); + setScrollState(SCROLL_STATE_IDLE); + return; + } + + setScrollingCacheEnabled(true); + setScrollState(SCROLL_STATE_SETTLING); + + final int width = getClientWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); + final float distance = halfWidth + halfWidth * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration = 0; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float pageWidth = width * mAdapter.getPageWidth(mCurItem); + final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); + duration = (int) ((pageDelta + 1) * 100); + } + duration = Math.min(duration, MAX_SETTLE_DURATION); + + mScroller.startScroll(sx, sy, dx, dy, duration); + postInvalidateOnAnimation(); + } + + ItemInfo addNewItem(int position, int index) { + ItemInfo ii = new ItemInfo(); + ii.position = position; + ii.object = mAdapter.instantiateItem(this, position); + ii.widthFactor = mAdapter.getPageWidth(position); + if (index < 0 || index >= mItems.size()) { + mItems.add(ii); + } else { + mItems.add(index, ii); + } + return ii; + } + + void dataSetChanged() { + // This method only gets called if our observer is attached, so mAdapter is non-null. + + final int adapterCount = mAdapter.getCount(); + mExpectedAdapterCount = adapterCount; + boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && + mItems.size() < adapterCount; + int newCurrItem = mCurItem; + + boolean isUpdating = false; + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + final int newPos = mAdapter.getItemPosition(ii.object); + + if (newPos == PagerAdapter.POSITION_UNCHANGED) { + continue; + } + + if (newPos == PagerAdapter.POSITION_NONE) { + mItems.remove(i); + i--; + + if (!isUpdating) { + mAdapter.startUpdate(this); + isUpdating = true; + } + + mAdapter.destroyItem(this, ii.position, ii.object); + needPopulate = true; + + if (mCurItem == ii.position) { + // Keep the current item in the valid range + newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); + needPopulate = true; + } + continue; + } + + if (ii.position != newPos) { + if (ii.position == mCurItem) { + // Our current item changed position. Follow it. + newCurrItem = newPos; + } + + ii.position = newPos; + needPopulate = true; + } + } + + if (isUpdating) { + mAdapter.finishUpdate(this); + } + + Collections.sort(mItems, COMPARATOR); + + if (needPopulate) { + // Reset our known page widths; populate will recompute them. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + lp.widthFactor = 0.f; + } + } + + setCurrentItemInternal(newCurrItem, false, true); + requestLayout(); + } + } + + void populate() { + populate(mCurItem); + } + + void populate(int newCurrentItem) { + ItemInfo oldCurInfo = null; + int focusDirection = View.FOCUS_FORWARD; + if (mCurItem != newCurrentItem) { + focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; + oldCurInfo = infoForPosition(mCurItem); + mCurItem = newCurrentItem; + } + + if (mAdapter == null) { + sortChildDrawingOrder(); + return; + } + + // Bail now if we are waiting to populate. This is to hold off + // on creating views from the time the user releases their finger to + // fling to a new position until we have finished the scroll to + // that position, avoiding glitches from happening at that point. + if (mPopulatePending) { + if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); + sortChildDrawingOrder(); + return; + } + + // Also, don't populate until we are attached to a window. This is to + // avoid trying to populate before we have restored our view hierarchy + // state and conflicting with what is restored. + if (getWindowToken() == null) { + return; + } + + mAdapter.startUpdate(this); + + final int pageLimit = mOffscreenPageLimit; + final int startPos = Math.max(0, mCurItem - pageLimit); + final int N = mAdapter.getCount(); + final int endPos = Math.min(N-1, mCurItem + pageLimit); + + if (N != mExpectedAdapterCount) { + String resName; + try { + resName = getResources().getResourceName(getId()); + } catch (Resources.NotFoundException e) { + resName = Integer.toHexString(getId()); + } + throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + + " contents without calling PagerAdapter#notifyDataSetChanged!" + + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + + " Pager id: " + resName + + " Pager class: " + getClass() + + " Problematic adapter: " + mAdapter.getClass()); + } + + // Locate the currently focused item or add it if needed. + int curIndex = -1; + ItemInfo curItem = null; + for (curIndex = 0; curIndex < mItems.size(); curIndex++) { + final ItemInfo ii = mItems.get(curIndex); + if (ii.position >= mCurItem) { + if (ii.position == mCurItem) curItem = ii; + break; + } + } + + if (curItem == null && N > 0) { + curItem = addNewItem(mCurItem, curIndex); + } + + // Fill 3x the available width or up to the number of offscreen + // pages requested to either side, whichever is larger. + // If we have no current item we have no work to do. + if (curItem != null) { + float extraWidthLeft = 0.f; + int itemIndex = curIndex - 1; + ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + final int clientWidth = getClientWidth(); + final float leftWidthNeeded = clientWidth <= 0 ? 0 : + 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; + for (int pos = mCurItem - 1; pos >= 0; pos--) { + if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + itemIndex--; + curIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthLeft += ii.widthFactor; + itemIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex + 1); + extraWidthLeft += ii.widthFactor; + curIndex++; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } + + float extraWidthRight = curItem.widthFactor; + itemIndex = curIndex + 1; + if (extraWidthRight < 2.f) { + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + final float rightWidthNeeded = clientWidth <= 0 ? 0 : + (float) getPaddingRight() / (float) clientWidth + 2.f; + for (int pos = mCurItem + 1; pos < N; pos++) { + if (extraWidthRight >= rightWidthNeeded && pos > endPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthRight += ii.widthFactor; + itemIndex++; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex); + itemIndex++; + extraWidthRight += ii.widthFactor; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } + } + + calculatePageOffsets(curItem, curIndex, oldCurInfo); + } + + if (DEBUG) { + Log.i(TAG, "Current page list:"); + for (int i=0; i<mItems.size(); i++) { + Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); + } + } + + mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); + + mAdapter.finishUpdate(this); + + // Check width measurement of current pages and drawing sort order. + // Update LayoutParams as needed. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.childIndex = i; + if (!lp.isDecor && lp.widthFactor == 0.f) { + // 0 means requery the adapter for this, it doesn't have a valid width. + final ItemInfo ii = infoForChild(child); + if (ii != null) { + lp.widthFactor = ii.widthFactor; + lp.position = ii.position; + } + } + } + sortChildDrawingOrder(); + + if (hasFocus()) { + View currentFocused = findFocus(); + ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; + if (ii == null || ii.position != mCurItem) { + for (int i=0; i<getChildCount(); i++) { + View child = getChildAt(i); + ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(focusDirection)) { + break; + } + } + } + } + } + } + + private void sortChildDrawingOrder() { + if (mDrawingOrder != DRAW_ORDER_DEFAULT) { + if (mDrawingOrderedChildren == null) { + mDrawingOrderedChildren = new ArrayList<View>(); + } else { + mDrawingOrderedChildren.clear(); + } + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + mDrawingOrderedChildren.add(child); + } + Collections.sort(mDrawingOrderedChildren, sPositionComparator); + } + } + + private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { + final int N = mAdapter.getCount(); + final int width = getClientWidth(); + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + // Fix up offsets for later layout. + if (oldCurInfo != null) { + final int oldCurPosition = oldCurInfo.position; + // Base offsets off of oldCurInfo. + if (oldCurPosition < curItem.position) { + int itemIndex = 0; + ItemInfo ii = null; + float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; + for (int pos = oldCurPosition + 1; + pos <= curItem.position && itemIndex < mItems.size(); pos++) { + ii = mItems.get(itemIndex); + while (pos > ii.position && itemIndex < mItems.size() - 1) { + itemIndex++; + ii = mItems.get(itemIndex); + } + while (pos < ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset += mAdapter.getPageWidth(pos) + marginOffset; + pos++; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + } else if (oldCurPosition > curItem.position) { + int itemIndex = mItems.size() - 1; + ItemInfo ii = null; + float offset = oldCurInfo.offset; + for (int pos = oldCurPosition - 1; + pos >= curItem.position && itemIndex >= 0; pos--) { + ii = mItems.get(itemIndex); + while (pos < ii.position && itemIndex > 0) { + itemIndex--; + ii = mItems.get(itemIndex); + } + while (pos > ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset -= mAdapter.getPageWidth(pos) + marginOffset; + pos--; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + } + } + } + + // Base all offsets off of curItem. + final int itemCount = mItems.size(); + float offset = curItem.offset; + int pos = curItem.position - 1; + mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; + mLastOffset = curItem.position == N - 1 ? + curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; + // Previous pages + for (int i = curIndex - 1; i >= 0; i--, pos--) { + final ItemInfo ii = mItems.get(i); + while (pos > ii.position) { + offset -= mAdapter.getPageWidth(pos--) + marginOffset; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + if (ii.position == 0) mFirstOffset = offset; + } + offset = curItem.offset + curItem.widthFactor + marginOffset; + pos = curItem.position + 1; + // Next pages + for (int i = curIndex + 1; i < itemCount; i++, pos++) { + final ItemInfo ii = mItems.get(i); + while (pos < ii.position) { + offset += mAdapter.getPageWidth(pos++) + marginOffset; + } + if (ii.position == N - 1) { + mLastOffset = offset + ii.widthFactor - 1; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + + mNeedCalculatePageOffsets = false; + } + + /** + * This is the persistent state that is saved by ViewPager. Only needed + * if you are creating a sublass of ViewPager that must save its own + * state, in which case it should implement a subclass of this which + * contains that state. + */ + public static class SavedState extends BaseSavedState { + int position; + Parcelable adapterState; + ClassLoader loader; + + public SavedState(Parcel source) { + super(source); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(position); + out.writeParcelable(adapterState, flags); + } + + @Override + public String toString() { + return "FragmentPager.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " position=" + position + "}"; + } + + public static final Creator<SavedState> CREATOR = new Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + + SavedState(Parcel in, ClassLoader loader) { + super(in); + if (loader == null) { + loader = getClass().getClassLoader(); + } + position = in.readInt(); + adapterState = in.readParcelable(loader); + this.loader = loader; + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.position = mCurItem; + if (mAdapter != null) { + ss.adapterState = mAdapter.saveState(); + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState)state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mAdapter != null) { + mAdapter.restoreState(ss.adapterState, ss.loader); + setCurrentItemInternal(ss.position, false, true); + } else { + mRestoredCurItem = ss.position; + mRestoredAdapterState = ss.adapterState; + mRestoredClassLoader = ss.loader; + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + final LayoutParams lp = (LayoutParams) params; + lp.isDecor |= child instanceof Decor; + if (mInLayout) { + if (lp != null && lp.isDecor) { + throw new IllegalStateException("Cannot add pager decor view during layout"); + } + lp.needsMeasure = true; + addViewInLayout(child, index, params); + } else { + super.addView(child, index, params); + } + + if (USE_CACHE) { + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(mScrollingCacheEnabled); + } else { + child.setDrawingCacheEnabled(false); + } + } + } + + @Override + public void removeView(View view) { + if (mInLayout) { + removeViewInLayout(view); + } else { + super.removeView(view); + } + } + + ItemInfo infoForChild(View child) { + for (int i=0; i<mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (mAdapter.isViewFromObject(child, ii.object)) { + return ii; + } + } + return null; + } + + ItemInfo infoForAnyChild(View child) { + ViewParent parent; + while ((parent=child.getParent()) != this) { + if (parent == null || !(parent instanceof View)) { + return null; + } + child = (View)parent; + } + return infoForChild(child); + } + + ItemInfo infoForPosition(int position) { + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.position == position) { + return ii; + } + } + return null; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // For simple implementation, our internal size is always 0. + // We depend on the container to specify the layout size of + // our view. We can't really know what it is since we will be + // adding and removing different arbitrary views and do not + // want the layout to change as this happens. + setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), + getDefaultSize(0, heightMeasureSpec)); + + final int measuredWidth = getMeasuredWidth(); + final int maxGutterSize = measuredWidth / 10; + mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); + + // Children are just made to fill our space. + int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight(); + int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); + + /* + * Make sure all children have been properly measured. Decor views first. + * Right now we cheat and make this less complicated by assuming decor + * views won't intersect. We will pin to edges based on gravity. + */ + int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp != null && lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + int widthMode = MeasureSpec.AT_MOST; + int heightMode = MeasureSpec.AT_MOST; + boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; + boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT; + + if (consumeVertical) { + widthMode = MeasureSpec.EXACTLY; + } else if (consumeHorizontal) { + heightMode = MeasureSpec.EXACTLY; + } + + int widthSize = childWidthSize; + int heightSize = childHeightSize; + if (lp.width != LayoutParams.WRAP_CONTENT) { + widthMode = MeasureSpec.EXACTLY; + if (lp.width != LayoutParams.FILL_PARENT) { + widthSize = lp.width; + } + } + if (lp.height != LayoutParams.WRAP_CONTENT) { + heightMode = MeasureSpec.EXACTLY; + if (lp.height != LayoutParams.FILL_PARENT) { + heightSize = lp.height; + } + } + final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); + final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); + child.measure(widthSpec, heightSpec); + + if (consumeVertical) { + childHeightSize -= child.getMeasuredHeight(); + } else if (consumeHorizontal) { + childWidthSize -= child.getMeasuredWidth(); + } + } + } + } + + mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); + mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); + + // Make sure we have created all fragments that we need to have shown. + mInLayout = true; + populate(); + mInLayout = false; + + // Page views next. + size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child + + ": " + mChildWidthMeasureSpec); + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp == null || !lp.isDecor) { + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); + child.measure(widthSpec, mChildHeightMeasureSpec); + } + } + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + // Make sure scroll position is set correctly. + if (w != oldw) { + recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); + } + } + + private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) { + if (oldWidth > 0 && !mItems.isEmpty()) { + final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; + final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() + + oldMargin; + final int xpos = getScrollX(); + final float pageOffset = (float) xpos / oldWidthWithMargin; + final int newOffsetPixels = (int) (pageOffset * widthWithMargin); + + scrollTo(newOffsetPixels, getScrollY()); + if (!mScroller.isFinished()) { + // We now return to your regularly scheduled scroll, already in progress. + final int newDuration = mScroller.getDuration() - mScroller.timePassed(); + ItemInfo targetInfo = infoForPosition(mCurItem); + mScroller.startScroll(newOffsetPixels, 0, + (int) (targetInfo.offset * width), 0, newDuration); + } + } else { + final ItemInfo ii = infoForPosition(mCurItem); + final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; + final int scrollPos = (int) (scrollOffset * + (width - getPaddingLeft() - getPaddingRight())); + if (scrollPos != getScrollX()) { + completeScroll(false); + scrollTo(scrollPos, getScrollY()); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + int width = r - l; + int height = b - t; + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + final int scrollX = getScrollX(); + + int decorCount = 0; + + // First pass - decor views. We need to do this in two passes so that + // we have the proper offsets for non-decor views later. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + int childLeft = 0; + int childTop = 0; + if (lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getMeasuredWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + switch (vgrav) { + default: + childTop = paddingTop; + break; + case Gravity.TOP: + childTop = paddingTop; + paddingTop += child.getMeasuredHeight(); + break; + case Gravity.CENTER_VERTICAL: + childTop = Math.max((height - child.getMeasuredHeight()) / 2, + paddingTop); + break; + case Gravity.BOTTOM: + childTop = height - paddingBottom - child.getMeasuredHeight(); + paddingBottom += child.getMeasuredHeight(); + break; + } + childLeft += scrollX; + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + decorCount++; + } + } + } + + final int childWidth = width - paddingLeft - paddingRight; + // Page views. Do this once we have the right padding offsets from above. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + ItemInfo ii; + if (!lp.isDecor && (ii = infoForChild(child)) != null) { + int loff = (int) (childWidth * ii.offset); + int childLeft = paddingLeft + loff; + int childTop = paddingTop; + if (lp.needsMeasure) { + // This was added during layout and needs measurement. + // Do it now that we know what we're working with. + lp.needsMeasure = false; + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidth * lp.widthFactor), + MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec( + (int) (height - paddingTop - paddingBottom), + MeasureSpec.EXACTLY); + child.measure(widthSpec, heightSpec); + } + if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object + + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() + + "x" + child.getMeasuredHeight()); + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + } + } + } + mTopPageBounds = paddingTop; + mBottomPageBounds = height - paddingBottom; + mDecorChildCount = decorCount; + + if (mFirstLayout) { + scrollToItem(mCurItem, false, 0, false); + } + mFirstLayout = false; + } + + @Override + public void computeScroll() { + if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (!pageScrolled(x)) { + mScroller.abortAnimation(); + scrollTo(0, y); + } + } + + // Keep on drawing until the animation has finished. + postInvalidateOnAnimation(); + return; + } + + // Done with scroll, clean up state. + completeScroll(true); + } + + private boolean pageScrolled(int xpos) { + if (mItems.size() == 0) { + mCalledSuper = false; + onPageScrolled(0, 0, 0); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return false; + } + final ItemInfo ii = infoForCurrentScrollPosition(); + final int width = getClientWidth(); + final int widthWithMargin = width + mPageMargin; + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) xpos / width) - ii.offset) / + (ii.widthFactor + marginOffset); + final int offsetPixels = (int) (pageOffset * widthWithMargin); + + mCalledSuper = false; + onPageScrolled(currentPage, pageOffset, offsetPixels); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return true; + } + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * If you override this method you must call through to the superclass implementation + * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled + * returns. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param offset Value from [0, 1) indicating the offset from the page at position. + * @param offsetPixels Value in pixels indicating the offset from position. + */ + protected void onPageScrolled(int position, float offset, int offsetPixels) { + // Offset any decor views if needed - keep them on-screen at all times. + if (mDecorChildCount > 0) { + final int scrollX = getScrollX(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + final int width = getWidth(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) continue; + + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int childLeft = 0; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + childLeft += scrollX; + + final int childOffset = childLeft - child.getLeft(); + if (childOffset != 0) { + child.offsetLeftAndRight(childOffset); + } + } + } + + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + + if (mPageTransformer != null) { + final int scrollX = getScrollX(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.isDecor) continue; + + final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); + mPageTransformer.transformPage(child, transformPos); + } + } + + mCalledSuper = true; + } + + private void completeScroll(boolean postEvents) { + boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; + if (needPopulate) { + // Done with scroll, no longer want to cache view drawing. + setScrollingCacheEnabled(false); + mScroller.abortAnimation(); + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (oldX != x || oldY != y) { + scrollTo(x, y); + } + } + mPopulatePending = false; + for (int i=0; i<mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.scrolling) { + needPopulate = true; + ii.scrolling = false; + } + } + if (needPopulate) { + if (postEvents) { + postOnAnimation(mEndScrollRunnable); + } else { + mEndScrollRunnable.run(); + } + } + } + + private boolean isGutterDrag(float x, float dx) { + return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0); + } + + private void enableLayers(boolean enable) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final int layerType = enable ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE; + getChildAt(i).setLayerType(layerType, null); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + final int action = ev.getAction() & MotionEvent.ACTION_MASK; + + // Always take care of the touch gesture being complete. + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + // Release the drag. + if (DEBUG) Log.v(TAG, "Intercept done!"); + mIsBeingDragged = false; + mIsUnableToDrag = false; + mActivePointerId = INVALID_POINTER; + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + return false; + } + + // Nothing more to do here if we have decided whether or not we + // are dragging. + if (action != MotionEvent.ACTION_DOWN) { + if (mIsBeingDragged) { + if (DEBUG) Log.v(TAG, "Intercept returning true!"); + return true; + } + if (mIsUnableToDrag) { + if (DEBUG) Log.v(TAG, "Intercept returning false!"); + return false; + } + } + + switch (action) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = ev.findPointerIndex(activePointerId); + final float x = ev.getX(pointerIndex); + final float dx = x - mLastMotionX; + final float xDiff = Math.abs(dx); + final float y = ev.getY(pointerIndex); + final float yDiff = Math.abs(y - mInitialMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + + if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && + canScroll(this, false, (int) dx, (int) x, (int) y)) { + // Nested view has scrollable area under this point. Let it be handled there. + mLastMotionX = x; + mLastMotionY = y; + mIsUnableToDrag = true; + return false; + } + if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollingCacheEnabled(true); + } else if (yDiff > mTouchSlop) { + // The finger has moved enough in the vertical + // direction to be counted as a drag... abort + // any attempt to drag horizontally, to work correctly + // with children that have scrolling containers. + if (DEBUG) Log.v(TAG, "Starting unable to drag!"); + mIsUnableToDrag = true; + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + if (performDrag(x)) { + postInvalidateOnAnimation(); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + mIsUnableToDrag = false; + + mScroller.computeScrollOffset(); + if (mScrollState == SCROLL_STATE_SETTLING && + Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { + // Let the user 'catch' the pager as it animates. + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } else { + completeScroll(false); + mIsBeingDragged = false; + } + + if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + + " mIsBeingDragged=" + mIsBeingDragged + + "mIsUnableToDrag=" + mIsUnableToDrag); + break; + } + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mFakeDragging) { + // A fake drag is in progress already, ignore this real one + // but still eat the touch events. + // (It is likely that the user is multi-touching the screen.) + return true; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (mAdapter == null || mAdapter.getCount() == 0) { + // Nothing to present or scroll; nothing to touch. + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + boolean needsInvalidate = false; + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + + // Remember where the motion event started + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + break; + } + case MotionEvent.ACTION_MOVE: + if (!mIsBeingDragged) { + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(pointerIndex); + final float xDiff = Math.abs(x - mLastMotionX); + final float y = ev.getY(pointerIndex); + final float yDiff = Math.abs(y - mLastMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + if (xDiff > mTouchSlop && xDiff > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollState(SCROLL_STATE_DRAGGING); + setScrollingCacheEnabled(true); + + // Disallow Parent Intercept, just in case + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + } + // Not else! Note that mIsBeingDragged can be set above. + if (mIsBeingDragged) { + // Scroll to follow the motion event + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(activePointerIndex); + needsInvalidate |= performDrag(x); + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int activePointerIndex = + ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(activePointerIndex); + final int totalDelta = (int) (x - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + + mActivePointerId = INVALID_POINTER; + endDrag(); + mLeftEdge.onRelease(); + mRightEdge.onRelease(); + needsInvalidate = true; + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged) { + scrollToItem(mCurItem, true, 0, false); + mActivePointerId = INVALID_POINTER; + endDrag(); + mLeftEdge.onRelease(); + mRightEdge.onRelease(); + needsInvalidate = true; + } + break; + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + final float x = ev.getX(index); + mLastMotionX = x; + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)); + break; + } + if (needsInvalidate) { + postInvalidateOnAnimation(); + } + return true; + } + + private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + private boolean performDrag(float x) { + boolean needsInvalidate = false; + + final float deltaX = mLastMotionX - x; + mLastMotionX = x; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX + deltaX; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + boolean leftAbsolute = true; + boolean rightAbsolute = true; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftAbsolute = false; + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightAbsolute = false; + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + if (leftAbsolute) { + float over = leftBound - scrollX; + mLeftEdge.onPull(Math.abs(over) / width); + needsInvalidate = true; + } + scrollX = leftBound; + } else if (scrollX > rightBound) { + if (rightAbsolute) { + float over = scrollX - rightBound; + mRightEdge.onPull(Math.abs(over) / width); + needsInvalidate = true; + } + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + return needsInvalidate; + } + + /** + * @return Info about the page at the current scroll position. + * This can be synthetic for a missing middle page; the 'object' field can be null. + */ + private ItemInfo infoForCurrentScrollPosition() { + final int width = getClientWidth(); + final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + int lastPos = -1; + float lastOffset = 0.f; + float lastWidth = 0.f; + boolean first = true; + + ItemInfo lastItem = null; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + float offset; + if (!first && ii.position != lastPos + 1) { + // Create a synthetic item for a missing page. + ii = mTempItem; + ii.offset = lastOffset + lastWidth + marginOffset; + ii.position = lastPos + 1; + ii.widthFactor = mAdapter.getPageWidth(ii.position); + i--; + } + offset = ii.offset; + + final float leftBound = offset; + final float rightBound = offset + ii.widthFactor + marginOffset; + if (first || scrollOffset >= leftBound) { + if (scrollOffset < rightBound || i == mItems.size() - 1) { + return ii; + } + } else { + return lastItem; + } + first = false; + lastPos = ii.position; + lastOffset = offset; + lastWidth = ii.widthFactor; + lastItem = ii; + } + + return lastItem; + } + + private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { + int targetPage; + if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { + targetPage = velocity > 0 ? currentPage : currentPage + 1; + } else { + final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; + targetPage = (int) (currentPage + pageOffset + truncator); + } + + if (mItems.size() > 0) { + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + + // Only let the user target pages we have items for + targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); + } + + return targetPage; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + boolean needsInvalidate = false; + + final int overScrollMode = getOverScrollMode(); + if (overScrollMode == View.OVER_SCROLL_ALWAYS || + (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && + mAdapter != null && mAdapter.getCount() > 1)) { + if (!mLeftEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + final int width = getWidth(); + + canvas.rotate(270); + canvas.translate(-height + getPaddingTop(), mFirstOffset * width); + mLeftEdge.setSize(height, width); + needsInvalidate |= mLeftEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mRightEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.rotate(90); + canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); + mRightEdge.setSize(height, width); + needsInvalidate |= mRightEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + } else { + mLeftEdge.finish(); + mRightEdge.finish(); + } + + if (needsInvalidate) { + // Keep animating + postInvalidateOnAnimation(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the margin drawable between pages if needed. + if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { + final int scrollX = getScrollX(); + final int width = getWidth(); + + final float marginOffset = (float) mPageMargin / width; + int itemIndex = 0; + ItemInfo ii = mItems.get(0); + float offset = ii.offset; + final int itemCount = mItems.size(); + final int firstPos = ii.position; + final int lastPos = mItems.get(itemCount - 1).position; + for (int pos = firstPos; pos < lastPos; pos++) { + while (pos > ii.position && itemIndex < itemCount) { + ii = mItems.get(++itemIndex); + } + + float drawAt; + if (pos == ii.position) { + drawAt = (ii.offset + ii.widthFactor) * width; + offset = ii.offset + ii.widthFactor + marginOffset; + } else { + float widthFactor = mAdapter.getPageWidth(pos); + drawAt = (offset + widthFactor) * width; + offset += widthFactor + marginOffset; + } + + if (drawAt + mPageMargin > scrollX) { + mMarginDrawable.setBounds((int) drawAt, mTopPageBounds, + (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds); + mMarginDrawable.draw(canvas); + } + + if (drawAt > scrollX + width) { + break; // No more visible, no sense in continuing + } + } + } + } + + /** + * Start a fake drag of the pager. + * + * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager + * with the touch scrolling of another view, while still letting the ViewPager + * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) + * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call + * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. + * + * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag + * is already in progress, this method will return false. + * + * @return true if the fake drag began successfully, false if it could not be started. + * + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean beginFakeDrag() { + if (mIsBeingDragged) { + return false; + } + mFakeDragging = true; + setScrollState(SCROLL_STATE_DRAGGING); + mInitialMotionX = mLastMotionX = 0; + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + mFakeDragBeginTime = time; + return true; + } + + /** + * End a fake drag of the pager. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + */ + public void endFakeDrag() { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int totalDelta = (int) (mLastMotionX - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + endDrag(); + + mFakeDragging = false; + } + + /** + * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. + * + * @param xOffset Offset in pixels to drag by. + * @see #beginFakeDrag() + * @see #endFakeDrag() + */ + public void fakeDragBy(float xOffset) { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + mLastMotionX += xOffset; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX - xOffset; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + scrollX = leftBound; + } else if (scrollX > rightBound) { + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + // Synthesize an event for the VelocityTracker. + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, + mLastMotionX, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + } + + /** + * Returns true if a fake drag is in progress. + * + * @return true if currently in a fake drag, false otherwise. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean isFakeDragging() { + return mFakeDragging; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = ev.getActionIndex(); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = ev.getX(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + mIsUnableToDrag = false; + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void setScrollingCacheEnabled(boolean enabled) { + if (mScrollingCacheEnabled != enabled) { + mScrollingCacheEnabled = enabled; + if (USE_CACHE) { + final int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(enabled); + } + } + } + } + } + + public boolean canScrollHorizontally(int direction) { + if (mAdapter == null) { + return false; + } + + final int width = getClientWidth(); + final int scrollX = getScrollX(); + if (direction < 0) { + return (scrollX > (int) (width * mFirstOffset)); + } else if (direction > 0) { + return (scrollX < (int) (width * mLastOffset)); + } else { + return false; + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && v.canScrollHorizontally(-dx); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_LEFT: + handled = arrowScroll(FOCUS_LEFT); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled = arrowScroll(FOCUS_RIGHT); + break; + case KeyEvent.KEYCODE_TAB: + if (event.hasNoModifiers()) { + handled = arrowScroll(FOCUS_FORWARD); + } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { + handled = arrowScroll(FOCUS_BACKWARD); + } + break; + } + } + return handled; + } + + public boolean arrowScroll(int direction) { + View currentFocused = findFocus(); + if (currentFocused == this) { + currentFocused = null; + } else if (currentFocused != null) { + boolean isChild = false; + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + if (parent == this) { + isChild = true; + break; + } + } + if (!isChild) { + // This would cause the focus search down below to fail in fun ways. + final StringBuilder sb = new StringBuilder(); + sb.append(currentFocused.getClass().getSimpleName()); + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + sb.append(" => ").append(parent.getClass().getSimpleName()); + } + Log.e(TAG, "arrowScroll tried to find focus based on non-child " + + "current focused view " + sb.toString()); + currentFocused = null; + } + } + + boolean handled = false; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, + direction); + if (nextFocused != null && nextFocused != currentFocused) { + if (direction == View.FOCUS_LEFT) { + // If there is nothing to the left, or this is causing us to + // jump to the right, then what we really want to do is page left. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft >= currLeft) { + handled = pageLeft(); + } else { + handled = nextFocused.requestFocus(); + } + } else if (direction == View.FOCUS_RIGHT) { + // If there is nothing to the right, or this is causing us to + // jump to the left, then what we really want to do is page right. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft <= currLeft) { + handled = pageRight(); + } else { + handled = nextFocused.requestFocus(); + } + } + } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { + // Trying to move left and nothing there; try to page. + handled = pageLeft(); + } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { + // Trying to move right and nothing there; try to page. + handled = pageRight(); + } + if (handled) { + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + return handled; + } + + private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { + if (outRect == null) { + outRect = new Rect(); + } + if (child == null) { + outRect.set(0, 0, 0, 0); + return outRect; + } + outRect.left = child.getLeft(); + outRect.right = child.getRight(); + outRect.top = child.getTop(); + outRect.bottom = child.getBottom(); + + ViewParent parent = child.getParent(); + while (parent instanceof ViewGroup && parent != this) { + final ViewGroup group = (ViewGroup) parent; + outRect.left += group.getLeft(); + outRect.right += group.getRight(); + outRect.top += group.getTop(); + outRect.bottom += group.getBottom(); + + parent = group.getParent(); + } + return outRect; + } + + boolean pageLeft() { + if (mCurItem > 0) { + setCurrentItem(mCurItem-1, true); + return true; + } + return false; + } + + boolean pageRight() { + if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) { + setCurrentItem(mCurItem+1, true); + return true; + } + return false; + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + final int focusableCount = views.size(); + + final int descendantFocusability = getDescendantFocusability(); + + if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addFocusables(views, direction, focusableMode); + } + } + } + } + + // we add ourselves (if focusable) in all cases except for when we are + // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is + // to avoid the focus search finding layouts when a more precise search + // among the focusable children would be more interesting. + if ( + descendantFocusability != FOCUS_AFTER_DESCENDANTS || + // No focusable descendants + (focusableCount == views.size())) { + // Note that we can't call the superclass here, because it will + // add all views in. So we need to do the same thing View does. + if (!isFocusable()) { + return; + } + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && + isInTouchMode() && !isFocusableInTouchMode()) { + return; + } + if (views != null) { + views.add(this); + } + } + } + + /** + * We only want the current page that is being shown to be touchable. + */ + @Override + public void addTouchables(ArrayList<View> views) { + // Note that we don't call super.addTouchables(), which means that + // we don't call View.addTouchables(). This is okay because a ViewPager + // is itself not touchable. + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addTouchables(views); + } + } + } + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + int index; + int increment; + int end; + int count = getChildCount(); + if ((direction & FOCUS_FORWARD) != 0) { + index = 0; + increment = 1; + end = count; + } else { + index = count - 1; + increment = -1; + end = -1; + } + for (int i = index; i != end; i += increment) { + View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + } + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // Dispatch scroll events from this ViewPager. + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + return super.dispatchPopulateAccessibilityEvent(event); + } + + // Dispatch all other accessibility events from the current page. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + final ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem && + child.dispatchPopulateAccessibilityEvent(event)) { + return true; + } + } + } + + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return generateDefaultLayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + class MyAccessibilityDelegate extends AccessibilityDelegate { + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(ViewPager.class.getName()); + final AccessibilityRecord record = AccessibilityRecord.obtain(); + record.setScrollable(canScroll()); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED + && mAdapter != null) { + record.setItemCount(mAdapter.getCount()); + record.setFromIndex(mCurItem); + record.setToIndex(mCurItem); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(ViewPager.class.getName()); + info.setScrollable(canScroll()); + if (canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + } + if (canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { + if (canScrollHorizontally(1)) { + setCurrentItem(mCurItem + 1); + return true; + } + } return false; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { + if (canScrollHorizontally(-1)) { + setCurrentItem(mCurItem - 1); + return true; + } + } return false; + } + return false; + } + + private boolean canScroll() { + return (mAdapter != null) && (mAdapter.getCount() > 1); + } + } + + private class PagerObserver extends DataSetObserver { + @Override + public void onChanged() { + dataSetChanged(); + } + @Override + public void onInvalidated() { + dataSetChanged(); + } + } + + /** + * Layout parameters that should be supplied for views added to a + * ViewPager. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * true if this view is a decoration on the pager itself and not + * a view supplied by the adapter. + */ + public boolean isDecor; + + /** + * Gravity setting for use on decor views only: + * Where to position the view page within the overall ViewPager + * container; constants are defined in {@link android.view.Gravity}. + */ + public int gravity; + + /** + * Width as a 0-1 multiplier of the measured pager width + */ + float widthFactor = 0.f; + + /** + * true if this view was added during layout and needs to be measured + * before being positioned. + */ + boolean needsMeasure; + + /** + * Adapter position this view is for if !isDecor + */ + int position; + + /** + * Current child index within the ViewPager that this view occupies + */ + int childIndex; + + public LayoutParams() { + super(FILL_PARENT, FILL_PARENT); + } + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + gravity = a.getInteger(0, Gravity.TOP); + a.recycle(); + } + } + + static class ViewPositionComparator implements Comparator<View> { + @Override + public int compare(View lhs, View rhs) { + final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); + final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); + if (llp.isDecor != rlp.isDecor) { + return llp.isDecor ? 1 : -1; + } + return llp.position - rlp.position; + } + } +} diff --git a/core/jni/android_view_Surface.cpp b/core/jni/android_view_Surface.cpp index bfa0534f0eb5..39064ed22604 100644 --- a/core/jni/android_view_Surface.cpp +++ b/core/jni/android_view_Surface.cpp @@ -130,6 +130,100 @@ jobject android_view_Surface_createFromIGraphicBufferProducer(JNIEnv* env, return surfaceObj; } +int android_view_Surface_mapPublicFormatToHalFormat(PublicFormat f) { + + switch(f) { + case PublicFormat::JPEG: + case PublicFormat::DEPTH_POINT_CLOUD: + return HAL_PIXEL_FORMAT_BLOB; + case PublicFormat::DEPTH16: + return HAL_PIXEL_FORMAT_Y16; + case PublicFormat::RAW_SENSOR: + return HAL_PIXEL_FORMAT_RAW16; + default: + // Most formats map 1:1 + return static_cast<int>(f); + } +} + +android_dataspace android_view_Surface_mapPublicFormatToHalDataspace( + PublicFormat f) { + switch(f) { + case PublicFormat::JPEG: + return HAL_DATASPACE_JFIF; + case PublicFormat::DEPTH_POINT_CLOUD: + case PublicFormat::DEPTH16: + return HAL_DATASPACE_DEPTH; + case PublicFormat::RAW_SENSOR: + case PublicFormat::RAW10: + return HAL_DATASPACE_ARBITRARY; + case PublicFormat::YUV_420_888: + case PublicFormat::NV21: + case PublicFormat::YV12: + return HAL_DATASPACE_JFIF; + default: + // Most formats map to UNKNOWN + return HAL_DATASPACE_UNKNOWN; + } +} + +PublicFormat android_view_Surface_mapHalFormatDataspaceToPublicFormat( + int format, android_dataspace dataSpace) { + switch(format) { + case HAL_PIXEL_FORMAT_RGBA_8888: + case HAL_PIXEL_FORMAT_RGBX_8888: + case HAL_PIXEL_FORMAT_RGB_888: + case HAL_PIXEL_FORMAT_RGB_565: + case HAL_PIXEL_FORMAT_Y8: + case HAL_PIXEL_FORMAT_RAW10: + case HAL_PIXEL_FORMAT_YCbCr_420_888: + case HAL_PIXEL_FORMAT_YV12: + // Enums overlap in both name and value + return static_cast<PublicFormat>(format); + case HAL_PIXEL_FORMAT_RAW16: + // Name differs, though value is the same + return PublicFormat::RAW_SENSOR; + case HAL_PIXEL_FORMAT_YCbCr_422_SP: + // Name differs, though the value is the same + return PublicFormat::NV16; + case HAL_PIXEL_FORMAT_YCrCb_420_SP: + // Name differs, though the value is the same + return PublicFormat::NV21; + case HAL_PIXEL_FORMAT_YCbCr_422_I: + // Name differs, though the value is the same + return PublicFormat::YUY2; + case HAL_PIXEL_FORMAT_Y16: + // Dataspace-dependent + switch (dataSpace) { + case HAL_DATASPACE_DEPTH: + return PublicFormat::DEPTH16; + default: + // Assume non-depth Y16 is just Y16. + return PublicFormat::Y16; + } + break; + case HAL_PIXEL_FORMAT_BLOB: + // Dataspace-dependent + switch (dataSpace) { + case HAL_DATASPACE_DEPTH: + return PublicFormat::DEPTH_POINT_CLOUD; + case HAL_DATASPACE_JFIF: + return PublicFormat::JPEG; + default: + // Assume otherwise-marked blobs are also JPEG + return PublicFormat::JPEG; + } + break; + case HAL_PIXEL_FORMAT_BGRA_8888: + case HAL_PIXEL_FORMAT_RAW_OPAQUE: + case HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED: + // Not defined in public API + return PublicFormat::UNKNOWN; + + default: + return PublicFormat::UNKNOWN; + } +} // ---------------------------------------------------------------------------- static inline bool isSurfaceValid(const sp<Surface>& sur) { diff --git a/docs/html/images/training/geofence.png b/docs/html/images/training/geofence.png Binary files differnew file mode 100644 index 000000000000..2d5d3aa0b176 --- /dev/null +++ b/docs/html/images/training/geofence.png diff --git a/docs/html/images/training/geofence@2x.png b/docs/html/images/training/geofence@2x.png Binary files differnew file mode 100644 index 000000000000..2f83105a29d7 --- /dev/null +++ b/docs/html/images/training/geofence@2x.png diff --git a/docs/html/training/location/geofencing.jd b/docs/html/training/location/geofencing.jd index 748b6ece8b87..59fc4c665a53 100644 --- a/docs/html/training/location/geofencing.jd +++ b/docs/html/training/location/geofencing.jd @@ -9,9 +9,11 @@ trainingnavtop=true <h2>This lesson teaches you to</h2> <ol> - <li><a href="#RequestGeofences">Request Geofence Monitoring</a></li> + <li><a href="#RequestGeofences">Set up for Geofence Monitoring</a></li> + <li><a href="#CreateAdd">Create and Add Geofences</a></li> <li><a href="#HandleGeofenceTransitions">Handle Geofence Transitions</a></li> <li><a href="#StopGeofenceMonitoring">Stop Geofence Monitoring</a></li> + </ol> <h2>You should also read</h2> @@ -23,577 +25,148 @@ trainingnavtop=true <h2>Try it out</h2> -<div class="download-box"> - <a href="http://developer.android.com/shareables/training/GeofenceDetection.zip" class="button">Download the sample</a> - <p class="filename">GeofenceDetection.zip</p> -</div> + <ul> + <li> + <a href="https://github.com/googlesamples/android-play-location/tree/master/Geofencing" + class="external-link">Geofencing</a> + </li> + </ul> </div> </div> <p> - Geofencing combines awareness of the user's current location with awareness of nearby - features, defined as the user's proximity to locations that may be of interest. To mark a + Geofencing combines awareness of the user's current location with awareness of the user's + proximity to locations that may be of interest. To mark a location of interest, you specify its latitude and longitude. To adjust the proximity for the - location, you add a radius. The latitude, longitude, and radius define a geofence. - You can have multiple active geofences at one time. + location, you add a radius. The latitude, longitude, and radius define a geofence, creating a + circular area, or fence, around the location of interest. </p> <p> - Location Services treats a geofences as an area rather than as a points and proximity. This - allows it to detect when the user enters or exits a geofence. For each geofence, you can ask - Location Services to send you entrance events or exit events or both. You can also limit the - duration of a geofence by specifying an expiration duration in milliseconds. After the geofence - expires, Location Services automatically removes it. + You can have multiple active geofences, with a limit of 100 per device user. For each geofence, + you can ask Location Services to send you entrance and exit events, or you can specify a + duration within the geofence area to wait, or <em>dwell</em>, before triggering an event. You + can limit the duration of any geofence by specifying an expiration duration in milliseconds. + After the geofence expires, Location Services automatically removes it. </p> -<!-- - Send geofences to Location Services - --> -<h2 id="RequestGeofences">Request Geofence Monitoring</h2> + +<img src="{@docRoot}images/training/geofence@2x.png" +srcset="{@docRoot}images/training/geofence.png 1x, {@docRoot}images/training/geofence@2x.png 2x" alt="" + width="400" height="400"/> +<p> + This lesson shows you how to add and remove geofences, and then listen for geofence transitions + using an {@link android.app.IntentService}.</p> + +<h2 id="RequestGeofences">Set up for Geofence Monitoring</h2> <p> The first step in requesting geofence monitoring is to request the necessary permission. To use geofencing, your app must request {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION}. To request this permission, add the following element as a child element of the <code><a href="{@docRoot}guide/topics/manifest/manifest-element.html"><manifest></a></code> - element: + element in your app manifest: </p> <pre> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> </pre> -<!-- Check for Google Play services --> -<h3>Check for Google Play Services</h3> -<p> - Location Services is part of the Google Play services APK. Since it's hard to anticipate the - state of the user's device, you should always check that the APK is installed before you attempt - to connect to Location Services. To check that the APK is installed, call -<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesUtil.html#isGooglePlayServicesAvailable(android.content.Context)">GooglePlayServicesUtil.isGooglePlayServicesAvailable()</a></code>, - which returns one of the - integer result codes listed in the API reference documentation. If you encounter an error, - call -<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesUtil.html#getErrorDialog(int, android.app.Activity, int)">GooglePlayServicesUtil.getErrorDialog()</a></code> - to retrieve localized dialog that prompts users to take the correct action, then display - the dialog in a {@link android.support.v4.app.DialogFragment}. The dialog may allow the - user to correct the problem, in which case Google Play services may send a result back to your - activity. To handle this result, override the method - {@link android.support.v4.app.FragmentActivity#onActivityResult onActivityResult()} -</p> -<p class="note"> - <strong>Note:</strong> To make your app compatible with - platform version 1.6 and later, the activity that displays the - {@link android.support.v4.app.DialogFragment} must subclass - {@link android.support.v4.app.FragmentActivity} instead of {@link android.app.Activity}. Using - {@link android.support.v4.app.FragmentActivity} also allows you to call - {@link android.support.v4.app.FragmentActivity#getSupportFragmentManager - getSupportFragmentManager()} to display the {@link android.support.v4.app.DialogFragment}. -</p> <p> - Since you usually need to check for Google Play services in more than one place in your code, - define a method that encapsulates the check, then call the method before each connection - attempt. The following snippet contains all of the code required to check for Google - Play services: + If you want to use an {@link android.app.IntentService} to listen for geofence transitions, + add an element specifying the service name. This element must be + a child of the <code><a href="{@docRoot}guide/topics/manifest/application-element.html"> + <application></a></code> element: </p> -<pre> -public class MainActivity extends FragmentActivity { - ... - // Global constants - /* - * Define a request code to send to Google Play services - * This code is returned in Activity.onActivityResult - */ - private final static int - CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000; - ... - // Define a DialogFragment that displays the error dialog - public static class ErrorDialogFragment extends DialogFragment { - // Global field to contain the error dialog - private Dialog mDialog; - ... - // Default constructor. Sets the dialog field to null - public ErrorDialogFragment() { - super(); - mDialog = null; - } - ... - // Set the dialog to display - public void setDialog(Dialog dialog) { - mDialog = dialog; - } - ... - // Return a Dialog to the DialogFragment. - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - return mDialog; - } - ... - } - ... - /* - * Handle results returned to the FragmentActivity - * by Google Play services - */ - @Override - protected void onActivityResult( - int requestCode, int resultCode, Intent data) { - // Decide what to do based on the original request code - switch (requestCode) { - ... - case CONNECTION_FAILURE_RESOLUTION_REQUEST : - /* - * If the result code is Activity.RESULT_OK, try - * to connect again - */ - switch (resultCode) { - ... - case Activity.RESULT_OK : - /* - * Try the request again - */ - ... - break; - } - ... - } - ... - } - ... - private boolean servicesConnected() { - // Check that Google Play services is available - int resultCode = - GooglePlayServicesUtil. - isGooglePlayServicesAvailable(this); - // If Google Play services is available - if (ConnectionResult.SUCCESS == resultCode) { - // In debug mode, log the status - Log.d("Geofence Detection", - "Google Play services is available."); - // Continue - return true; - // Google Play services was not available for some reason - } else { - // Get the error code - int errorCode = connectionResult.getErrorCode(); - // Get the error dialog from Google Play services - Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog( - errorCode, - this, - CONNECTION_FAILURE_RESOLUTION_REQUEST); - // If Google Play services can provide an error dialog - if (errorDialog != null) { - // Create a new DialogFragment for the error dialog - ErrorDialogFragment errorFragment = - new ErrorDialogFragment(); - // Set the dialog in the DialogFragment - errorFragment.setDialog(errorDialog); - // Show the error dialog in the DialogFragment - errorFragment.show( - getSupportFragmentManager(), - "Geofence Detection"); - } - } - } - ... -} +<pre> +<application + android:allowBackup="true"> + ... + <service android:name=".GeofenceTransitionsIntentService"/> +<application/> </pre> -<p> - Snippets in the following sections call this method to verify that Google Play services is - available. -</p> -<p> - To use geofencing, start by defining the geofences you want to monitor. Although you usually - store geofence data in a local database or download it from the network, you need to send - a geofence to Location Services as an instance of -<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code>, - which you create with -<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.Builder.html">Geofence.Builder</a></code>. - Each -<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code> - object contains the following information: -</p> -<dl> - <dt>Latitude, longitude, and radius</dt> - <dd> - Define a circular area for the geofence. Use the latitude and longitude to mark a location - of interest, and then use the radius to adjust how close the user needs to approach the - location before the geofence is detected. The larger the radius, the more likely the - user will trigger a geofence transition alert by approaching the geofence. For example, - providing a large radius for a geofencing app that turns on lights in the user's house as - the user returns home might cause the lights to go on even if the user is simply passing by. - </dd> - <dt>Expiration time</dt> - <dd> - How long the geofence should remain active. Once the expiration time is reached, Location - Services deletes the geofence. Most of the time, you should specify an expiration time, but - you may want to keep permanent geofences for the user's home or place of work. - </dd> - <dt>Transition type</dt> - <dd> - Location Services can detect when the user steps within the radius of the geofence ("entry") - and when the user steps outside the radius of the geofence ("exit"), or both. - </dd> - <dt>Geofence ID</dt> - <dd> - A string that is stored with the geofence. You should make this unique, so that you can - use it to remove a geofence from Location Services tracking. - </dd> -</dl> -<h3>Define geofence storage</h3> -<p> - A geofencing app needs to read and write geofence data to persistent storage. You shouldn't use -<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code> - objects to do this; instead, use storage techniques such as databases that can store groups of - related data. + +<p>To access the location APIs, you need to create an instance of the + Google Play services API client. To learn how to connect your client, see + <a href="{@docRoot}training/location/retrieve-current.html#play-services">Connect + to Google Play Services</a>.</p> + +<h2 id="CreateAdd">Create and Add Geofences</h2> + +<p>Your app needs to create and add geofences using the location API's builder class for + creating Geofence objects, and the convenience class for adding them. Also, to handle the + intents sent from Location Services when geofence transitions occur, you can define a + {@link android.app.PendingIntent} as shown in this section. </p> + +<h3>Create geofence objects</h3> + <p> - As an example of storing geofence data, the following snippet defines two classes that use - the app's {@link android.content.SharedPreferences} instance for persistent storage. The class - {@code SimpleGeofence}, analogous to a database record, stores the - data for a single -<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code> - object in a "flattened" form. The class {@code SimpleGeofenceStore}, analogous to a database, - reads and writes {@code SimpleGeofence} data to the - {@link android.content.SharedPreferences} instance. -</p> -<pre> -public class MainActivity extends FragmentActivity { - ... - /** - * A single Geofence object, defined by its center and radius. - */ - public class SimpleGeofence { - // Instance variables - private final String mId; - private final double mLatitude; - private final double mLongitude; - private final float mRadius; - private long mExpirationDuration; - private int mTransitionType; + First, use <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.Builder. + html">Geofence.Builder</a></code> to create a geofence, setting the desired radius, duration, and + transition types for the geofence. For example, to populate a list object named + {@code mGeofenceList}: + </p> - /** - * @param geofenceId The Geofence's request ID - * @param latitude Latitude of the Geofence's center. - * @param longitude Longitude of the Geofence's center. - * @param radius Radius of the geofence circle. - * @param expiration Geofence expiration duration - * @param transition Type of Geofence transition. - */ - public SimpleGeofence( - String geofenceId, - double latitude, - double longitude, - float radius, - long expiration, - int transition) { - // Set the instance fields from the constructor - this.mId = geofenceId; - this.mLatitude = latitude; - this.mLongitude = longitude; - this.mRadius = radius; - this.mExpirationDuration = expiration; - this.mTransitionType = transition; - } - // Instance field getters - public String getId() { - return mId; - } - public double getLatitude() { - return mLatitude; - } - public double getLongitude() { - return mLongitude; - } - public float getRadius() { - return mRadius; - } - public long getExpirationDuration() { - return mExpirationDuration; - } - public int getTransitionType() { - return mTransitionType; - } - /** - * Creates a Location Services Geofence object from a - * SimpleGeofence. - * - * @return A Geofence object - */ - public Geofence toGeofence() { - // Build a new Geofence object - return new Geofence.Builder() - .setRequestId(getId()) - .setTransitionTypes(mTransitionType) - .setCircularRegion( - getLatitude(), getLongitude(), getRadius()) - .setExpirationDuration(mExpirationDuration) - .build(); - } - } - ... - /** - * Storage for geofence values, implemented in SharedPreferences. - */ - public class SimpleGeofenceStore { - // Keys for flattened geofences stored in SharedPreferences - public static final String KEY_LATITUDE = - "com.example.android.geofence.KEY_LATITUDE"; - public static final String KEY_LONGITUDE = - "com.example.android.geofence.KEY_LONGITUDE"; - public static final String KEY_RADIUS = - "com.example.android.geofence.KEY_RADIUS"; - public static final String KEY_EXPIRATION_DURATION = - "com.example.android.geofence.KEY_EXPIRATION_DURATION"; - public static final String KEY_TRANSITION_TYPE = - "com.example.android.geofence.KEY_TRANSITION_TYPE"; - // The prefix for flattened geofence keys - public static final String KEY_PREFIX = - "com.example.android.geofence.KEY"; - /* - * Invalid values, used to test geofence storage when - * retrieving geofences - */ - public static final long INVALID_LONG_VALUE = -999l; - public static final float INVALID_FLOAT_VALUE = -999.0f; - public static final int INVALID_INT_VALUE = -999; - // The SharedPreferences object in which geofences are stored - private final SharedPreferences mPrefs; - // The name of the SharedPreferences - private static final String SHARED_PREFERENCES = - "SharedPreferences"; - // Create the SharedPreferences storage with private access only - public SimpleGeofenceStore(Context context) { - mPrefs = - context.getSharedPreferences( - SHARED_PREFERENCES, - Context.MODE_PRIVATE); - } - /** - * Returns a stored geofence by its id, or returns {@code null} - * if it's not found. - * - * @param id The ID of a stored geofence - * @return A geofence defined by its center and radius. See - */ - public SimpleGeofence getGeofence(String id) { - /* - * Get the latitude for the geofence identified by id, or - * INVALID_FLOAT_VALUE if it doesn't exist - */ - double lat = mPrefs.getFloat( - getGeofenceFieldKey(id, KEY_LATITUDE), - INVALID_FLOAT_VALUE); - /* - * Get the longitude for the geofence identified by id, or - * INVALID_FLOAT_VALUE if it doesn't exist - */ - double lng = mPrefs.getFloat( - getGeofenceFieldKey(id, KEY_LONGITUDE), - INVALID_FLOAT_VALUE); - /* - * Get the radius for the geofence identified by id, or - * INVALID_FLOAT_VALUE if it doesn't exist - */ - float radius = mPrefs.getFloat( - getGeofenceFieldKey(id, KEY_RADIUS), - INVALID_FLOAT_VALUE); - /* - * Get the expiration duration for the geofence identified - * by id, or INVALID_LONG_VALUE if it doesn't exist - */ - long expirationDuration = mPrefs.getLong( - getGeofenceFieldKey(id, KEY_EXPIRATION_DURATION), - INVALID_LONG_VALUE); - /* - * Get the transition type for the geofence identified by - * id, or INVALID_INT_VALUE if it doesn't exist - */ - int transitionType = mPrefs.getInt( - getGeofenceFieldKey(id, KEY_TRANSITION_TYPE), - INVALID_INT_VALUE); - // If none of the values is incorrect, return the object - if ( - lat != GeofenceUtils.INVALID_FLOAT_VALUE && - lng != GeofenceUtils.INVALID_FLOAT_VALUE && - radius != GeofenceUtils.INVALID_FLOAT_VALUE && - expirationDuration != - GeofenceUtils.INVALID_LONG_VALUE && - transitionType != GeofenceUtils.INVALID_INT_VALUE) { +<pre> +mGeofenceList.add(new Geofence.Builder() + // Set the request ID of the geofence. This is a string to identify this + // geofence. + .setRequestId(entry.getKey()) - // Return a true Geofence object - return new SimpleGeofence( - id, lat, lng, radius, expirationDuration, - transitionType); - // Otherwise, return null. - } else { - return null; - } - } - /** - * Save a geofence. - * @param geofence The SimpleGeofence containing the - * values you want to save in SharedPreferences - */ - public void setGeofence(String id, SimpleGeofence geofence) { - /* - * Get a SharedPreferences editor instance. Among other - * things, SharedPreferences ensures that updates are atomic - * and non-concurrent - */ - Editor editor = mPrefs.edit(); - // Write the Geofence values to SharedPreferences - editor.putFloat( - getGeofenceFieldKey(id, KEY_LATITUDE), - (float) geofence.getLatitude()); - editor.putFloat( - getGeofenceFieldKey(id, KEY_LONGITUDE), - (float) geofence.getLongitude()); - editor.putFloat( - getGeofenceFieldKey(id, KEY_RADIUS), - geofence.getRadius()); - editor.putLong( - getGeofenceFieldKey(id, KEY_EXPIRATION_DURATION), - geofence.getExpirationDuration()); - editor.putInt( - getGeofenceFieldKey(id, KEY_TRANSITION_TYPE), - geofence.getTransitionType()); - // Commit the changes - editor.commit(); - } - public void clearGeofence(String id) { - /* - * Remove a flattened geofence object from storage by - * removing all of its keys - */ - Editor editor = mPrefs.edit(); - editor.remove(getGeofenceFieldKey(id, KEY_LATITUDE)); - editor.remove(getGeofenceFieldKey(id, KEY_LONGITUDE)); - editor.remove(getGeofenceFieldKey(id, KEY_RADIUS)); - editor.remove(getGeofenceFieldKey(id, - KEY_EXPIRATION_DURATION)); - editor.remove(getGeofenceFieldKey(id, KEY_TRANSITION_TYPE)); - editor.commit(); - } - /** - * Given a Geofence object's ID and the name of a field - * (for example, KEY_LATITUDE), return the key name of the - * object's values in SharedPreferences. - * - * @param id The ID of a Geofence object - * @param fieldName The field represented by the key - * @return The full key name of a value in SharedPreferences - */ - private String getGeofenceFieldKey(String id, - String fieldName) { - return KEY_PREFIX + "_" + id + "_" + fieldName; - } - } - ... -} + .setCircularRegion( + entry.getValue().latitude, + entry.getValue().longitude, + Constants.GEOFENCE_RADIUS_IN_METERS + ) + .setExpirationDuration(Constants.GEOFENCE_EXPIRATION_IN_MILLISECONDS) + .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | + Geofence.GEOFENCE_TRANSITION_EXIT) + .build()); </pre> -<h3>Create Geofence objects</h3> + +<p>This example pulls data from a constants file. In actual practice, apps might + dynamically create geofences based on the user's location.</p> + +<h3>Specify geofences and initial triggers</h3> + <p> - The following snippet uses the {@code SimpleGeofence} and {@code SimpleGeofenceStore} classes - gets geofence data from the UI, stores it in {@code SimpleGeofence} objects, stores these - objects in a {@code SimpleGeofenceStore} object, and then creates -<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code> - objects: + The following snippet uses the <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingRequest.html"> + GeofencingRequest</a></code> class + and its nested <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingRequest.Builder.html"> + GeofencingRequestBuilder</a></code> class to + specify the geofences to monitor and to set how related geofence events are triggered: </p> <pre> -public class MainActivity extends FragmentActivity { - ... - /* - * Use to set an expiration time for a geofence. After this amount - * of time Location Services will stop tracking the geofence. - */ - private static final long SECONDS_PER_HOUR = 60; - private static final long MILLISECONDS_PER_SECOND = 1000; - private static final long GEOFENCE_EXPIRATION_IN_HOURS = 12; - private static final long GEOFENCE_EXPIRATION_TIME = - GEOFENCE_EXPIRATION_IN_HOURS * - SECONDS_PER_HOUR * - MILLISECONDS_PER_SECOND; - ... - /* - * Handles to UI views containing geofence data - */ - // Handle to geofence 1 latitude in the UI - private EditText mLatitude1; - // Handle to geofence 1 longitude in the UI - private EditText mLongitude1; - // Handle to geofence 1 radius in the UI - private EditText mRadius1; - // Handle to geofence 2 latitude in the UI - private EditText mLatitude2; - // Handle to geofence 2 longitude in the UI - private EditText mLongitude2; - // Handle to geofence 2 radius in the UI - private EditText mRadius2; - /* - * Internal geofence objects for geofence 1 and 2 - */ - private SimpleGeofence mUIGeofence1; - private SimpleGeofence mUIGeofence2; - ... - // Internal List of Geofence objects - List<Geofence> mGeofenceList; - // Persistent storage for geofences - private SimpleGeofenceStore mGeofenceStorage; - ... - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ... - // Instantiate a new geofence storage area - mGeofenceStorage = new SimpleGeofenceStore(this); - - // Instantiate the current List of geofences - mCurrentGeofences = new ArrayList<Geofence>(); - } - ... - /** - * Get the geofence parameters for each geofence from the UI - * and add them to a List. - */ - public void createGeofences() { - /* - * Create an internal object to store the data. Set its - * ID to "1". This is a "flattened" object that contains - * a set of strings - */ - mUIGeofence1 = new SimpleGeofence( - "1", - Double.valueOf(mLatitude1.getText().toString()), - Double.valueOf(mLongitude1.getText().toString()), - Float.valueOf(mRadius1.getText().toString()), - GEOFENCE_EXPIRATION_TIME, - // This geofence records only entry transitions - Geofence.GEOFENCE_TRANSITION_ENTER); - // Store this flat version - mGeofenceStorage.setGeofence("1", mUIGeofence1); - // Create another internal object. Set its ID to "2" - mUIGeofence2 = new SimpleGeofence( - "2", - Double.valueOf(mLatitude2.getText().toString()), - Double.valueOf(mLongitude2.getText().toString()), - Float.valueOf(mRadius2.getText().toString()), - GEOFENCE_EXPIRATION_TIME, - // This geofence records both entry and exit transitions - Geofence.GEOFENCE_TRANSITION_ENTER | - Geofence.GEOFENCE_TRANSITION_EXIT); - // Store this flat version - mGeofenceStorage.setGeofence(2, mUIGeofence2); - mGeofenceList.add(mUIGeofence1.toGeofence()); - mGeofenceList.add(mUIGeofence2.toGeofence()); - } - ... +private GeofencingRequest getGeofencingRequest() { + GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); + builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER); + builder.addGeofences(mGeofenceList); + return builder.build(); } </pre> + <p> - In addition to the {@link java.util.List} of -<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code> - objects you want to monitor, you need to provide Location Services with the - {@link android.content.Intent} that it sends to your app when it detects geofence - transitions. -<h4>Define a Intent for geofence transitions</h4> + This example shows the use of two geofence triggers. The <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html#GEOFENCE_TRANSITION_ENTER"> + GEOFENCE_TRANSITION_ENTER</a></code> + transition triggers when a device enters a geofence, and the <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html#GEOFENCE_TRANSITION_EXIT"> + GEOFENCE_TRANSITION_EXIT</a></code> + transition triggers when a device exits a geofence. Specifying + <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingRequest.html#INITIAL_TRIGGER_ENTER"> + INITIAL_TRIGGER_ENTER</a></code> tells Location services that + <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html#GEOFENCE_TRANSITION_ENTER"> + GEOFENCE_TRANSITION_ENTER</a></code> + should be triggered if the the device is already inside the geofence.</p> +</p> + +<p>In many cases, it may be preferable to use instead <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingRequest.html#INITIAL_TRIGGER_DWELL"> + INITIAL_TRIGGER_DWELL</a></code>, + which triggers events only when the user stops for a defined duration within a geofence. + This approach can help reduce "alert spam" resulting from large numbers notifications when a + device briefly enters and exits geofences. Another strategy for getting best results from your + geofences is to set a minimum radius of 100 meters. This helps account for the location accuracy + of typical WiFi networks, and also helps reduce device power consumption. +</p> + +<h3>Define an Intent for geofence transitions</h3> <p> The {@link android.content.Intent} sent from Location Services can trigger various actions in your app, but you should <i>not</i> have it start an activity or fragment, because components @@ -601,807 +174,133 @@ public class MainActivity extends FragmentActivity { {@link android.app.IntentService} is a good way to handle the intent. An {@link android.app.IntentService} can post a notification, do long-running background work, send intents to other services, or send a broadcast intent. The following snippet shows how - how to define a {@link android.app.PendingIntent} that starts an - {@link android.app.IntentService}: + to define a {@link android.app.PendingIntent} that starts an {@link android.app.IntentService}: </p> <pre> public class MainActivity extends FragmentActivity { ... - /* - * Create a PendingIntent that triggers an IntentService in your - * app when a geofence transition occurs. - */ - private PendingIntent getTransitionPendingIntent() { - // Create an explicit Intent - Intent intent = new Intent(this, - ReceiveTransitionsIntentService.class); - /* - * Return the PendingIntent - */ - return PendingIntent.getService( - this, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - ... -} -</pre> -<p> - Now you have all the code you need to send a request to monitor geofences to Location - Services. -</p> -<!-- Send the monitoring request --> -<h3 id="requestmonitoring">Send the monitoring request</h3> -<p> - Sending the monitoring request requires two asynchronous operations. The first operation gets a - location client for the request, and the second makes the request using the client. In both - cases, Location Services invokes a callback method when it finishes the operation. The best way - to handle these operations is to chain together the method calls. The following snippets - demonstrate how to set up an activity, define the methods, and call them in the proper order. -</p> -<p> - First, modify the activity's class definition to implement the necessary callback interfaces. - Add the following interfaces: -</p> -<dl> - <dt> -<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html">ConnectionCallbacks</a></code> - </dt> - <dd> - Specifies methods that Location Services calls when a location client is connected or - disconnected. - </dd> - <dt> -<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.OnConnectionFailedListener.html">OnConnectionFailedListener</a></code> - </dt> - <dd> - Specifies a method that Location Services calls if an error occurs while attempting to - connect the location client. - </dd> - <dt> -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnAddGeofencesResultListener.html">OnAddGeofencesResultListener</a></code> - </dt> - <dd> - Specifies a method that Location Services calls once it has added the geofences. - </dd> -</dl> -<p> - For example: -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... -} -</pre> -<h4>Start the request process</h4> -<p> - Next, define a method that starts the request process by connecting to Location Services. - Mark this as a request to add a geofence by setting a global variable. This allows you to - use the callback -<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html#onConnected(android.os.Bundle)">ConnectionCallbacks.onConnected()</a></code> - to add geofences and to remove them, as described in succeeding sections. -</p> -<p> -<p> - To guard against race conditions that might arise if your app tries to start another request - before the first one finishes, define a boolean flag that tracks the state of the current - request: -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - // Holds the location client - private LocationClient mLocationClient; - // Stores the PendingIntent used to request geofence monitoring - private PendingIntent mGeofenceRequestIntent; - // Defines the allowable request types. - public enum REQUEST_TYPE = {ADD} - private REQUEST_TYPE mRequestType; - // Flag that indicates if a request is underway. - private boolean mInProgress; - ... - @Override - protected void onCreate(Bundle savedInstanceState) { - ... - // Start with the request flag set to false - mInProgress = false; - ... - } - ... - /** - * Start a request for geofence monitoring by calling - * LocationClient.connect(). - */ - public void addGeofences() { - // Start a request to add geofences - mRequestType = ADD; - /* - * Test for Google Play services after setting the request type. - * If Google Play services isn't present, the proper request - * can be restarted. - */ - if (!servicesConnected()) { - return; - } - /* - * Create a new location client object. Since the current - * activity class implements ConnectionCallbacks and - * OnConnectionFailedListener, pass the current activity object - * as the listener for both parameters - */ - mLocationClient = new LocationClient(this, this, this) - // If a request is not already underway - if (!mInProgress) { - // Indicate that a request is underway - mInProgress = true; - // Request a connection from the client to Location Services - mLocationClient.connect(); - } else { - /* - * A request is already underway. You can handle - * this situation by disconnecting the client, - * re-setting the flag, and then re-trying the - * request. - */ - } - } - ... -} -</pre> -<h4>Send a request to add the geofences</h4> -<p> - In your implementation of -<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html#onConnected(android.os.Bundle)">ConnectionCallbacks.onConnected()</a></code>, - call -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#addGeofences(java.util.List<com.google.android.gms.location.Geofence>, android.app.PendingIntent, com.google.android.gms.location.LocationClient.OnAddGeofencesResultListener)">LocationClient.addGeofences()</a></code>. - Notice that if the connection fails, -<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html#onConnected(android.os.Bundle)">onConnected()</a></code> - isn't called, and the request stops. -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - /* - * Provide the implementation of ConnectionCallbacks.onConnected() - * Once the connection is available, send a request to add the - * Geofences - */ - @Override - private void onConnected(Bundle dataBundle) { - ... - switch (mRequestType) { - case ADD : - // Get the PendingIntent for the request - mTransitionPendingIntent = - getTransitionPendingIntent(); - // Send a request to add the current geofences - mLocationClient.addGeofences( - mCurrentGeofences, pendingIntent, this); - ... + private PendingIntent getGeofencePendingIntent() { + // Reuse the PendingIntent if we already have it. + if (mGeofencePendingIntent != null) { + return mGeofencePendingIntent; } + Intent intent = new Intent(this, GeofenceTransitionsIntentService.class); + // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when + // calling addGeofences() and removeGeofences(). + return PendingIntent.getService(this, 0, intent, PendingIntent. + FLAG_UPDATE_CURRENT); } - ... -} </pre> + +<h3>Add geofences</h3> + <p> - Notice that -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#addGeofences(java.util.List<com.google.android.gms.location.Geofence>, android.app.PendingIntent, com.google.android.gms.location.LocationClient.OnAddGeofencesResultListener)">addGeofences()</a></code> - returns immediately, but the status of the request is indeterminate until Location Services - calls -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnAddGeofencesResultListener.html#onAddGeofencesResult(int, java.lang.String[])">onAddGeofencesResult()</a></code> - Once this method is called, you can determine if the request was successful or not. -</p> -<h4>Check the result returned by Location Services</h4> -<p> - When Location Services invokes your implementation of the callback method -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnAddGeofencesResultListener.html#onAddGeofencesResult(int, java.lang.String[])">onAddGeofencesResult()</a></code>, - indicating that the request is complete, examine the incoming status code. If the request - was successful, the geofences you requested are active. If the request was unsuccessful, - the geofences aren't active, and you need to re-try the request or report an error. For example: -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - /* - * Provide the implementation of - * OnAddGeofencesResultListener.onAddGeofencesResult. - * Handle the result of adding the geofences - * - */ - @Override - public void onAddGeofencesResult( - int statusCode, String[] geofenceRequestIds) { - // If adding the geofences was successful - if (LocationStatusCodes.SUCCESS == statusCode) { - /* - * Handle successful addition of geofences here. - * You can send out a broadcast intent or update the UI. - * geofences into the Intent's extended data. - */ - } else { - // If adding the geofences failed - /* - * Report errors here. - * You can log the error using Log.e() or update - * the UI. - */ - } - // Turn off the in progress flag and disconnect the client - mInProgress = false; - mLocationClient.disconnect(); - } - ... -} -</pre> -<!-- Handle disconnections --> -<h3>Handle disconnections</h3> -<p> - In some cases, Location Services may disconnect from the activity recognition client before - you call -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#disconnect()">disconnect()</a></code>. - To handle this situation, implement <code> -<a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html#onDisconnected()">onDisconnected()</a></code>. - In this method, set the request flag to indicate that a request is not in progress, and - delete the client: -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - /* - * Implement ConnectionCallbacks.onDisconnected() - * Called by Location Services once the location client is - * disconnected. - */ - @Override - public void onDisconnected() { - // Turn off the request flag - mInProgress = false; - // Destroy the current location client - mLocationClient = null; - } - ... -} -</pre> -<!-- Handle connection errors --> -<h3>Handle connection errors</h3> -<p> - Besides handling the normal callbacks from Location Services, you have to provide a callback - method that Location Services calls if a connection error occurs. This callback method - can re-use the {@link android.support.v4.app.DialogFragment} class that you defined to - handle the check for Google Play services. It can also re-use the override you defined - for {@link android.support.v4.app.FragmentActivity#onActivityResult onActivityResult()} that - receives any Google Play services results that occur when the user interacts with the - error dialog. The following snippet shows you a sample implementation of the callback method: + To add geofences, use the <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingApi.html#addGeofences(com.google.android.gms.common.api.GoogleApiClient, com.google.android.gms.location.GeofencingRequest, android.app.PendingIntent)">{@code GeoencingApi.addGeofences()}</a></code> method. + Provide the Google API client, the <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingRequest"> + GeofencingRequest</a></code> object, and the {@link android.app.PendingIntent}. + The following snippet, which processes the results in <code><a href="{@docRoot}reference/com/google/android/gms/common/api/ResultCallback.html#onResult(R)"> + onResult()</a></code>, assumes that the main activity implements <code><a href="{@docRoot}reference/com/google/android/gms/common/api/ResultCallback.html"> + ResultCallback</a></code>: </p> <pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - // Implementation of OnConnectionFailedListener.onConnectionFailed - @Override - public void onConnectionFailed(ConnectionResult connectionResult) { - // Turn off the request flag - mInProgress = false; - /* - * If the error has a resolution, start a Google Play services - * activity to resolve it. - */ - if (connectionResult.hasResolution()) { - try { - connectionResult.startResolutionForResult( - this, - CONNECTION_FAILURE_RESOLUTION_REQUEST); - } catch (SendIntentException e) { - // Log the error - e.printStackTrace(); - } - // If no resolution is available, display an error dialog - } else { - // Get the error code - int errorCode = connectionResult.getErrorCode(); - // Get the error dialog from Google Play services - Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog( - errorCode, - this, - CONNECTION_FAILURE_RESOLUTION_REQUEST); - // If Google Play services can provide an error dialog - if (errorDialog != null) { - // Create a new DialogFragment for the error dialog - ErrorDialogFragment errorFragment = - new ErrorDialogFragment(); - // Set the dialog in the DialogFragment - errorFragment.setDialog(errorDialog); - // Show the error dialog in the DialogFragment - errorFragment.show( - getSupportFragmentManager(), - "Geofence Detection"); - } - } - } +public class MainActivity extends FragmentActivity { ... -} + LocationServices.GeofencingApi.addGeofences( + mGoogleApiClient, + getGeofencingRequest(), + getGeofencePendingIntent() + ).setResultCallback(this); </pre> -<!-- - Handle Geofence Transitions - --> + + <h2 id="HandleGeofenceTransitions">Handle Geofence Transitions</h2> <p> When Location Services detects that the user has entered or exited a geofence, it sends out the {@link android.content.Intent} contained in the {@link android.app.PendingIntent} - you included in the request to add geofences. This {@link android.content.Intent} is + you included in the request to add geofences. This {@link android.content.Intent} is received + by a service like <code>GeofenceTransitionsIntentService</code>, + which obtains the geofencing event from the intent, determines the type of Geofence transition(s), + and determines which of the defined geofences was triggered. It then sends a notification as + the output. </p> -<h3>Define an IntentService</h3> <p> The following snippet shows how to define an {@link android.app.IntentService} that posts a notification when a geofence transition occurs. When the user clicks the notification, the app's main activity appears: </p> <pre> -public class ReceiveTransitionsIntentService extends IntentService { - ... - /** - * Sets an identifier for the service - */ - public ReceiveTransitionsIntentService() { - super("ReceiveTransitionsIntentService"); - } - /** - * Handles incoming intents - *@param intent The Intent sent by Location Services. This - * Intent is provided - * to Location Services (inside a PendingIntent) when you call - * addGeofences() - */ - @Override +public class GeofenceTransitionsIntentService extends IntentService { + ... protected void onHandleIntent(Intent intent) { - // First check for errors - if (LocationClient.hasError(intent)) { - // Get the error code with a static method - int errorCode = LocationClient.getErrorCode(intent); - // Log the error - Log.e("ReceiveTransitionsIntentService", - "Location Services error: " + - Integer.toString(errorCode)); - /* - * You can also send the error code to an Activity or - * Fragment with a broadcast Intent - */ - /* - * If there's no error, get the transition type and the IDs - * of the geofence or geofences that triggered the transition - */ - } else { - // Get the type of transition (entry or exit) - int transitionType = - LocationClient.getGeofenceTransition(intent); - // Test that a valid transition was reported - if ( - (transitionType == Geofence.GEOFENCE_TRANSITION_ENTER) - || - (transitionType == Geofence.GEOFENCE_TRANSITION_EXIT) - ) { - List <Geofence> triggerList = - getTriggeringGeofences(intent); + GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); + if (geofencingEvent.hasError()) { + String errorMessage = GeofenceErrorMessages.getErrorString(this, + geofencingEvent.getErrorCode()); + Log.e(TAG, errorMessage); + return; + } - String[] triggerIds = new String[geofenceList.size()]; + // Get the transition type. + int geofenceTransition = geofencingEvent.getGeofenceTransition(); + + // Test that the reported transition was of interest. + if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER || + geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { + + // Get the geofences that were triggered. A single event can trigger + // multiple geofences. + List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences(); + + // Get the transition details as a String. + String geofenceTransitionDetails = getGeofenceTransitionDetails( + this, + geofenceTransition, + triggeringGeofences + ); - for (int i = 0; i < triggerIds.length; i++) { - // Store the Id of each geofence - triggerIds[i] = triggerList.get(i).getRequestId(); - } - /* - * At this point, you can store the IDs for further use - * display them, or display the details associated with - * them. - */ - } - // An invalid transition was reported + // Send notification and log the transition details. + sendNotification(geofenceTransitionDetails); + Log.i(TAG, geofenceTransitionDetails); } else { - Log.e("ReceiveTransitionsIntentService", - "Geofence transition error: " + - Integer.toString()transitionType)); + // Log the error. + Log.e(TAG, getString(R.string.geofence_transition_invalid_type, + geofenceTransition)); } } - ... -} -</pre> -<!-- Specify the IntentService in the manifest --> -<h3>Specify the IntentService in the manifest</h3> -<p> - To identify the {@link android.app.IntentService} to the system, add a - <code><a href="{@docRoot}guide/topics/manifest/service-element.html"><service></a></code> - element to the app manifest. For example: -</p> -<pre> -<service - android:name="com.example.android.location.ReceiveTransitionsIntentService" - android:label="@string/app_name" - android:exported="false"> -</service> </pre> -<p> - Notice that you don't have to specify intent filters for the service, because it only receives - explicit intents. How the incoming geofence transition intents are created is described in the - section <a href="#requestmonitoring">Send the monitoring request</a>. -</p> + +<p>After detecting the transition event via the {@link android.app.PendingIntent}, + this {@link android.app.IntentService} gets the geofence transition type and tests whether + it is one of the events the app uses to trigger notifications -- either + <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html#GEOFENCE_TRANSITION_ENTER">GEOFENCE_TRANSITION_ENTER</a></code> + or <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html#GEOFENCE_TRANSITION_EXIT">GEOFENCE_TRANSITION_EXIT</a></code> + in this case. The service then sends a notification and logs the transition details.</p> <!-- Remove Geofences --> <h2 id="StopGeofenceMonitoring">Stop Geofence Monitoring</h2> -<p> - To stop geofence monitoring, you remove the geofences themselves. You can remove a specific - set of geofences or all the geofences associated with a {@link android.app.PendingIntent}. The - procedure is similar to adding geofences. The first operation gets a location - client for the removal request, and the second makes the request using the client. -</p> -<p> - The callback methods that Location Services invokes when it has finished removing geofences - are defined in the interface -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnRemoveGeofencesResultListener.html">LocationClient.OnRemoveGeofencesResultListener</a></code>. Declare - this interface as part of your class definition, and then add definitions for its two methods: -</p> -<dl> - <dt> -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnRemoveGeofencesResultListener.html#onRemoveGeofencesByPendingIntentResult(int, android.app.PendingIntent)">onRemoveGeofencesByPendingIntentResult()</a></code> - </dt> - <dd> - Callback invoked when Location Services finishes a request to remove all geofences made - by the method -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#removeGeofences(android.app.PendingIntent, com.google.android.gms.location.LocationClient.OnRemoveGeofencesResultListener)">removeGeofences(PendingIntent, LocationClient.OnRemoveGeofencesResultListener)</a></code>. - </dd> - <dt> -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnRemoveGeofencesResultListener.html#onRemoveGeofencesByRequestIdsResult(int, java.lang.String[])">onRemoveGeofencesByRequestIdsResult(List<String>, LocationClient.OnRemoveGeofencesResultListener)</a></code> - </dt> - <dd> - Callback invoked when Location Services finished a request to remove a set of geofences, - specified by their geofence IDs, by the method -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#removeGeofences(java.util.List<java.lang.String>, com.google.android.gms.location.LocationClient.OnRemoveGeofencesResultListener)">removeGeofences(List<String>, LocationClient.OnRemoveGeofencesResultListener)</a></code>. - </dd> -</dl> -<p> - Examples of implementing these methods are shown in the next snippets. -</p> -<h3>Remove all geofences</h3> -<p> - Since removing geofences uses some of the methods you use to add geofences, start by defining - another request type: -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - // Enum type for controlling the type of removal requested - public enum REQUEST_TYPE = {ADD, REMOVE_INTENT} - ... -} -</pre> -<p> - Start the removal request by getting a connection to Location Services. If the connection fails, -<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html#onConnected(android.os.Bundle)">onConnected()</a></code> isn't called, - and the request stops. The following snippet shows how to start the request: -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - /** - * Start a request to remove geofences by calling - * LocationClient.connect() - */ - public void removeGeofences(PendingIntent requestIntent) { - // Record the type of removal request - mRequestType = REMOVE_INTENT; - /* - * Test for Google Play services after setting the request type. - * If Google Play services isn't present, the request can be - * restarted. - */ - if (!servicesConnected()) { - return; - } - // Store the PendingIntent - mGeofenceRequestIntent = requestIntent; - /* - * Create a new location client object. Since the current - * activity class implements ConnectionCallbacks and - * OnConnectionFailedListener, pass the current activity object - * as the listener for both parameters - */ - mLocationClient = new LocationClient(this, this, this); - // If a request is not already underway - if (!mInProgress) { - // Indicate that a request is underway - mInProgress = true; - // Request a connection from the client to Location Services - mLocationClient.connect(); - } else { - /* - * A request is already underway. You can handle - * this situation by disconnecting the client, - * re-setting the flag, and then re-trying the - * request. - */ - } - } - ... -} -</pre> -<p> - When Location Services invokes the callback method indicating that the connection is open, - make the request to remove all geofences. Disconnect the client after making the request. - For example: -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - /** - * Once the connection is available, send a request to remove the - * Geofences. The method signature used depends on which type of - * remove request was originally received. - */ - private void onConnected(Bundle dataBundle) { - /* - * Choose what to do based on the request type set in - * removeGeofences - */ - switch (mRequestType) { - ... - case REMOVE_INTENT : - mLocationClient.removeGeofences( - mGeofenceRequestIntent, this); - break; - ... - } - } - ... -} -</pre> -<p> - Although the call to -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#removeGeofences(android.app.PendingIntent, com.google.android.gms.location.LocationClient.OnRemoveGeofencesResultListener)">removeGeofences(PendingIntent, LocationClient.OnRemoveGeofencesResultListener)</a></code> Services calls - returns immediately, the result of the removal request is indeterminate until Location Services - calls -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnRemoveGeofencesResultListener.html#onRemoveGeofencesByPendingIntentResult(int, android.app.PendingIntent)">onRemoveGeofencesByPendingIntentResult()</a></code>. - The following snippet shows how to define this method: -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - /** - * When the request to remove geofences by PendingIntent returns, - * handle the result. - * - *@param statusCode the code returned by Location Services - *@param requestIntent The Intent used to request the removal. - */ - @Override - public void onRemoveGeofencesByPendingIntentResult(int statusCode, - PendingIntent requestIntent) { - // If removing the geofences was successful - if (statusCode == LocationStatusCodes.SUCCESS) { - /* - * Handle successful removal of geofences here. - * You can send out a broadcast intent or update the UI. - * geofences into the Intent's extended data. - */ - } else { - // If adding the geocodes failed - /* - * Report errors here. - * You can log the error using Log.e() or update - * the UI. - */ - } - /* - * Disconnect the location client regardless of the - * request status, and indicate that a request is no - * longer in progress - */ - mInProgress = false; - mLocationClient.disconnect(); - } - ... -} -</pre> -<h3>Remove individual geofences</h3> -<p> - The procedure for removing an individual geofence or set of geofences is similar to the - removal of all geofences. To specify the geofences you want remove, add their geofence ID - values to a {@link java.util.List} of String objects. Pass this {@link java.util.List} to a - different definition of {@code removeGeofences} with the appropriate signature. This method - then starts the removal process. -</p> -<p> - Start by adding a request type for removing geofences by a list, and also add a global variable - for storing the list of geofences: -</p> -<pre> - ... - // Enum type for controlling the type of removal requested - public enum REQUEST_TYPE = {ADD, REMOVE_INTENT, REMOVE_LIST} - // Store the list of geofence Ids to remove - String<List> mGeofencesToRemove; -</pre> -<p> - Next, define a list of geofences you want to remove. For example, this snippet removes the -<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code> - defined by the geofence ID "1": -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - List<String> listOfGeofences = - Collections.singletonList("1"); - removeGeofences(listOfGeofences); - ... -} -</pre> -<p> - The following snippet defines the {@code removeGeofences()} method: -</p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - /** - * Start a request to remove monitoring by - * calling LocationClient.connect() - * - */ - public void removeGeofences(List<String> geofenceIds) { - // If Google Play services is unavailable, exit - // Record the type of removal request - mRequestType = REMOVE_LIST; - /* - * Test for Google Play services after setting the request type. - * If Google Play services isn't present, the request can be - * restarted. - */ - if (!servicesConnected()) { - return; - } - // Store the list of geofences to remove - mGeofencesToRemove = geofenceIds; - /* - * Create a new location client object. Since the current - * activity class implements ConnectionCallbacks and - * OnConnectionFailedListener, pass the current activity object - * as the listener for both parameters - */ - mLocationClient = new LocationClient(this, this, this); - // If a request is not already underway - if (!mInProgress) { - // Indicate that a request is underway - mInProgress = true; - // Request a connection from the client to Location Services - mLocationClient.connect(); - } else { - /* - * A request is already underway. You can handle - * this situation by disconnecting the client, - * re-setting the flag, and then re-trying the - * request. - */ - } - } - ... -} -</pre> -<p> - When Location Services invokes the callback method indicating that the connection is open, - make the request to remove the list of geofences. Disconnect the client after making the request. - For example: + +<p>Stopping geofence monitoring when it is no longer needed or desired can help save battery + power and CPU cycles on the device. You can stop geofence monitoring + in the main activity used to add and remove geofences; removing a geofence stops it + immediately. The API provides methods to + remove geofences either by request IDs, or by removing geofences associated with a given + {@link android.app.PendingIntent}. </p> -<pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - private void onConnected(Bundle dataBundle) { - ... - switch (mRequestType) { - ... - // If removeGeofencesById was called - case REMOVE_LIST : - mLocationClient.removeGeofences( - mGeofencesToRemove, this); - break; - ... - } - ... - } - ... -} -</pre> <p> - Define an implementation of -<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnRemoveGeofencesResultListener.html#onRemoveGeofencesByRequestIdsResult(int, java.lang.String[])">onRemoveGeofencesByRequestIdsResult()</a></code>. - Location Services invokes this callback method to indicate that the request to remove a list of - geofences is complete. In this method, examine the incoming status code and take the - appropriate action: + The following snippet removes geofences by {@link android.app.PendingIntent}, stopping all + further notification when the device enters or exits previously added geofences: </p> <pre> -public class MainActivity extends FragmentActivity implements - ConnectionCallbacks, - OnConnectionFailedListener, - OnAddGeofencesResultListener { - ... - /** - * When the request to remove geofences by IDs returns, handle the - * result. - * - * @param statusCode The code returned by Location Services - * @param geofenceRequestIds The IDs removed - */ - @Override - public void onRemoveGeofencesByRequestIdsResult( - int statusCode, String[] geofenceRequestIds) { - // If removing the geocodes was successful - if (LocationStatusCodes.SUCCESS == statusCode) { - /* - * Handle successful removal of geofences here. - * You can send out a broadcast intent or update the UI. - * geofences into the Intent's extended data. - */ - } else { - // If removing the geofences failed - /* - * Report errors here. - * You can log the error using Log.e() or update - * the UI. - */ - } - // Indicate that a request is no longer in progress - mInProgress = false; - // Disconnect the location client - mLocationClient.disconnect(); - } - ... +LocationServices.GeofencingApi.removeGeofences( + mGoogleApiClient, + // This is the same pending intent that was used in addGeofences(). + getGeofencePendingIntent() + ).setResultCallback(this); // Result processed in onResult(). } </pre> + <p> - You can combine geofencing with other location-aware features, such as periodic location updates - or activity recognition, which are described in other lessons in this class. -</p> -<p> - The next lesson, - <a href="activity-recognition.html">Recognizing the User's Current Activity</a>, shows you how - to request and receive activity updates. At regular intervals, Location Services can send you - information about the user's current physical activity. Based on this information, you can - change your app's behavior; for example, you can switch to a longer update interval if you - detect that the user is walking instead of driving. + You can combine geofencing with other location-aware features, such as periodic location updates. + For more information, see the other lessons in this class. </p> diff --git a/docs/html/training/location/index.jd b/docs/html/training/location/index.jd index 35e177f923b0..c4dec9928d82 100644 --- a/docs/html/training/location/index.jd +++ b/docs/html/training/location/index.jd @@ -81,4 +81,10 @@ startpage=true Learn how to convert a location's latitude and longitude into an address (reverse geocoding). </dd> + <dt> + <b><a href="geofencing.html">Creating and Monitoring Geofences</a></b> + </dt> <dd> + Learn how to define one or more geographic areas as locations of interest, + called geofences, and detect when the user is close to or inside a geofence. + </dd> </dl> diff --git a/docs/html/training/training_toc.cs b/docs/html/training/training_toc.cs index 89e72f1706b9..11ae1a6f2d67 100644 --- a/docs/html/training/training_toc.cs +++ b/docs/html/training/training_toc.cs @@ -736,6 +736,10 @@ include the action bar on devices running Android 2.1 or higher." Displaying a Location Address </a> </li> + <li><a href="<?cs var:toroot ?>training/location/geofencing.html"> + Creating and Monitoring Geofences + </a> + </li> </ul> </li> </ul> diff --git a/docs/html/wear/images/partners/acer.png b/docs/html/wear/images/partners/acer.png Binary files differnew file mode 100644 index 000000000000..439de80535b3 --- /dev/null +++ b/docs/html/wear/images/partners/acer.png diff --git a/docs/html/wear/images/partners/arm.png b/docs/html/wear/images/partners/arm.png Binary files differnew file mode 100644 index 000000000000..3e2f642f02b9 --- /dev/null +++ b/docs/html/wear/images/partners/arm.png diff --git a/docs/html/wear/images/partners/huawei.png b/docs/html/wear/images/partners/huawei.png Binary files differnew file mode 100644 index 000000000000..9099ed4751e8 --- /dev/null +++ b/docs/html/wear/images/partners/huawei.png diff --git a/docs/html/wear/index.jd b/docs/html/wear/index.jd index 27e809825aec..316f5ca03881 100644 --- a/docs/html/wear/index.jd +++ b/docs/html/wear/index.jd @@ -202,6 +202,12 @@ page.type=about <div class="landing-partners cols"> <div class="col-4"> + <img src="/wear/images/partners/acer.png" alt="Acer"> + </div> + <div class="col-4"> + <img src="/wear/images/partners/arm.png" alt="ARM"> + </div> + <div class="col-4"> <img src="/wear/images/partners/asus.png" alt="Asus"> </div> <div class="col-4"> @@ -214,6 +220,9 @@ page.type=about <img src="/wear/images/partners/htc.png" alt="HTC"> </div> <div class="col-4"> + <img src="/wear/images/partners/huawei.png" alt="Huawei"> + </div> + <div class="col-4"> <img src="/wear/images/partners/intel.png" alt="Intel"> </div> <div class="col-4"> diff --git a/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java index 3efb9c0bf67d..49c424748fdd 100644 --- a/graphics/java/android/graphics/ImageFormat.java +++ b/graphics/java/android/graphics/ImageFormat.java @@ -356,6 +356,38 @@ public class ImageFormat { public static final int RAW10 = 0x25; /** + * Android dense depth image format. + * + * Each pixel is 16 bits, representing a depth ranging measurement from + * a depth camera or similar sensor. + * + * <p>This format assumes + * <ul> + * <li>an even width</li> + * <li>an even height</li> + * <li>a horizontal stride multiple of 16 pixels</li> + * </ul> + * </p> + * + * <pre> y_size = stride * height </pre> + * + * When produced by a camera, the units are millimeters. + */ + public static final int DEPTH16 = 0x44363159; + + /** + * Android sparse depth point cloud format. + * + * <p>A variable-length list of 3D points, with each point represented + * by a triple of floats.</p> + * + * <p>The number of points is {@code (size of the buffer in bytes) / 12}. + * + * The coordinate system and units depend on the source of the point cloud data. + */ + public static final int DEPTH_POINT_CLOUD = 0x101; + + /** * Use this function to retrieve the number of bits per pixel of an * ImageFormat. * @@ -376,6 +408,7 @@ public class ImageFormat { case Y8: return 8; case Y16: + case DEPTH16: return 16; case NV21: return 12; @@ -412,6 +445,8 @@ public class ImageFormat { case YUV_420_888: case RAW_SENSOR: case RAW10: + case DEPTH16: + case DEPTH_POINT_CLOUD: return true; } diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java deleted file mode 100644 index 138d73a8a4a3..000000000000 --- a/graphics/java/android/graphics/drawable/Ripple.java +++ /dev/null @@ -1,578 +0,0 @@ -/* - * 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.graphics.drawable; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.TimeInterpolator; -import android.graphics.Canvas; -import android.graphics.CanvasProperty; -import android.graphics.Paint; -import android.graphics.Rect; -import android.util.MathUtils; -import android.view.HardwareCanvas; -import android.view.RenderNodeAnimator; -import android.view.animation.LinearInterpolator; - -import java.util.ArrayList; - -/** - * Draws a Material ripple. - */ -class Ripple { - private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); - private static final TimeInterpolator DECEL_INTERPOLATOR = new LogInterpolator(); - - private static final float GLOBAL_SPEED = 1.0f; - private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED; - private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED; - private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED; - - private static final long RIPPLE_ENTER_DELAY = 80; - - // Hardware animators. - private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>(); - - private final RippleDrawable mOwner; - - /** Bounds used for computing max radius. */ - private final Rect mBounds; - - /** Maximum ripple radius. */ - private float mOuterRadius; - - /** Screen density used to adjust pixel-based velocities. */ - private float mDensity; - - private float mStartingX; - private float mStartingY; - private float mClampedStartingX; - private float mClampedStartingY; - - // Hardware rendering properties. - private CanvasProperty<Paint> mPropPaint; - private CanvasProperty<Float> mPropRadius; - private CanvasProperty<Float> mPropX; - private CanvasProperty<Float> mPropY; - - // Software animators. - private ObjectAnimator mAnimRadius; - private ObjectAnimator mAnimOpacity; - private ObjectAnimator mAnimX; - private ObjectAnimator mAnimY; - - // Temporary paint used for creating canvas properties. - private Paint mTempPaint; - - // Software rendering properties. - private float mOpacity = 1; - private float mOuterX; - private float mOuterY; - - // Values used to tween between the start and end positions. - private float mTweenRadius = 0; - private float mTweenX = 0; - private float mTweenY = 0; - - /** Whether we should be drawing hardware animations. */ - private boolean mHardwareAnimating; - - /** Whether we can use hardware acceleration for the exit animation. */ - private boolean mCanUseHardware; - - /** Whether we have an explicit maximum radius. */ - private boolean mHasMaxRadius; - - /** Whether we were canceled externally and should avoid self-removal. */ - private boolean mCanceled; - - private boolean mHasPendingHardwareExit; - private int mPendingRadiusDuration; - private int mPendingOpacityDuration; - - /** - * Creates a new ripple. - */ - public Ripple(RippleDrawable owner, Rect bounds, float startingX, float startingY) { - mOwner = owner; - mBounds = bounds; - - mStartingX = startingX; - mStartingY = startingY; - } - - public void setup(float maxRadius, float density) { - if (maxRadius >= 0) { - mHasMaxRadius = true; - mOuterRadius = maxRadius; - } else { - final float halfWidth = mBounds.width() / 2.0f; - final float halfHeight = mBounds.height() / 2.0f; - mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); - } - - mOuterX = 0; - mOuterY = 0; - mDensity = density; - - clampStartingPosition(); - } - - public boolean isHardwareAnimating() { - return mHardwareAnimating; - } - - private void clampStartingPosition() { - final float cX = mBounds.exactCenterX(); - final float cY = mBounds.exactCenterY(); - final float dX = mStartingX - cX; - final float dY = mStartingY - cY; - final float r = mOuterRadius; - if (dX * dX + dY * dY > r * r) { - // Point is outside the circle, clamp to the circumference. - final double angle = Math.atan2(dY, dX); - mClampedStartingX = cX + (float) (Math.cos(angle) * r); - mClampedStartingY = cY + (float) (Math.sin(angle) * r); - } else { - mClampedStartingX = mStartingX; - mClampedStartingY = mStartingY; - } - } - - public void onHotspotBoundsChanged() { - if (!mHasMaxRadius) { - final float halfWidth = mBounds.width() / 2.0f; - final float halfHeight = mBounds.height() / 2.0f; - mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); - - clampStartingPosition(); - } - } - - public void setOpacity(float a) { - mOpacity = a; - invalidateSelf(); - } - - public float getOpacity() { - return mOpacity; - } - - @SuppressWarnings("unused") - public void setRadiusGravity(float r) { - mTweenRadius = r; - invalidateSelf(); - } - - @SuppressWarnings("unused") - public float getRadiusGravity() { - return mTweenRadius; - } - - @SuppressWarnings("unused") - public void setXGravity(float x) { - mTweenX = x; - invalidateSelf(); - } - - @SuppressWarnings("unused") - public float getXGravity() { - return mTweenX; - } - - @SuppressWarnings("unused") - public void setYGravity(float y) { - mTweenY = y; - invalidateSelf(); - } - - @SuppressWarnings("unused") - public float getYGravity() { - return mTweenY; - } - - /** - * Draws the ripple centered at (0,0) using the specified paint. - */ - public boolean draw(Canvas c, Paint p) { - final boolean canUseHardware = c.isHardwareAccelerated(); - if (mCanUseHardware != canUseHardware && mCanUseHardware) { - // We've switched from hardware to non-hardware mode. Panic. - cancelHardwareAnimations(true); - } - mCanUseHardware = canUseHardware; - - final boolean hasContent; - if (canUseHardware && (mHardwareAnimating || mHasPendingHardwareExit)) { - hasContent = drawHardware((HardwareCanvas) c, p); - } else { - hasContent = drawSoftware(c, p); - } - - return hasContent; - } - - private boolean drawHardware(HardwareCanvas c, Paint p) { - if (mHasPendingHardwareExit) { - cancelHardwareAnimations(false); - startPendingHardwareExit(c, p); - } - - c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); - - return true; - } - - private boolean drawSoftware(Canvas c, Paint p) { - boolean hasContent = false; - - final int paintAlpha = p.getAlpha(); - final int alpha = (int) (paintAlpha * mOpacity + 0.5f); - final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); - if (alpha > 0 && radius > 0) { - final float x = MathUtils.lerp( - mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); - final float y = MathUtils.lerp( - mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); - p.setAlpha(alpha); - c.drawCircle(x, y, radius, p); - p.setAlpha(paintAlpha); - hasContent = true; - } - - return hasContent; - } - - /** - * Returns the maximum bounds of the ripple relative to the ripple center. - */ - public void getBounds(Rect bounds) { - final int outerX = (int) mOuterX; - final int outerY = (int) mOuterY; - final int r = (int) mOuterRadius + 1; - bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); - } - - /** - * Specifies the starting position relative to the drawable bounds. No-op if - * the ripple has already entered. - */ - public void move(float x, float y) { - mStartingX = x; - mStartingY = y; - - clampStartingPosition(); - } - - /** - * Starts the enter animation. - */ - public void enter() { - cancel(); - - final int radiusDuration = (int) - (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5); - - final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1); - radius.setAutoCancel(true); - radius.setDuration(radiusDuration); - radius.setInterpolator(LINEAR_INTERPOLATOR); - radius.setStartDelay(RIPPLE_ENTER_DELAY); - - final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1); - cX.setAutoCancel(true); - cX.setDuration(radiusDuration); - cX.setInterpolator(LINEAR_INTERPOLATOR); - cX.setStartDelay(RIPPLE_ENTER_DELAY); - - final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1); - cY.setAutoCancel(true); - cY.setDuration(radiusDuration); - cY.setInterpolator(LINEAR_INTERPOLATOR); - cY.setStartDelay(RIPPLE_ENTER_DELAY); - - mAnimRadius = radius; - mAnimX = cX; - mAnimY = cY; - - // Enter animations always run on the UI thread, since it's unlikely - // that anything interesting is happening until the user lifts their - // finger. - radius.start(); - cX.start(); - cY.start(); - } - - /** - * Starts the exit animation. - */ - public void exit() { - final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); - final float remaining; - if (mAnimRadius != null && mAnimRadius.isRunning()) { - remaining = mOuterRadius - radius; - } else { - remaining = mOuterRadius; - } - - cancel(); - - final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION - + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5); - final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); - - if (mCanUseHardware) { - createPendingHardwareExit(radiusDuration, opacityDuration); - } else { - exitSoftware(radiusDuration, opacityDuration); - } - } - - private void createPendingHardwareExit(int radiusDuration, int opacityDuration) { - mHasPendingHardwareExit = true; - mPendingRadiusDuration = radiusDuration; - mPendingOpacityDuration = opacityDuration; - - // The animation will start on the next draw(). - invalidateSelf(); - } - - private void startPendingHardwareExit(HardwareCanvas c, Paint p) { - mHasPendingHardwareExit = false; - - final int radiusDuration = mPendingRadiusDuration; - final int opacityDuration = mPendingOpacityDuration; - - final float startX = MathUtils.lerp( - mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); - final float startY = MathUtils.lerp( - mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); - - final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); - final Paint paint = getTempPaint(p); - paint.setAlpha((int) (paint.getAlpha() * mOpacity + 0.5f)); - mPropPaint = CanvasProperty.createPaint(paint); - mPropRadius = CanvasProperty.createFloat(startRadius); - mPropX = CanvasProperty.createFloat(startX); - mPropY = CanvasProperty.createFloat(startY); - - final RenderNodeAnimator radiusAnim = new RenderNodeAnimator(mPropRadius, mOuterRadius); - radiusAnim.setDuration(radiusDuration); - radiusAnim.setInterpolator(DECEL_INTERPOLATOR); - radiusAnim.setTarget(c); - radiusAnim.start(); - - final RenderNodeAnimator xAnim = new RenderNodeAnimator(mPropX, mOuterX); - xAnim.setDuration(radiusDuration); - xAnim.setInterpolator(DECEL_INTERPOLATOR); - xAnim.setTarget(c); - xAnim.start(); - - final RenderNodeAnimator yAnim = new RenderNodeAnimator(mPropY, mOuterY); - yAnim.setDuration(radiusDuration); - yAnim.setInterpolator(DECEL_INTERPOLATOR); - yAnim.setTarget(c); - yAnim.start(); - - final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPropPaint, - RenderNodeAnimator.PAINT_ALPHA, 0); - opacityAnim.setDuration(opacityDuration); - opacityAnim.setInterpolator(LINEAR_INTERPOLATOR); - opacityAnim.addListener(mAnimationListener); - opacityAnim.setTarget(c); - opacityAnim.start(); - - mRunningAnimations.add(radiusAnim); - mRunningAnimations.add(opacityAnim); - mRunningAnimations.add(xAnim); - mRunningAnimations.add(yAnim); - - mHardwareAnimating = true; - - // Set up the software values to match the hardware end values. - mOpacity = 0; - mTweenX = 1; - mTweenY = 1; - mTweenRadius = 1; - } - - /** - * Jump all animations to their end state. The caller is responsible for - * removing the ripple from the list of animating ripples. - */ - public void jump() { - mCanceled = true; - endSoftwareAnimations(); - cancelHardwareAnimations(true); - mCanceled = false; - } - - private void endSoftwareAnimations() { - if (mAnimRadius != null) { - mAnimRadius.end(); - mAnimRadius = null; - } - - if (mAnimOpacity != null) { - mAnimOpacity.end(); - mAnimOpacity = null; - } - - if (mAnimX != null) { - mAnimX.end(); - mAnimX = null; - } - - if (mAnimY != null) { - mAnimY.end(); - mAnimY = null; - } - } - - private Paint getTempPaint(Paint original) { - if (mTempPaint == null) { - mTempPaint = new Paint(); - } - mTempPaint.set(original); - return mTempPaint; - } - - private void exitSoftware(int radiusDuration, int opacityDuration) { - final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1); - radiusAnim.setAutoCancel(true); - radiusAnim.setDuration(radiusDuration); - radiusAnim.setInterpolator(DECEL_INTERPOLATOR); - - final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1); - xAnim.setAutoCancel(true); - xAnim.setDuration(radiusDuration); - xAnim.setInterpolator(DECEL_INTERPOLATOR); - - final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1); - yAnim.setAutoCancel(true); - yAnim.setDuration(radiusDuration); - yAnim.setInterpolator(DECEL_INTERPOLATOR); - - final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0); - opacityAnim.setAutoCancel(true); - opacityAnim.setDuration(opacityDuration); - opacityAnim.setInterpolator(LINEAR_INTERPOLATOR); - opacityAnim.addListener(mAnimationListener); - - mAnimRadius = radiusAnim; - mAnimOpacity = opacityAnim; - mAnimX = xAnim; - mAnimY = yAnim; - - radiusAnim.start(); - opacityAnim.start(); - xAnim.start(); - yAnim.start(); - } - - /** - * Cancels all animations. The caller is responsible for removing - * the ripple from the list of animating ripples. - */ - public void cancel() { - mCanceled = true; - cancelSoftwareAnimations(); - cancelHardwareAnimations(false); - mCanceled = false; - } - - private void cancelSoftwareAnimations() { - if (mAnimRadius != null) { - mAnimRadius.cancel(); - mAnimRadius = null; - } - - if (mAnimOpacity != null) { - mAnimOpacity.cancel(); - mAnimOpacity = null; - } - - if (mAnimX != null) { - mAnimX.cancel(); - mAnimX = null; - } - - if (mAnimY != null) { - mAnimY.cancel(); - mAnimY = null; - } - } - - /** - * Cancels any running hardware animations. - */ - private void cancelHardwareAnimations(boolean jumpToEnd) { - final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; - final int N = runningAnimations.size(); - for (int i = 0; i < N; i++) { - if (jumpToEnd) { - runningAnimations.get(i).end(); - } else { - runningAnimations.get(i).cancel(); - } - } - runningAnimations.clear(); - - if (mHasPendingHardwareExit) { - // If we had a pending hardware exit, jump to the end state. - mHasPendingHardwareExit = false; - - if (jumpToEnd) { - mOpacity = 0; - mTweenX = 1; - mTweenY = 1; - mTweenRadius = 1; - } - } - - mHardwareAnimating = false; - } - - private void removeSelf() { - // The owner will invalidate itself. - if (!mCanceled) { - mOwner.removeRipple(this); - } - } - - private void invalidateSelf() { - mOwner.invalidateSelf(); - } - - private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - removeSelf(); - } - }; - - /** - * Interpolator with a smooth log deceleration - */ - private static final class LogInterpolator implements TimeInterpolator { - @Override - public float getInterpolation(float input) { - return 1 - (float) Math.pow(400, -input * 1.4); - } - } -} diff --git a/graphics/java/android/graphics/drawable/RippleBackground.java b/graphics/java/android/graphics/drawable/RippleBackground.java index ef352890e424..6d1b1fecd51c 100644 --- a/graphics/java/android/graphics/drawable/RippleBackground.java +++ b/graphics/java/android/graphics/drawable/RippleBackground.java @@ -17,432 +17,162 @@ package android.graphics.drawable; import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.graphics.Canvas; import android.graphics.CanvasProperty; -import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; -import android.util.MathUtils; +import android.util.FloatProperty; import android.view.HardwareCanvas; import android.view.RenderNodeAnimator; import android.view.animation.LinearInterpolator; -import java.util.ArrayList; - /** - * Draws a Material ripple. + * Draws a ripple background. */ -class RippleBackground { +class RippleBackground extends RippleComponent { private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); - private static final float GLOBAL_SPEED = 1.0f; - private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED; - private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED; - private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED; - private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f; - private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f; - - private static final int ENTER_DURATION = 667; - private static final int ENTER_DURATION_FAST = 100; - - // Hardware animators. - private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>(); - - private final RippleDrawable mOwner; - - /** Bounds used for computing max radius. */ - private final Rect mBounds; - - /** ARGB color for drawing this ripple. */ - private int mColor; - - /** Maximum ripple radius. */ - private float mOuterRadius; - - /** Screen density used to adjust pixel-based velocities. */ - private float mDensity; + private static final int OPACITY_ENTER_DURATION = 600; + private static final int OPACITY_ENTER_DURATION_FAST = 120; + private static final int OPACITY_EXIT_DURATION = 480; // Hardware rendering properties. - private CanvasProperty<Paint> mPropOuterPaint; - private CanvasProperty<Float> mPropOuterRadius; - private CanvasProperty<Float> mPropOuterX; - private CanvasProperty<Float> mPropOuterY; - - // Software animators. - private ObjectAnimator mAnimOuterOpacity; - - // Temporary paint used for creating canvas properties. - private Paint mTempPaint; + private CanvasProperty<Paint> mPropPaint; + private CanvasProperty<Float> mPropRadius; + private CanvasProperty<Float> mPropX; + private CanvasProperty<Float> mPropY; // Software rendering properties. - private float mOuterOpacity = 0; - private float mOuterX; - private float mOuterY; - - /** Whether we should be drawing hardware animations. */ - private boolean mHardwareAnimating; - - /** Whether we can use hardware acceleration for the exit animation. */ - private boolean mCanUseHardware; - - /** Whether we have an explicit maximum radius. */ - private boolean mHasMaxRadius; - - private boolean mHasPendingHardwareExit; - private int mPendingOpacityDuration; - private int mPendingInflectionDuration; - private int mPendingInflectionOpacity; + private float mOpacity = 0; - /** - * Creates a new ripple. - */ public RippleBackground(RippleDrawable owner, Rect bounds) { - mOwner = owner; - mBounds = bounds; - } - - public void setup(float maxRadius, float density) { - if (maxRadius >= 0) { - mHasMaxRadius = true; - mOuterRadius = maxRadius; - } else { - final float halfWidth = mBounds.width() / 2.0f; - final float halfHeight = mBounds.height() / 2.0f; - mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); - } - - mOuterX = 0; - mOuterY = 0; - mDensity = density; + super(owner, bounds); } - public void onHotspotBoundsChanged() { - if (!mHasMaxRadius) { - final float halfWidth = mBounds.width() / 2.0f; - final float halfHeight = mBounds.height() / 2.0f; - mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); - } - } - - @SuppressWarnings("unused") - public void setOuterOpacity(float a) { - mOuterOpacity = a; - invalidateSelf(); - } - - @SuppressWarnings("unused") - public float getOuterOpacity() { - return mOuterOpacity; - } - - /** - * Draws the ripple centered at (0,0) using the specified paint. - */ - public boolean draw(Canvas c, Paint p) { - mColor = p.getColor(); - - final boolean canUseHardware = c.isHardwareAccelerated(); - if (mCanUseHardware != canUseHardware && mCanUseHardware) { - // We've switched from hardware to non-hardware mode. Panic. - cancelHardwareAnimations(true); - } - mCanUseHardware = canUseHardware; - - final boolean hasContent; - if (canUseHardware && (mHardwareAnimating || mHasPendingHardwareExit)) { - hasContent = drawHardware((HardwareCanvas) c, p); - } else { - hasContent = drawSoftware(c, p); - } - - return hasContent; + public boolean isVisible() { + return mOpacity > 0 || isHardwareAnimating(); } - public boolean shouldDraw() { - return (mCanUseHardware && mHardwareAnimating) || (mOuterOpacity > 0 && mOuterRadius > 0); - } - - private boolean drawHardware(HardwareCanvas c, Paint p) { - if (mHasPendingHardwareExit) { - cancelHardwareAnimations(false); - startPendingHardwareExit(c, p); - } - - c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint); - - return true; - } - - private boolean drawSoftware(Canvas c, Paint p) { + @Override + protected boolean drawSoftware(Canvas c, Paint p) { boolean hasContent = false; - final int paintAlpha = p.getAlpha(); - final int alpha = (int) (paintAlpha * mOuterOpacity + 0.5f); - final float radius = mOuterRadius; - if (alpha > 0 && radius > 0) { + final int origAlpha = p.getAlpha(); + final int alpha = (int) (origAlpha * mOpacity + 0.5f); + if (alpha > 0) { p.setAlpha(alpha); - c.drawCircle(mOuterX, mOuterY, radius, p); - p.setAlpha(paintAlpha); + c.drawCircle(0, 0, mTargetRadius, p); + p.setAlpha(origAlpha); hasContent = true; } return hasContent; } - /** - * Returns the maximum bounds of the ripple relative to the ripple center. - */ - public void getBounds(Rect bounds) { - final int outerX = (int) mOuterX; - final int outerY = (int) mOuterY; - final int r = (int) mOuterRadius + 1; - bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); + @Override + protected boolean drawHardware(HardwareCanvas c) { + c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); + return true; } - /** - * Starts the enter animation. - */ - public void enter(boolean fast) { - cancel(); + @Override + protected Animator createSoftwareEnter(boolean fast) { + // Linear enter based on current opacity. + final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION; + final int duration = (int) ((1 - mOpacity) * maxDuration); - final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1); + final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); opacity.setAutoCancel(true); - opacity.setDuration(fast ? ENTER_DURATION_FAST : ENTER_DURATION); + opacity.setDuration(duration); opacity.setInterpolator(LINEAR_INTERPOLATOR); - mAnimOuterOpacity = opacity; - - // Enter animations always run on the UI thread, since it's unlikely - // that anything interesting is happening until the user lifts their - // finger. - opacity.start(); - } - - /** - * Starts the exit animation. - */ - public void exit() { - cancel(); - - // Scale the outer max opacity and opacity velocity based - // on the size of the outer radius. - final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); - final float outerSizeInfluence = MathUtils.constrain( - (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity) - / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1); - final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN, - WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence); - - // Determine at what time the inner and outer opacity intersect. - // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000 - // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000 - final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity) - / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f)); - final int inflectionOpacity = (int) (Color.alpha(mColor) * (mOuterOpacity - + inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f); - - if (mCanUseHardware) { - createPendingHardwareExit(opacityDuration, inflectionDuration, inflectionOpacity); - } else { - exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity); - } - } - - private void createPendingHardwareExit( - int opacityDuration, int inflectionDuration, int inflectionOpacity) { - mHasPendingHardwareExit = true; - mPendingOpacityDuration = opacityDuration; - mPendingInflectionDuration = inflectionDuration; - mPendingInflectionOpacity = inflectionOpacity; - - // The animation will start on the next draw(). - invalidateSelf(); + return opacity; } - private void startPendingHardwareExit(HardwareCanvas c, Paint p) { - mHasPendingHardwareExit = false; + @Override + protected Animator createSoftwareExit() { + final AnimatorSet set = new AnimatorSet(); - final int opacityDuration = mPendingOpacityDuration; - final int inflectionDuration = mPendingInflectionDuration; - final int inflectionOpacity = mPendingInflectionOpacity; + // Linear exit after enter is completed. + final ObjectAnimator exit = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 0); + exit.setInterpolator(LINEAR_INTERPOLATOR); + exit.setDuration(OPACITY_EXIT_DURATION); + exit.setAutoCancel(true); - final Paint outerPaint = getTempPaint(p); - outerPaint.setAlpha((int) (outerPaint.getAlpha() * mOuterOpacity + 0.5f)); - mPropOuterPaint = CanvasProperty.createPaint(outerPaint); - mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius); - mPropOuterX = CanvasProperty.createFloat(mOuterX); - mPropOuterY = CanvasProperty.createFloat(mOuterY); + final AnimatorSet.Builder builder = set.play(exit); - final RenderNodeAnimator outerOpacityAnim; - if (inflectionDuration > 0) { - // Outer opacity continues to increase for a bit. - outerOpacityAnim = new RenderNodeAnimator(mPropOuterPaint, - RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity); - outerOpacityAnim.setDuration(inflectionDuration); - outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); + // Linear "fast" enter based on current opacity. + final int fastEnterDuration = (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST); + if (fastEnterDuration > 0) { + final ObjectAnimator enter = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 1); + enter.setInterpolator(LINEAR_INTERPOLATOR); + enter.setDuration(fastEnterDuration); + enter.setAutoCancel(true); - // Chain the outer opacity exit animation. - final int outerDuration = opacityDuration - inflectionDuration; - if (outerDuration > 0) { - final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator( - mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); - outerFadeOutAnim.setDuration(outerDuration); - outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); - outerFadeOutAnim.setStartDelay(inflectionDuration); - outerFadeOutAnim.setStartValue(inflectionOpacity); - outerFadeOutAnim.addListener(mAnimationListener); - outerFadeOutAnim.setTarget(c); - outerFadeOutAnim.start(); - - mRunningAnimations.add(outerFadeOutAnim); - } else { - outerOpacityAnim.addListener(mAnimationListener); - } - } else { - outerOpacityAnim = new RenderNodeAnimator( - mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); - outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); - outerOpacityAnim.setDuration(opacityDuration); - outerOpacityAnim.addListener(mAnimationListener); + builder.after(enter); } - outerOpacityAnim.setTarget(c); - outerOpacityAnim.start(); - - mRunningAnimations.add(outerOpacityAnim); - - mHardwareAnimating = true; - - // Set up the software values to match the hardware end values. - mOuterOpacity = 0; + return set; } - /** - * Jump all animations to their end state. The caller is responsible for - * removing the ripple from the list of animating ripples. - */ - public void jump() { - endSoftwareAnimations(); - cancelHardwareAnimations(true); - } - - private void endSoftwareAnimations() { - if (mAnimOuterOpacity != null) { - mAnimOuterOpacity.end(); - mAnimOuterOpacity = null; - } - } - - private Paint getTempPaint(Paint original) { - if (mTempPaint == null) { - mTempPaint = new Paint(); - } - mTempPaint.set(original); - return mTempPaint; - } - - private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) { - final ObjectAnimator outerOpacityAnim; - if (inflectionDuration > 0) { - // Outer opacity continues to increase for a bit. - outerOpacityAnim = ObjectAnimator.ofFloat(this, - "outerOpacity", inflectionOpacity / 255.0f); - outerOpacityAnim.setAutoCancel(true); - outerOpacityAnim.setDuration(inflectionDuration); - outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); - - // Chain the outer opacity exit animation. - final int outerDuration = opacityDuration - inflectionDuration; - if (outerDuration > 0) { - outerOpacityAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat( - RippleBackground.this, "outerOpacity", 0); - outerFadeOutAnim.setAutoCancel(true); - outerFadeOutAnim.setDuration(outerDuration); - outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); - outerFadeOutAnim.addListener(mAnimationListener); + @Override + protected RenderNodeAnimatorSet createHardwareExit(Paint p) { + final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); - mAnimOuterOpacity = outerFadeOutAnim; + final int targetAlpha = p.getAlpha(); + final int currentAlpha = (int) (mOpacity * targetAlpha + 0.5f); + p.setAlpha(currentAlpha); - outerFadeOutAnim.start(); - } + mPropPaint = CanvasProperty.createPaint(p); + mPropRadius = CanvasProperty.createFloat(mTargetRadius); + mPropX = CanvasProperty.createFloat(0); + mPropY = CanvasProperty.createFloat(0); - @Override - public void onAnimationCancel(Animator animation) { - animation.removeListener(this); - } - }); - } else { - outerOpacityAnim.addListener(mAnimationListener); - } - } else { - outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0); - outerOpacityAnim.setAutoCancel(true); - outerOpacityAnim.setDuration(opacityDuration); - outerOpacityAnim.addListener(mAnimationListener); + // Linear "fast" enter based on current opacity. + final int fastEnterDuration = (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST); + if (fastEnterDuration > 0) { + final RenderNodeAnimator enter = new RenderNodeAnimator( + mPropPaint, RenderNodeAnimator.PAINT_ALPHA, targetAlpha); + enter.setInterpolator(LINEAR_INTERPOLATOR); + enter.setDuration(fastEnterDuration); + set.add(enter); } - mAnimOuterOpacity = outerOpacityAnim; + // Linear exit after enter is completed. + final RenderNodeAnimator exit = new RenderNodeAnimator( + mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0); + exit.setInterpolator(LINEAR_INTERPOLATOR); + exit.setDuration(OPACITY_EXIT_DURATION); + exit.setStartDelay(fastEnterDuration); + set.add(exit); - outerOpacityAnim.start(); + return set; } - /** - * Cancel all animations. The caller is responsible for removing - * the ripple from the list of animating ripples. - */ - public void cancel() { - cancelSoftwareAnimations(); - cancelHardwareAnimations(false); + @Override + protected void jumpValuesToExit() { + mOpacity = 0; } - private void cancelSoftwareAnimations() { - if (mAnimOuterOpacity != null) { - mAnimOuterOpacity.cancel(); - mAnimOuterOpacity = null; + private static abstract class BackgroundProperty extends FloatProperty<RippleBackground> { + public BackgroundProperty(String name) { + super(name); } } - /** - * Cancels any running hardware animations. - */ - private void cancelHardwareAnimations(boolean jumpToEnd) { - final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; - final int N = runningAnimations.size(); - for (int i = 0; i < N; i++) { - if (jumpToEnd) { - runningAnimations.get(i).end(); - } else { - runningAnimations.get(i).cancel(); - } - } - runningAnimations.clear(); - - if (mHasPendingHardwareExit) { - // If we had a pending hardware exit, jump to the end state. - mHasPendingHardwareExit = false; - - if (jumpToEnd) { - mOuterOpacity = 0; - } + private static final BackgroundProperty OPACITY = new BackgroundProperty("opacity") { + @Override + public void setValue(RippleBackground object, float value) { + object.mOpacity = value; + object.invalidateSelf(); } - mHardwareAnimating = false; - } - - private void invalidateSelf() { - mOwner.invalidateSelf(); - } - - private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { - mHardwareAnimating = false; + public Float get(RippleBackground object) { + return object.mOpacity; } }; } diff --git a/graphics/java/android/graphics/drawable/RippleComponent.java b/graphics/java/android/graphics/drawable/RippleComponent.java new file mode 100644 index 000000000000..fd3e06cad0d3 --- /dev/null +++ b/graphics/java/android/graphics/drawable/RippleComponent.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2015 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.graphics.drawable; + +import android.animation.Animator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.HardwareCanvas; +import android.view.RenderNodeAnimator; + +import java.util.ArrayList; + +/** + * Abstract class that handles hardware/software hand-off and lifecycle for + * animated ripple foreground and background components. + */ +abstract class RippleComponent { + private final RippleDrawable mOwner; + + /** Bounds used for computing max radius. May be modified by the owner. */ + protected final Rect mBounds; + + /** Whether we can use hardware acceleration for the exit animation. */ + private boolean mHasHardwareCanvas; + + private boolean mHasPendingHardwareAnimator; + private RenderNodeAnimatorSet mHardwareAnimator; + + private Animator mSoftwareAnimator; + + /** Whether we have an explicit maximum radius. */ + private boolean mHasMaxRadius; + + /** How big this ripple should be when fully entered. */ + protected float mTargetRadius; + + /** Screen density used to adjust pixel-based constants. */ + protected float mDensity; + + public RippleComponent(RippleDrawable owner, Rect bounds) { + mOwner = owner; + mBounds = bounds; + } + + public final void setup(float maxRadius, float density) { + if (maxRadius >= 0) { + mHasMaxRadius = true; + mTargetRadius = maxRadius; + } else { + final float halfWidth = mBounds.width() / 2.0f; + final float halfHeight = mBounds.height() / 2.0f; + mTargetRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); + } + + mDensity = density; + + onSetup(); + onTargetRadiusChanged(mTargetRadius); + } + + /** + * Starts a ripple enter animation. + * + * @param fast whether the ripple should enter quickly + */ + public final void enter(boolean fast) { + cancel(); + + mSoftwareAnimator = createSoftwareEnter(fast); + mSoftwareAnimator.start(); + } + + /** + * Starts a ripple exit animation. + */ + public final void exit() { + cancel(); + + if (mHasHardwareCanvas) { + // We don't have access to a canvas here, but we expect one on the + // next frame. We'll start the render thread animation then. + mHasPendingHardwareAnimator = true; + + // Request another frame. + invalidateSelf(); + } else { + mSoftwareAnimator = createSoftwareExit(); + mSoftwareAnimator.start(); + } + } + + /** + * Cancels all animations. Software animation values are left in the + * current state, while hardware animation values jump to the end state. + */ + public void cancel() { + cancelSoftwareAnimations(); + endHardwareAnimations(); + } + + /** + * Ends all animations, jumping values to the end state. + */ + public void end() { + endSoftwareAnimations(); + endHardwareAnimations(); + } + + /** + * Draws the ripple to the canvas, inheriting the paint's color and alpha + * properties. + * + * @param c the canvas to which the ripple should be drawn + * @param p the paint used to draw the ripple + * @return {@code true} if something was drawn, {@code false} otherwise + */ + public boolean draw(Canvas c, Paint p) { + final boolean hasHardwareCanvas = c.isHardwareAccelerated() + && c instanceof HardwareCanvas; + if (mHasHardwareCanvas != hasHardwareCanvas) { + mHasHardwareCanvas = hasHardwareCanvas; + + if (!hasHardwareCanvas) { + // We've switched from hardware to non-hardware mode. Panic. + endHardwareAnimations(); + } + } + + if (hasHardwareCanvas) { + final HardwareCanvas hw = (HardwareCanvas) c; + startPendingAnimation(hw, p); + + if (mHardwareAnimator != null) { + return drawHardware(hw); + } + } + + return drawSoftware(c, p); + } + + /** + * Populates {@code bounds} with the maximum drawing bounds of the ripple + * relative to its center. The resulting bounds should be translated into + * parent drawable coordinates before use. + * + * @param bounds the rect to populate with drawing bounds + */ + public void getBounds(Rect bounds) { + final int r = (int) Math.ceil(mTargetRadius); + bounds.set(-r, -r, r, r); + } + + /** + * Starts the pending hardware animation, if available. + * + * @param hw hardware canvas on which the animation should draw + * @param p paint whose properties the hardware canvas should use + */ + private void startPendingAnimation(HardwareCanvas hw, Paint p) { + if (mHasPendingHardwareAnimator) { + mHasPendingHardwareAnimator = false; + + mHardwareAnimator = createHardwareExit(new Paint(p)); + mHardwareAnimator.start(hw); + + // Preemptively jump the software values to the end state now that + // the hardware exit has read whatever values it needs. + jumpValuesToExit(); + } + } + + /** + * Cancels any current software animations, leaving the values in their + * current state. + */ + private void cancelSoftwareAnimations() { + if (mSoftwareAnimator != null) { + mSoftwareAnimator.cancel(); + } + } + + /** + * Ends any current software animations, jumping the values to their end + * state. + */ + private void endSoftwareAnimations() { + if (mSoftwareAnimator != null) { + mSoftwareAnimator.end(); + } + } + + /** + * Ends any pending or current hardware animations. + * <p> + * Hardware animations can't synchronize values back to the software + * thread, so there is no "cancel" equivalent. + */ + private void endHardwareAnimations() { + if (mHardwareAnimator != null) { + mHardwareAnimator.end(); + mHardwareAnimator = null; + } + + if (mHasPendingHardwareAnimator) { + mHasPendingHardwareAnimator = false; + } + } + + protected final void invalidateSelf() { + mOwner.invalidateSelf(); + } + + protected final boolean isHardwareAnimating() { + return mHardwareAnimator != null && mHardwareAnimator.isRunning() + || mHasPendingHardwareAnimator; + } + + protected final void onHotspotBoundsChanged() { + if (!mHasMaxRadius) { + final float halfWidth = mBounds.width() / 2.0f; + final float halfHeight = mBounds.height() / 2.0f; + final float targetRadius = (float) Math.sqrt(halfWidth * halfWidth + + halfHeight * halfHeight); + + onTargetRadiusChanged(targetRadius); + } + } + + /** + * Called when the target radius changes. + * + * @param targetRadius the new target radius + */ + protected void onTargetRadiusChanged(float targetRadius) { + // Stub. + } + + /** + * Called during ripple setup, which occurs before the first enter + * animation. + */ + protected void onSetup() { + // Stub. + } + + protected abstract Animator createSoftwareEnter(boolean fast); + + protected abstract Animator createSoftwareExit(); + + protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p); + + protected abstract boolean drawHardware(HardwareCanvas c); + + protected abstract boolean drawSoftware(Canvas c, Paint p); + + /** + * Called when the hardware exit is cancelled. Jumps software values to end + * state to ensure that software and hardware values are synchronized. + */ + protected abstract void jumpValuesToExit(); + + public static class RenderNodeAnimatorSet { + private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>(); + + public void add(RenderNodeAnimator anim) { + mAnimators.add(anim); + } + + public void clear() { + mAnimators.clear(); + } + + public void start(HardwareCanvas target) { + if (target == null) { + throw new IllegalArgumentException("Hardware canvas must be non-null"); + } + + final ArrayList<RenderNodeAnimator> animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final RenderNodeAnimator anim = animators.get(i); + anim.setTarget(target); + anim.start(); + } + } + + public void cancel() { + final ArrayList<RenderNodeAnimator> animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final RenderNodeAnimator anim = animators.get(i); + anim.cancel(); + } + } + + public void end() { + final ArrayList<RenderNodeAnimator> animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final RenderNodeAnimator anim = animators.get(i); + anim.end(); + } + } + + public boolean isRunning() { + final ArrayList<RenderNodeAnimator> animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final RenderNodeAnimator anim = animators.get(i); + if (anim.isRunning()) { + return true; + } + } + return false; + } + } +} diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java index b028eeb0612a..bc8c7d2ff246 100644 --- a/graphics/java/android/graphics/drawable/RippleDrawable.java +++ b/graphics/java/android/graphics/drawable/RippleDrawable.java @@ -137,7 +137,7 @@ public class RippleDrawable extends LayerDrawable { private boolean mBackgroundActive; /** The current ripple. May be actively animating or pending entry. */ - private Ripple mRipple; + private RippleForeground mRipple; /** Whether we expect to draw a ripple when visible. */ private boolean mRippleActive; @@ -151,7 +151,7 @@ public class RippleDrawable extends LayerDrawable { * Lazily-created array of actively animating ripples. Inactive ripples are * pruned during draw(). The locations of these will not change. */ - private Ripple[] mExitingRipples; + private RippleForeground[] mExitingRipples; private int mExitingRipplesCount = 0; /** Paint used to control appearance of ripples. */ @@ -204,11 +204,11 @@ public class RippleDrawable extends LayerDrawable { super.jumpToCurrentState(); if (mRipple != null) { - mRipple.jump(); + mRipple.end(); } if (mBackground != null) { - mBackground.jump(); + mBackground.end(); } cancelExitingRipples(); @@ -219,10 +219,13 @@ public class RippleDrawable extends LayerDrawable { boolean needsDraw = false; final int count = mExitingRipplesCount; - final Ripple[] ripples = mExitingRipples; + final RippleForeground[] ripples = mExitingRipples; for (int i = 0; i < count; i++) { + // If the ripple is animating on the hardware thread, we'll need to + // draw an additional frame after canceling to restore the software + // drawing path. needsDraw |= ripples[i].isHardwareAnimating(); - ripples[i].cancel(); + ripples[i].end(); } if (ripples != null) { @@ -264,11 +267,9 @@ public class RippleDrawable extends LayerDrawable { for (int state : stateSet) { if (state == R.attr.state_enabled) { enabled = true; - } - if (state == R.attr.state_focused) { + } else if (state == R.attr.state_focused) { focused = true; - } - if (state == R.attr.state_pressed) { + } else if (state == R.attr.state_pressed) { pressed = true; } } @@ -563,11 +564,11 @@ public class RippleDrawable extends LayerDrawable { x = mHotspotBounds.exactCenterX(); y = mHotspotBounds.exactCenterY(); } - mRipple = new Ripple(this, mHotspotBounds, x, y); + mRipple = new RippleForeground(this, mHotspotBounds, x, y); } mRipple.setup(mState.mMaxRadius, mDensity); - mRipple.enter(); + mRipple.enter(false); } /** @@ -577,7 +578,7 @@ public class RippleDrawable extends LayerDrawable { private void tryRippleExit() { if (mRipple != null) { if (mExitingRipples == null) { - mExitingRipples = new Ripple[MAX_RIPPLES]; + mExitingRipples = new RippleForeground[MAX_RIPPLES]; } mExitingRipples[mExitingRipplesCount++] = mRipple; mRipple.exit(); @@ -591,13 +592,13 @@ public class RippleDrawable extends LayerDrawable { */ private void clearHotspots() { if (mRipple != null) { - mRipple.cancel(); + mRipple.end(); mRipple = null; mRippleActive = false; } if (mBackground != null) { - mBackground.cancel(); + mBackground.end(); mBackground = null; mBackgroundActive = false; } @@ -624,7 +625,7 @@ public class RippleDrawable extends LayerDrawable { */ private void onHotspotBoundsChanged() { final int count = mExitingRipplesCount; - final Ripple[] ripples = mExitingRipples; + final RippleForeground[] ripples = mExitingRipples; for (int i = 0; i < count; i++) { ripples[i].onHotspotBoundsChanged(); } @@ -662,6 +663,8 @@ public class RippleDrawable extends LayerDrawable { */ @Override public void draw(@NonNull Canvas canvas) { + pruneRipples(); + // Clip to the dirty bounds, which will be the drawable bounds if we // have a mask or content and the ripple bounds if we're projecting. final Rect bounds = getDirtyBounds(); @@ -682,6 +685,26 @@ public class RippleDrawable extends LayerDrawable { mHasValidMask = false; } + private void pruneRipples() { + int remaining = 0; + + // Move remaining entries into pruned spaces. + final RippleForeground[] ripples = mExitingRipples; + final int count = mExitingRipplesCount; + for (int i = 0; i < count; i++) { + if (!ripples[i].hasFinishedExit()) { + ripples[remaining++] = ripples[i]; + } + } + + // Null out the remaining entries. + for (int i = remaining; i < count; i++) { + ripples[i] = null; + } + + mExitingRipplesCount = remaining; + } + /** * @return whether we need to use a mask */ @@ -747,7 +770,7 @@ public class RippleDrawable extends LayerDrawable { private int getMaskType() { if (mRipple == null && mExitingRipplesCount <= 0 - && (mBackground == null || !mBackground.shouldDraw())) { + && (mBackground == null || !mBackground.isVisible())) { // We might need a mask later. return MASK_UNKNOWN; } @@ -774,36 +797,6 @@ public class RippleDrawable extends LayerDrawable { return MASK_NONE; } - /** - * Removes a ripple from the exiting ripple list. - * - * @param ripple the ripple to remove - */ - void removeRipple(Ripple ripple) { - // Ripple ripple ripple ripple. Ripple ripple. - final Ripple[] ripples = mExitingRipples; - final int count = mExitingRipplesCount; - final int index = getRippleIndex(ripple); - if (index >= 0) { - System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1)); - ripples[count - 1] = null; - mExitingRipplesCount--; - - invalidateSelf(); - } - } - - private int getRippleIndex(Ripple ripple) { - final Ripple[] ripples = mExitingRipples; - final int count = mExitingRipplesCount; - for (int i = 0; i < count; i++) { - if (ripples[i] == ripple) { - return i; - } - } - return -1; - } - private void drawContent(Canvas canvas) { // Draw everything except the mask. final ChildDrawable[] array = mLayerState.mChildren; @@ -816,10 +809,10 @@ public class RippleDrawable extends LayerDrawable { } private void drawBackgroundAndRipples(Canvas canvas) { - final Ripple active = mRipple; + final RippleForeground active = mRipple; final RippleBackground background = mBackground; final int count = mExitingRipplesCount; - if (active == null && count <= 0 && (background == null || !background.shouldDraw())) { + if (active == null && count <= 0 && (background == null || !background.isVisible())) { // Move along, nothing to draw here. return; } @@ -859,12 +852,12 @@ public class RippleDrawable extends LayerDrawable { p.setShader(null); } - if (background != null && background.shouldDraw()) { + if (background != null && background.isVisible()) { background.draw(canvas, p); } if (count > 0) { - final Ripple[] ripples = mExitingRipples; + final RippleForeground[] ripples = mExitingRipples; for (int i = 0; i < count; i++) { ripples[i].draw(canvas, p); } @@ -902,7 +895,7 @@ public class RippleDrawable extends LayerDrawable { final int cY = (int) mHotspotBounds.exactCenterY(); final Rect rippleBounds = mTempRect; - final Ripple[] activeRipples = mExitingRipples; + final RippleForeground[] activeRipples = mExitingRipples; final int N = mExitingRipplesCount; for (int i = 0; i < N; i++) { activeRipples[i].getBounds(rippleBounds); diff --git a/graphics/java/android/graphics/drawable/RippleForeground.java b/graphics/java/android/graphics/drawable/RippleForeground.java new file mode 100644 index 000000000000..2023f04c92e4 --- /dev/null +++ b/graphics/java/android/graphics/drawable/RippleForeground.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2015 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.graphics.drawable; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.graphics.Canvas; +import android.graphics.CanvasProperty; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.FloatProperty; +import android.util.MathUtils; +import android.view.HardwareCanvas; +import android.view.RenderNodeAnimator; +import android.view.animation.LinearInterpolator; + +/** + * Draws a ripple foreground. + */ +class RippleForeground extends RippleComponent { + private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator( + 400f, 1.4f, 0); + + // Pixel-based accelerations and velocities. + private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024; + private static final float WAVE_TOUCH_UP_ACCELERATION = 3400; + private static final float WAVE_OPACITY_DECAY_VELOCITY = 3; + + private static final int RIPPLE_ENTER_DELAY = 80; + private static final int OPACITY_ENTER_DURATION_FAST = 120; + + private float mStartingX; + private float mStartingY; + private float mClampedStartingX; + private float mClampedStartingY; + + // Hardware rendering properties. + private CanvasProperty<Paint> mPropPaint; + private CanvasProperty<Float> mPropRadius; + private CanvasProperty<Float> mPropX; + private CanvasProperty<Float> mPropY; + + // Software rendering properties. + private float mOpacity = 1; + private float mOuterX; + private float mOuterY; + + // Values used to tween between the start and end positions. + private float mTweenRadius = 0; + private float mTweenX = 0; + private float mTweenY = 0; + + /** Whether this ripple has finished its exit animation. */ + private boolean mHasFinishedExit; + + public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY) { + super(owner, bounds); + + mStartingX = startingX; + mStartingY = startingY; + } + + @Override + public void onSetup() { + mOuterX = 0; + mOuterY = 0; + } + + @Override + protected void onTargetRadiusChanged(float targetRadius) { + clampStartingPosition(); + } + + @Override + protected boolean drawSoftware(Canvas c, Paint p) { + boolean hasContent = false; + + final int origAlpha = p.getAlpha(); + final int alpha = (int) (origAlpha * mOpacity + 0.5f); + final float radius = MathUtils.lerp(0, mTargetRadius, mTweenRadius); + if (alpha > 0 && radius > 0) { + final float x = MathUtils.lerp( + mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); + final float y = MathUtils.lerp( + mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); + p.setAlpha(alpha); + c.drawCircle(x, y, radius, p); + p.setAlpha(origAlpha); + hasContent = true; + } + + return hasContent; + } + + @Override + protected boolean drawHardware(HardwareCanvas c) { + c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); + return true; + } + + /** + * Returns the maximum bounds of the ripple relative to the ripple center. + */ + public void getBounds(Rect bounds) { + final int outerX = (int) mOuterX; + final int outerY = (int) mOuterY; + final int r = (int) mTargetRadius + 1; + bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); + } + + /** + * Specifies the starting position relative to the drawable bounds. No-op if + * the ripple has already entered. + */ + public void move(float x, float y) { + mStartingX = x; + mStartingY = y; + + clampStartingPosition(); + } + + /** + * @return {@code true} if this ripple has finished its exit animation + */ + public boolean hasFinishedExit() { + return mHasFinishedExit; + } + + @Override + protected Animator createSoftwareEnter(boolean fast) { + final int duration = (int) + (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5); + + final ObjectAnimator tweenAll = ObjectAnimator.ofFloat(this, TWEEN_ALL, 1); + tweenAll.setAutoCancel(true); + tweenAll.setDuration(duration); + tweenAll.setInterpolator(LINEAR_INTERPOLATOR); + tweenAll.setStartDelay(RIPPLE_ENTER_DELAY); + + final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); + opacity.setAutoCancel(true); + opacity.setDuration(OPACITY_ENTER_DURATION_FAST); + opacity.setInterpolator(LINEAR_INTERPOLATOR); + + final AnimatorSet set = new AnimatorSet(); + set.play(tweenAll).with(opacity); + + return set; + } + + private int getRadiusExitDuration() { + final float radius = MathUtils.lerp(0, mTargetRadius, mTweenRadius); + final float remaining = mTargetRadius - radius; + return (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION + + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5); + } + + private int getOpacityExitDuration() { + return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); + } + + @Override + protected Animator createSoftwareExit() { + final int radiusDuration = getRadiusExitDuration(); + final int opacityDuration = getOpacityExitDuration(); + + final ObjectAnimator tweenAll = ObjectAnimator.ofFloat(this, TWEEN_ALL, 1); + tweenAll.setAutoCancel(true); + tweenAll.setDuration(radiusDuration); + tweenAll.setInterpolator(DECELERATE_INTERPOLATOR); + + final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0); + opacity.setAutoCancel(true); + opacity.setDuration(opacityDuration); + opacity.setInterpolator(LINEAR_INTERPOLATOR); + + final AnimatorSet set = new AnimatorSet(); + set.play(tweenAll).with(opacity); + set.addListener(mAnimationListener); + + return set; + } + + @Override + protected RenderNodeAnimatorSet createHardwareExit(Paint p) { + final int radiusDuration = getRadiusExitDuration(); + final int opacityDuration = getOpacityExitDuration(); + + final float startX = MathUtils.lerp( + mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); + final float startY = MathUtils.lerp( + mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); + + final float startRadius = MathUtils.lerp(0, mTargetRadius, mTweenRadius); + p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f)); + + mPropPaint = CanvasProperty.createPaint(p); + mPropRadius = CanvasProperty.createFloat(startRadius); + mPropX = CanvasProperty.createFloat(startX); + mPropY = CanvasProperty.createFloat(startY); + + final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius); + radius.setDuration(radiusDuration); + radius.setInterpolator(DECELERATE_INTERPOLATOR); + + final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mOuterX); + x.setDuration(radiusDuration); + x.setInterpolator(DECELERATE_INTERPOLATOR); + + final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mOuterY); + y.setDuration(radiusDuration); + y.setInterpolator(DECELERATE_INTERPOLATOR); + + final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, + RenderNodeAnimator.PAINT_ALPHA, 0); + opacity.setDuration(opacityDuration); + opacity.setInterpolator(LINEAR_INTERPOLATOR); + opacity.addListener(mAnimationListener); + + final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); + set.add(radius); + set.add(opacity); + set.add(x); + set.add(y); + + return set; + } + + @Override + protected void jumpValuesToExit() { + mOpacity = 0; + mTweenX = 1; + mTweenY = 1; + mTweenRadius = 1; + } + + /** + * Clamps the starting position to fit within the ripple bounds. + */ + private void clampStartingPosition() { + final float cX = mBounds.exactCenterX(); + final float cY = mBounds.exactCenterY(); + final float dX = mStartingX - cX; + final float dY = mStartingY - cY; + final float r = mTargetRadius; + if (dX * dX + dY * dY > r * r) { + // Point is outside the circle, clamp to the perimeter. + final double angle = Math.atan2(dY, dX); + mClampedStartingX = cX + (float) (Math.cos(angle) * r); + mClampedStartingY = cY + (float) (Math.sin(angle) * r); + } else { + mClampedStartingX = mStartingX; + mClampedStartingY = mStartingY; + } + } + + private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mHasFinishedExit = true; + } + }; + + /** + * Interpolator with a smooth log deceleration. + */ + private static final class LogDecelerateInterpolator implements TimeInterpolator { + private final float mBase; + private final float mDrift; + private final float mTimeScale; + private final float mOutputScale; + + public LogDecelerateInterpolator(float base, float timeScale, float drift) { + mBase = base; + mDrift = drift; + mTimeScale = 1f / timeScale; + + mOutputScale = 1f / computeLog(1f); + } + + private float computeLog(float t) { + return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t); + } + + @Override + public float getInterpolation(float t) { + return computeLog(t) * mOutputScale; + } + } + + /** + * Property for animating radius, center X, and center Y between their + * initial and target values. + */ + private static final FloatProperty<RippleForeground> TWEEN_ALL = + new FloatProperty<RippleForeground>("tweenAll") { + @Override + public void setValue(RippleForeground object, float value) { + object.mTweenRadius = value; + object.mTweenX = value; + object.mTweenY = value; + object.invalidateSelf(); + } + + @Override + public Float get(RippleForeground object) { + return object.mTweenRadius; + } + }; + + /** + * Property for animating opacity between 0 and its target value. + */ + private static final FloatProperty<RippleForeground> OPACITY = + new FloatProperty<RippleForeground>("opacity") { + @Override + public void setValue(RippleForeground object, float value) { + object.mOpacity = value; + object.invalidateSelf(); + } + + @Override + public Float get(RippleForeground object) { + return object.mOpacity; + } + }; +} diff --git a/include/android_runtime/android_view_Surface.h b/include/android_runtime/android_view_Surface.h index 53e8b4970c8d..a6836a8bed2d 100644 --- a/include/android_runtime/android_view_Surface.h +++ b/include/android_runtime/android_view_Surface.h @@ -26,6 +26,33 @@ namespace android { class Surface; class IGraphicBufferProducer; +/** + * Enum mirroring the public API definitions for image and pixel formats. + * Some of these are hidden in the public API + * + * Keep up to date with android.graphics.ImageFormat and + * android.graphics.PixelFormat + */ +enum class PublicFormat { + UNKNOWN = 0x0, + RGBA_8888 = 0x1, + RGBX_8888 = 0x2, + RGB_888 = 0x3, + RGB_565 = 0x4, + NV16 = 0x10, + NV21 = 0x11, + YUY2 = 0x14, + RAW_SENSOR = 0x20, + YUV_420_888 = 0x23, + RAW10 = 0x25, + JPEG = 0x100, + DEPTH_POINT_CLOUD = 0x101, + YV12 = 0x32315659, + Y8 = 0x20203859, // @hide + Y16 = 0x20363159, // @hide + DEPTH16 = 0x44363159 +}; + /* Gets the underlying ANativeWindow for a Surface. */ extern sp<ANativeWindow> android_view_Surface_getNativeWindow( JNIEnv* env, jobject surfaceObj); @@ -40,6 +67,21 @@ extern sp<Surface> android_view_Surface_getSurface(JNIEnv* env, jobject surfaceO extern jobject android_view_Surface_createFromIGraphicBufferProducer(JNIEnv* env, const sp<IGraphicBufferProducer>& bufferProducer); +/* Convert from android.graphics.ImageFormat/PixelFormat enums to graphics.h HAL + * format */ +extern int android_view_Surface_mapPublicFormatToHalFormat(PublicFormat f); + +/* Convert from android.graphics.ImageFormat/PixelFormat enums to graphics.h HAL + * dataspace */ +extern android_dataspace android_view_Surface_mapPublicFormatToHalDataspace( + PublicFormat f); + +/* Convert from HAL format, dataspace pair to + * android.graphics.ImageFormat/PixelFormat. + * For unknown/unspecified pairs, returns PublicFormat::UNKNOWN */ +extern PublicFormat android_view_Surface_mapHalFormatDataspaceToPublicFormat( + int format, android_dataspace dataSpace); + } // namespace android #endif // _ANDROID_VIEW_SURFACE_H diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index 09f4bac8dbae..452683942ab5 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -279,6 +279,7 @@ public final class AudioAttributes implements Parcelable { * Internal use only * @return a combined mask of all flags */ + @SystemApi public int getAllFlags() { return (mFlags & FLAG_ALL); } @@ -541,14 +542,15 @@ public final class AudioAttributes implements Parcelable { /** * @hide * Same as {@link #setCapturePreset(int)} but authorizes the use of HOTWORD, - * REMOTE_SUBMIX and FM_TUNER. + * REMOTE_SUBMIX and RADIO_TUNER. * @param preset * @return the same Builder instance. */ + @SystemApi public Builder setInternalCapturePreset(int preset) { if ((preset == MediaRecorder.AudioSource.HOTWORD) || (preset == MediaRecorder.AudioSource.REMOTE_SUBMIX) - || (preset == MediaRecorder.AudioSource.FM_TUNER)) { + || (preset == MediaRecorder.AudioSource.RADIO_TUNER)) { mSource = preset; } else { setCapturePreset(preset); diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index de10ef9abef9..259fe37884e6 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -20,6 +20,7 @@ import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.Iterator; +import android.annotation.SystemApi; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -238,7 +239,6 @@ public class AudioRecord /** * @hide - * CANDIDATE FOR PUBLIC API * Class constructor with {@link AudioAttributes} and {@link AudioFormat}. * @param attributes a non-null {@link AudioAttributes} instance. Use * {@link AudioAttributes.Builder#setCapturePreset(int)} for configuring the capture @@ -257,6 +257,7 @@ public class AudioRecord * construction. * @throws IllegalArgumentException */ + @SystemApi public AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int sessionId) throws IllegalArgumentException { mRecordingState = RECORDSTATE_STOPPED; @@ -376,7 +377,7 @@ public class AudioRecord // audio source if ( (audioSource < MediaRecorder.AudioSource.DEFAULT) || ((audioSource > MediaRecorder.getAudioSourceMax()) && - (audioSource != MediaRecorder.AudioSource.FM_TUNER) && + (audioSource != MediaRecorder.AudioSource.RADIO_TUNER) && (audioSource != MediaRecorder.AudioSource.HOTWORD)) ) { throw new IllegalArgumentException("Invalid audio source."); } diff --git a/media/java/android/media/ImageReader.java b/media/java/android/media/ImageReader.java index 8d6a5883c6c4..824a7adcf057 100644 --- a/media/java/android/media/ImageReader.java +++ b/media/java/android/media/ImageReader.java @@ -483,6 +483,8 @@ public class ImageReader implements AutoCloseable { case ImageFormat.Y16: case ImageFormat.RAW_SENSOR: case ImageFormat.RAW10: + case ImageFormat.DEPTH16: + case ImageFormat.DEPTH_POINT_CLOUD: return 1; default: throw new UnsupportedOperationException( diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java index 97b3f6336bf3..58c86f20605a 100644 --- a/media/java/android/media/MediaRecorder.java +++ b/media/java/android/media/MediaRecorder.java @@ -16,6 +16,7 @@ package android.media; +import android.annotation.SystemApi; import android.app.ActivityThread; import android.hardware.Camera; import android.os.Handler; @@ -222,12 +223,11 @@ public class MediaRecorder public static final int REMOTE_SUBMIX = 8; /** - * Audio source for FM, which is used to capture current FM tuner output by FMRadio app. - * There are two use cases, one is for record FM stream for later listening, another is - * for FM indirect mode(the routing except FM to headset(headphone) device routing). + * Audio source for capturing broadcast radio tuner output. * @hide */ - public static final int FM_TUNER = 1998; + @SystemApi + public static final int RADIO_TUNER = 1998; /** * Audio source for preemptible, low-priority software hotword detection @@ -240,7 +240,8 @@ public class MediaRecorder * This is a hidden audio source. * @hide */ - protected static final int HOTWORD = 1999; + @SystemApi + public static final int HOTWORD = 1999; } /** diff --git a/media/java/android/media/midi/MidiDevice.java b/media/java/android/media/midi/MidiDevice.java index 1a39485b5e47..af0737da516f 100644 --- a/media/java/android/media/midi/MidiDevice.java +++ b/media/java/android/media/midi/MidiDevice.java @@ -24,11 +24,13 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; +import dalvik.system.CloseGuard; + import java.io.Closeable; import java.io.IOException; /** - * This class is used for sending and receiving data to and from an MIDI device + * This class is used for sending and receiving data to and from a MIDI device * Instances of this class are created by {@link MidiManager#openDevice}. * * CANDIDATE FOR PUBLIC API @@ -42,11 +44,10 @@ public final class MidiDevice implements Closeable { private Context mContext; private ServiceConnection mServiceConnection; + private final CloseGuard mGuard = CloseGuard.get(); + /* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server) { - mDeviceInfo = deviceInfo; - mDeviceServer = server; - mContext = null; - mServiceConnection = null; + this(deviceInfo, server, null, null); } /* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server, @@ -55,6 +56,7 @@ public final class MidiDevice implements Closeable { mDeviceServer = server; mContext = context; mServiceConnection = serviceConnection; + mGuard.open("close"); } /** @@ -108,10 +110,23 @@ public final class MidiDevice implements Closeable { @Override public void close() throws IOException { - if (mContext != null && mServiceConnection != null) { - mContext.unbindService(mServiceConnection); - mContext = null; - mServiceConnection = null; + synchronized (mGuard) { + mGuard.close(); + if (mContext != null && mServiceConnection != null) { + mContext.unbindService(mServiceConnection); + mContext = null; + mServiceConnection = null; + } + } + } + + @Override + protected void finalize() throws Throwable { + try { + mGuard.warnIfOpen(); + close(); + } finally { + super.finalize(); } } diff --git a/media/java/android/media/midi/MidiDeviceInfo.java b/media/java/android/media/midi/MidiDeviceInfo.java index b7756fdb6f17..1e97258fe37a 100644 --- a/media/java/android/media/midi/MidiDeviceInfo.java +++ b/media/java/android/media/midi/MidiDeviceInfo.java @@ -31,7 +31,7 @@ import android.os.Parcelable; * CANDIDATE FOR PUBLIC API * @hide */ -public class MidiDeviceInfo implements Parcelable { +public final class MidiDeviceInfo implements Parcelable { private static final String TAG = "MidiDeviceInfo"; diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java index 4d59c6395faf..3b4b6f0f0283 100644 --- a/media/java/android/media/midi/MidiDeviceServer.java +++ b/media/java/android/media/midi/MidiDeviceServer.java @@ -24,11 +24,14 @@ import android.os.RemoteException; import android.system.OsConstants; import android.util.Log; +import dalvik.system.CloseGuard; + import libcore.io.IoUtils; import java.io.Closeable; import java.io.IOException; import java.util.HashMap; +import java.util.concurrent.CopyOnWriteArrayList; /** * Internal class used for providing an implementation for a MIDI device. @@ -54,6 +57,12 @@ public final class MidiDeviceServer implements Closeable { // MidiOutputPorts for clients connected to our input ports private final MidiOutputPort[] mInputPortOutputPorts; + // List of all MidiInputPorts we created + private final CopyOnWriteArrayList<MidiInputPort> mInputPorts + = new CopyOnWriteArrayList<MidiInputPort>(); + + private final CloseGuard mGuard = CloseGuard.get(); + abstract private class PortClient implements IBinder.DeathRecipient { final IBinder mToken; @@ -105,6 +114,7 @@ public final class MidiDeviceServer implements Closeable { void close() { mToken.unlinkToDeath(this, 0); mOutputPortDispatchers[mInputPort.getPortNumber()].getSender().disconnect(mInputPort); + mInputPorts.remove(mInputPort); IoUtils.closeQuietly(mInputPort); } } @@ -169,6 +179,7 @@ public final class MidiDeviceServer implements Closeable { OsConstants.SOCK_SEQPACKET); MidiInputPort inputPort = new MidiInputPort(pair[0], portNumber); mOutputPortDispatchers[portNumber].getSender().connect(inputPort); + mInputPorts.add(inputPort); OutputPortClient client = new OutputPortClient(token, inputPort); synchronized (mPortClients) { mPortClients.put(token, client); @@ -204,6 +215,8 @@ public final class MidiDeviceServer implements Closeable { for (int i = 0; i < numOutputPorts; i++) { mOutputPortDispatchers[i] = new MidiDispatcher(); } + + mGuard.open("close"); } /* package */ IMidiDeviceServer getBinderInterface() { @@ -219,11 +232,35 @@ public final class MidiDeviceServer implements Closeable { @Override public void close() throws IOException { + synchronized (mGuard) { + mGuard.close(); + + for (int i = 0; i < mInputPortCount; i++) { + MidiOutputPort outputPort = mInputPortOutputPorts[i]; + if (outputPort != null) { + IoUtils.closeQuietly(outputPort); + mInputPortOutputPorts[i] = null; + } + } + for (MidiInputPort inputPort : mInputPorts) { + IoUtils.closeQuietly(inputPort); + } + mInputPorts.clear(); + try { + mMidiManager.unregisterDeviceServer(mServer); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in unregisterDeviceServer"); + } + } + } + + @Override + protected void finalize() throws Throwable { try { - // FIXME - close input and output ports too? - mMidiManager.unregisterDeviceServer(mServer); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in unregisterDeviceServer"); + mGuard.warnIfOpen(); + close(); + } finally { + super.finalize(); } } diff --git a/media/java/android/media/midi/MidiDispatcher.java b/media/java/android/media/midi/MidiDispatcher.java index b2d8b6fd01e6..a5193f10aad5 100644 --- a/media/java/android/media/midi/MidiDispatcher.java +++ b/media/java/android/media/midi/MidiDispatcher.java @@ -17,7 +17,7 @@ package android.media.midi; import java.io.IOException; -import java.util.ArrayList; +import java.util.concurrent.CopyOnWriteArrayList; /** * Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s. @@ -29,9 +29,10 @@ import java.util.ArrayList; * CANDIDATE FOR PUBLIC API * @hide */ -public class MidiDispatcher extends MidiReceiver { +public final class MidiDispatcher extends MidiReceiver { - private final ArrayList<MidiReceiver> mReceivers = new ArrayList<MidiReceiver>(); + private final CopyOnWriteArrayList<MidiReceiver> mReceivers + = new CopyOnWriteArrayList<MidiReceiver>(); private final MidiSender mSender = new MidiSender() { /** @@ -72,17 +73,12 @@ public class MidiDispatcher extends MidiReceiver { @Override public void receive(byte[] msg, int offset, int count, long timestamp) throws IOException { - synchronized (mReceivers) { - for (int i = 0; i < mReceivers.size(); ) { - MidiReceiver receiver = mReceivers.get(i); - try { - receiver.receive(msg, offset, count, timestamp); - i++; // increment only on success. on failure we remove the receiver - // so i should not be incremented - } catch (IOException e) { - // if the receiver fails we remove the receiver but do not propogate the exception - mSender.disconnect(receiver); - } + for (MidiReceiver receiver : mReceivers) { + try { + receiver.receive(msg, offset, count, timestamp); + } catch (IOException e) { + // if the receiver fails we remove the receiver but do not propogate the exception + mReceivers.remove(receiver); } } } diff --git a/media/java/android/media/midi/MidiInputPort.java b/media/java/android/media/midi/MidiInputPort.java index 5d944cb30da7..86749699bf81 100644 --- a/media/java/android/media/midi/MidiInputPort.java +++ b/media/java/android/media/midi/MidiInputPort.java @@ -35,15 +35,16 @@ import java.io.IOException; * CANDIDATE FOR PUBLIC API * @hide */ -public class MidiInputPort extends MidiReceiver implements Closeable { +public final class MidiInputPort extends MidiReceiver implements Closeable { private static final String TAG = "MidiInputPort"; - private final IMidiDeviceServer mDeviceServer; + private IMidiDeviceServer mDeviceServer; private final IBinder mToken; private final int mPortNumber; private final FileOutputStream mOutputStream; private final CloseGuard mGuard = CloseGuard.get(); + private boolean mIsClosed; // buffer to use for sending data out our output stream private final byte[] mBuffer = new byte[MidiPortImpl.MAX_PACKET_SIZE]; @@ -97,23 +98,27 @@ public class MidiInputPort extends MidiReceiver implements Closeable { @Override public void close() throws IOException { - mGuard.close(); - mOutputStream.close(); - if (mDeviceServer != null) { - try { - mDeviceServer.closePort(mToken); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in MidiInputPort.close()"); + synchronized (mGuard) { + if (mIsClosed) return; + mGuard.close(); + mOutputStream.close(); + if (mDeviceServer != null) { + try { + mDeviceServer.closePort(mToken); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in MidiInputPort.close()"); + } } + mIsClosed = true; } } @Override protected void finalize() throws Throwable { try { - if (mGuard != null) { - mGuard.warnIfOpen(); - } + mGuard.warnIfOpen(); + // not safe to make binder calls from finalize() + mDeviceServer = null; close(); } finally { super.finalize(); diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java index 08ac25a145db..d7b8c5702bd8 100644 --- a/media/java/android/media/midi/MidiManager.java +++ b/media/java/android/media/midi/MidiManager.java @@ -42,7 +42,7 @@ import java.util.HashMap; * CANDIDATE FOR PUBLIC API * @hide */ -public class MidiManager { +public final class MidiManager { private static final String TAG = "MidiManager"; private final Context mContext; diff --git a/media/java/android/media/midi/MidiOutputPort.java b/media/java/android/media/midi/MidiOutputPort.java index d46b2028fbb1..b31cdd37bb5f 100644 --- a/media/java/android/media/midi/MidiOutputPort.java +++ b/media/java/android/media/midi/MidiOutputPort.java @@ -35,16 +35,17 @@ import java.io.IOException; * CANDIDATE FOR PUBLIC API * @hide */ -public class MidiOutputPort extends MidiSender implements Closeable { +public final class MidiOutputPort extends MidiSender implements Closeable { private static final String TAG = "MidiOutputPort"; - private final IMidiDeviceServer mDeviceServer; + private IMidiDeviceServer mDeviceServer; private final IBinder mToken; private final int mPortNumber; private final FileInputStream mInputStream; private final MidiDispatcher mDispatcher = new MidiDispatcher(); private final CloseGuard mGuard = CloseGuard.get(); + private boolean mIsClosed; // This thread reads MIDI events from a socket and distributes them to the list of // MidiReceivers attached to this device. @@ -113,23 +114,28 @@ public class MidiOutputPort extends MidiSender implements Closeable { @Override public void close() throws IOException { - mGuard.close(); - mInputStream.close(); - if (mDeviceServer != null) { - try { - mDeviceServer.closePort(mToken); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in MidiOutputPort.close()"); + synchronized (mGuard) { + if (mIsClosed) return; + + mGuard.close(); + mInputStream.close(); + if (mDeviceServer != null) { + try { + mDeviceServer.closePort(mToken); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in MidiOutputPort.close()"); + } } + mIsClosed = true; } } @Override protected void finalize() throws Throwable { try { - if (mGuard != null) { - mGuard.warnIfOpen(); - } + mGuard.warnIfOpen(); + // not safe to make binder calls from finalize() + mDeviceServer = null; close(); } finally { super.finalize(); diff --git a/media/jni/android_media_ImageReader.cpp b/media/jni/android_media_ImageReader.cpp index cf69b8f829b0..b2474931e9c6 100644 --- a/media/jni/android_media_ImageReader.cpp +++ b/media/jni/android_media_ImageReader.cpp @@ -95,6 +95,9 @@ public: void setBufferFormat(int format) { mFormat = format; } int getBufferFormat() { return mFormat; } + void setBufferDataspace(android_dataspace dataSpace) { mDataSpace = dataSpace; } + android_dataspace getBufferDataspace() { return mDataSpace; } + void setBufferWidth(int width) { mWidth = width; } int getBufferWidth() { return mWidth; } @@ -111,6 +114,7 @@ private: jobject mWeakThiz; jclass mClazz; int mFormat; + android_dataspace mDataSpace; int mWidth; int mHeight; }; @@ -263,29 +267,6 @@ static void Image_setBuffer(JNIEnv* env, jobject thiz, env->SetLongField(thiz, gSurfaceImageClassInfo.mLockedBuffer, reinterpret_cast<jlong>(buffer)); } -// Some formats like JPEG defined with different values between android.graphics.ImageFormat and -// graphics.h, need convert to the one defined in graphics.h here. -static int Image_getPixelFormat(JNIEnv* env, int format) -{ - int jpegFormat; - jfieldID fid; - - ALOGV("%s: format = 0x%x", __FUNCTION__, format); - - jclass imageFormatClazz = env->FindClass("android/graphics/ImageFormat"); - ALOG_ASSERT(imageFormatClazz != NULL); - - fid = env->GetStaticFieldID(imageFormatClazz, "JPEG", "I"); - jpegFormat = env->GetStaticIntField(imageFormatClazz, fid); - - // Translate the JPEG to BLOB for camera purpose. - if (format == jpegFormat) { - format = HAL_PIXEL_FORMAT_BLOB; - } - - return format; -} - static uint32_t Image_getJpegSize(CpuConsumer::LockedBuffer* buffer, bool usingRGBAOverride) { ALOG_ASSERT(buffer != NULL, "Input buffer is NULL!!!"); @@ -483,7 +464,7 @@ static void Image_getLockedBufferInfo(JNIEnv* env, CpuConsumer::LockedBuffer* bu } static jint Image_imageGetPixelStride(JNIEnv* env, CpuConsumer::LockedBuffer* buffer, int idx, - int32_t readerFormat) + int32_t halReaderFormat) { ALOGV("%s: buffer index: %d", __FUNCTION__, idx); ALOG_ASSERT((idx < IMAGE_READER_MAX_NUM_PLANES) && (idx >= 0), "Index is out of range:%d", idx); @@ -493,7 +474,7 @@ static jint Image_imageGetPixelStride(JNIEnv* env, CpuConsumer::LockedBuffer* bu int32_t fmt = buffer->flexFormat; - fmt = applyFormatOverrides(fmt, readerFormat); + fmt = applyFormatOverrides(fmt, halReaderFormat); switch (fmt) { case HAL_PIXEL_FORMAT_YCbCr_420_888: @@ -543,7 +524,7 @@ static jint Image_imageGetPixelStride(JNIEnv* env, CpuConsumer::LockedBuffer* bu } static jint Image_imageGetRowStride(JNIEnv* env, CpuConsumer::LockedBuffer* buffer, int idx, - int32_t readerFormat) + int32_t halReaderFormat) { ALOGV("%s: buffer index: %d", __FUNCTION__, idx); ALOG_ASSERT((idx < IMAGE_READER_MAX_NUM_PLANES) && (idx >= 0)); @@ -553,7 +534,7 @@ static jint Image_imageGetRowStride(JNIEnv* env, CpuConsumer::LockedBuffer* buff int32_t fmt = buffer->flexFormat; - fmt = applyFormatOverrides(fmt, readerFormat); + fmt = applyFormatOverrides(fmt, halReaderFormat); switch (fmt) { case HAL_PIXEL_FORMAT_YCbCr_420_888: @@ -682,11 +663,16 @@ static void ImageReader_init(JNIEnv* env, jobject thiz, jobject weakThiz, { status_t res; int nativeFormat; + android_dataspace nativeDataspace; ALOGV("%s: width:%d, height: %d, format: 0x%x, maxImages:%d", __FUNCTION__, width, height, format, maxImages); - nativeFormat = Image_getPixelFormat(env, format); + PublicFormat publicFormat = static_cast<PublicFormat>(format); + nativeFormat = android_view_Surface_mapPublicFormatToHalFormat( + publicFormat); + nativeDataspace = android_view_Surface_mapPublicFormatToHalDataspace( + publicFormat); sp<IGraphicBufferProducer> gbProducer; sp<IGraphicBufferConsumer> gbConsumer; @@ -710,10 +696,11 @@ static void ImageReader_init(JNIEnv* env, jobject thiz, jobject weakThiz, consumer->setFrameAvailableListener(ctx); ImageReader_setNativeContext(env, thiz, ctx); ctx->setBufferFormat(nativeFormat); + ctx->setBufferDataspace(nativeDataspace); ctx->setBufferWidth(width); ctx->setBufferHeight(height); - // Set the width/height/format to the CpuConsumer + // Set the width/height/format/dataspace to the CpuConsumer res = consumer->setDefaultBufferSize(width, height); if (res != OK) { jniThrowException(env, "java/lang/IllegalStateException", @@ -725,6 +712,12 @@ static void ImageReader_init(JNIEnv* env, jobject thiz, jobject weakThiz, jniThrowException(env, "java/lang/IllegalStateException", "Failed to set CpuConsumer buffer format"); } + res = consumer->setDefaultBufferDataSpace(nativeDataspace); + if (res != OK) { + jniThrowException(env, "java/lang/IllegalStateException", + "Failed to set CpuConsumer buffer dataSpace"); + } + } static void ImageReader_close(JNIEnv* env, jobject thiz) @@ -884,6 +877,8 @@ static jobject ImageReader_getSurface(JNIEnv* env, jobject thiz) static jobject Image_createSurfacePlane(JNIEnv* env, jobject thiz, int idx, int readerFormat) { int rowStride, pixelStride; + PublicFormat publicReaderFormat = static_cast<PublicFormat>(readerFormat); + ALOGV("%s: buffer index: %d", __FUNCTION__, idx); CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, thiz); @@ -893,10 +888,11 @@ static jobject Image_createSurfacePlane(JNIEnv* env, jobject thiz, int idx, int jniThrowException(env, "java/lang/IllegalStateException", "Image was released"); } - readerFormat = Image_getPixelFormat(env, readerFormat); + int halReaderFormat = android_view_Surface_mapPublicFormatToHalFormat( + publicReaderFormat); - rowStride = Image_imageGetRowStride(env, buffer, idx, readerFormat); - pixelStride = Image_imageGetPixelStride(env, buffer, idx, readerFormat); + rowStride = Image_imageGetRowStride(env, buffer, idx, halReaderFormat); + pixelStride = Image_imageGetPixelStride(env, buffer, idx, halReaderFormat); jobject surfPlaneObj = env->NewObject(gSurfacePlaneClassInfo.clazz, gSurfacePlaneClassInfo.ctor, thiz, idx, rowStride, pixelStride); @@ -909,6 +905,7 @@ static jobject Image_getByteBuffer(JNIEnv* env, jobject thiz, int idx, int reade uint8_t *base = NULL; uint32_t size = 0; jobject byteBuffer; + PublicFormat readerPublicFormat = static_cast<PublicFormat>(readerFormat); ALOGV("%s: buffer index: %d", __FUNCTION__, idx); @@ -918,10 +915,11 @@ static jobject Image_getByteBuffer(JNIEnv* env, jobject thiz, int idx, int reade jniThrowException(env, "java/lang/IllegalStateException", "Image was released"); } - readerFormat = Image_getPixelFormat(env, readerFormat); + int readerHalFormat = android_view_Surface_mapPublicFormatToHalFormat( + readerPublicFormat); // Create byteBuffer from native buffer - Image_getLockedBufferInfo(env, buffer, idx, &base, &size, readerFormat); + Image_getLockedBufferInfo(env, buffer, idx, &base, &size, readerHalFormat); if (size > static_cast<uint32_t>(INT32_MAX)) { // Byte buffer have 'int capacity', so check the range diff --git a/media/jni/soundpool/SoundPool.cpp b/media/jni/soundpool/SoundPool.cpp index dfe2844222a1..1205f9d3a2f2 100644 --- a/media/jni/soundpool/SoundPool.cpp +++ b/media/jni/soundpool/SoundPool.cpp @@ -256,7 +256,7 @@ int SoundPool::play(int sampleID, float leftVolume, float rightVolume, dump(); // allocate a channel - channel = allocateChannel_l(priority, sampleID); + channel = allocateChannel_l(priority); // no channel allocated - return 0 if (!channel) { @@ -271,25 +271,13 @@ int SoundPool::play(int sampleID, float leftVolume, float rightVolume, return channelID; } -SoundChannel* SoundPool::allocateChannel_l(int priority, int sampleID) +SoundChannel* SoundPool::allocateChannel_l(int priority) { List<SoundChannel*>::iterator iter; SoundChannel* channel = NULL; - // check if channel for given sampleID still available + // allocate a channel if (!mChannels.empty()) { - for (iter = mChannels.begin(); iter != mChannels.end(); ++iter) { - if (sampleID == (*iter)->getPrevSampleID() && (*iter)->state() == SoundChannel::IDLE) { - channel = *iter; - mChannels.erase(iter); - ALOGV("Allocated recycled channel for same sampleID"); - break; - } - } - } - - // allocate any channel - if (!channel && !mChannels.empty()) { iter = mChannels.begin(); if (priority >= (*iter)->priority()) { channel = *iter; @@ -638,7 +626,7 @@ status_t Sample::doLoad() goto error; } - if ((numChannels < 1) || (numChannels > 2)) { + if ((numChannels < 1) || (numChannels > 8)) { ALOGE("Sample channel count (%d) out of range", numChannels); status = BAD_VALUE; goto error; @@ -660,7 +648,6 @@ error: void SoundChannel::init(SoundPool* soundPool) { mSoundPool = soundPool; - mPrevSampleID = -1; } // call with sound pool lock held @@ -669,7 +656,7 @@ void SoundChannel::play(const sp<Sample>& sample, int nextChannelID, float leftV { sp<AudioTrack> oldTrack; sp<AudioTrack> newTrack; - status_t status = NO_ERROR; + status_t status; { // scope for the lock Mutex::Autolock lock(&mLock); @@ -702,8 +689,10 @@ void SoundChannel::play(const sp<Sample>& sample, int nextChannelID, float leftV size_t frameCount = 0; if (loop) { - frameCount = sample->size()/numChannels/ - ((sample->format() == AUDIO_FORMAT_PCM_16_BIT) ? sizeof(int16_t) : sizeof(uint8_t)); + const audio_format_t format = sample->format(); + const size_t frameSize = audio_is_linear_pcm(format) + ? numChannels * audio_bytes_per_sample(format) : 1; + frameCount = sample->size() / frameSize; } #ifndef USE_SHARED_MEM_BUFFER @@ -714,43 +703,38 @@ void SoundChannel::play(const sp<Sample>& sample, int nextChannelID, float leftV } #endif - if (!mAudioTrack.get() || mPrevSampleID != sample->sampleID()) { - // mToggle toggles each time a track is started on a given channel. - // The toggle is concatenated with the SoundChannel address and passed to AudioTrack - // as callback user data. This enables the detection of callbacks received from the old - // audio track while the new one is being started and avoids processing them with - // wrong audio audio buffer size (mAudioBufferSize) - unsigned long toggle = mToggle ^ 1; - void *userData = (void *)((unsigned long)this | toggle); - audio_channel_mask_t channelMask = audio_channel_out_mask_from_count(numChannels); - - // do not create a new audio track if current track is compatible with sample parameters - #ifdef USE_SHARED_MEM_BUFFER - newTrack = new AudioTrack(streamType, sampleRate, sample->format(), - channelMask, sample->getIMemory(), AUDIO_OUTPUT_FLAG_FAST, callback, userData); - #else - uint32_t bufferFrames = (totalFrames + (kDefaultBufferCount - 1)) / kDefaultBufferCount; - newTrack = new AudioTrack(streamType, sampleRate, sample->format(), - channelMask, frameCount, AUDIO_OUTPUT_FLAG_FAST, callback, userData, - bufferFrames); - #endif - oldTrack = mAudioTrack; - status = newTrack->initCheck(); - if (status != NO_ERROR) { - ALOGE("Error creating AudioTrack"); - goto exit; - } - // From now on, AudioTrack callbacks received with previous toggle value will be ignored. - mToggle = toggle; - mAudioTrack = newTrack; - ALOGV("using new track %p for sample %d", newTrack.get(), sample->sampleID()); - } else { - newTrack = mAudioTrack; - newTrack->setSampleRate(sampleRate); - ALOGV("reusing track %p for sample %d", mAudioTrack.get(), sample->sampleID()); + // mToggle toggles each time a track is started on a given channel. + // The toggle is concatenated with the SoundChannel address and passed to AudioTrack + // as callback user data. This enables the detection of callbacks received from the old + // audio track while the new one is being started and avoids processing them with + // wrong audio audio buffer size (mAudioBufferSize) + unsigned long toggle = mToggle ^ 1; + void *userData = (void *)((unsigned long)this | toggle); + audio_channel_mask_t channelMask = audio_channel_out_mask_from_count(numChannels); + + // do not create a new audio track if current track is compatible with sample parameters +#ifdef USE_SHARED_MEM_BUFFER + newTrack = new AudioTrack(streamType, sampleRate, sample->format(), + channelMask, sample->getIMemory(), AUDIO_OUTPUT_FLAG_FAST, callback, userData); +#else + uint32_t bufferFrames = (totalFrames + (kDefaultBufferCount - 1)) / kDefaultBufferCount; + newTrack = new AudioTrack(streamType, sampleRate, sample->format(), + channelMask, frameCount, AUDIO_OUTPUT_FLAG_FAST, callback, userData, + bufferFrames); +#endif + oldTrack = mAudioTrack; + status = newTrack->initCheck(); + if (status != NO_ERROR) { + ALOGE("Error creating AudioTrack"); + goto exit; } + ALOGV("setVolume %p", newTrack.get()); newTrack->setVolume(leftVolume, rightVolume); newTrack->setLoop(0, frameCount, loop); + + // From now on, AudioTrack callbacks received with previous toggle value will be ignored. + mToggle = toggle; + mAudioTrack = newTrack; mPos = 0; mSample = sample; mChannelID = nextChannelID; @@ -893,7 +877,6 @@ bool SoundChannel::doStop_l() setVolume_l(0, 0); ALOGV("stop"); mAudioTrack->stop(); - mPrevSampleID = mSample->sampleID(); mSample.clear(); mState = IDLE; mPriority = IDLE_PRIORITY; diff --git a/media/jni/soundpool/SoundPool.h b/media/jni/soundpool/SoundPool.h index f520406e8753..d19cd9103222 100644 --- a/media/jni/soundpool/SoundPool.h +++ b/media/jni/soundpool/SoundPool.h @@ -72,8 +72,8 @@ private: volatile int32_t mRefCount; uint16_t mSampleID; uint16_t mSampleRate; - uint8_t mState : 3; - uint8_t mNumChannels : 2; + uint8_t mState; + uint8_t mNumChannels; audio_format_t mFormat; int mFd; int64_t mOffset; @@ -136,7 +136,6 @@ public: void nextEvent(); int nextChannelID() { return mNextEvent.channelID(); } void dump(); - int getPrevSampleID(void) { return mPrevSampleID; } private: static void callback(int event, void* user, void *info); @@ -153,7 +152,6 @@ private: int mAudioBufferSize; unsigned long mToggle; bool mAutoPaused; - int mPrevSampleID; }; // application object for managing a pool of sounds @@ -195,7 +193,7 @@ private: sp<Sample> findSample(int sampleID) { return mSamples.valueFor(sampleID); } SoundChannel* findChannel (int channelID); SoundChannel* findNextChannel (int channelID); - SoundChannel* allocateChannel_l(int priority, int sampleID); + SoundChannel* allocateChannel_l(int priority); void moveToFront_l(SoundChannel* channel); void notify(SoundPoolEvent event); void dump(); diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java index 3cae19db269d..d756d058e0f1 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java @@ -161,8 +161,7 @@ public class CameraDeviceBinderTest extends AndroidTestCase { assertFalse(request.isEmpty()); assertFalse(metadata.isEmpty()); if (needStream) { - int streamId = mCameraUser.createStream(/* ignored */10, /* ignored */20, - /* ignored */30, mSurface); + int streamId = mCameraUser.createStream(mSurface); assertEquals(0, streamId); request.addTarget(mSurface); } @@ -235,12 +234,11 @@ public class CameraDeviceBinderTest extends AndroidTestCase { @SmallTest public void testCreateStream() throws Exception { - int streamId = mCameraUser.createStream(/* ignored */10, /* ignored */20, /* ignored */30, - mSurface); + int streamId = mCameraUser.createStream(mSurface); assertEquals(0, streamId); assertEquals(CameraBinderTestUtils.ALREADY_EXISTS, - mCameraUser.createStream(/* ignored */0, /* ignored */0, /* ignored */0, mSurface)); + mCameraUser.createStream(mSurface)); assertEquals(CameraBinderTestUtils.NO_ERROR, mCameraUser.deleteStream(streamId)); } @@ -257,20 +255,18 @@ public class CameraDeviceBinderTest extends AndroidTestCase { public void testCreateStreamTwo() throws Exception { // Create first stream - int streamId = mCameraUser.createStream(/* ignored */0, /* ignored */0, /* ignored */0, - mSurface); + int streamId = mCameraUser.createStream(mSurface); assertEquals(0, streamId); assertEquals(CameraBinderTestUtils.ALREADY_EXISTS, - mCameraUser.createStream(/* ignored */0, /* ignored */0, /* ignored */0, mSurface)); + mCameraUser.createStream(mSurface)); // Create second stream with a different surface. SurfaceTexture surfaceTexture = new SurfaceTexture(/* ignored */0); surfaceTexture.setDefaultBufferSize(640, 480); Surface surface2 = new Surface(surfaceTexture); - int streamId2 = mCameraUser.createStream(/* ignored */0, /* ignored */0, /* ignored */0, - surface2); + int streamId2 = mCameraUser.createStream(surface2); assertEquals(1, streamId2); // Clean up streams diff --git a/packages/SystemUI/res/layout/remote_input.xml b/packages/SystemUI/res/layout/remote_input.xml new file mode 100644 index 000000000000..8ca5634ea9f1 --- /dev/null +++ b/packages/SystemUI/res/layout/remote_input.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2015 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 + --> + +<!-- FrameLayout --> +<com.android.systemui.statusbar.policy.RemoteInputView + xmlns:android="http://schemas.android.com/apk/res/android" + android:theme="@style/systemui_theme_light" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:paddingStart="4dp" + android:paddingEnd="2dp" + android:paddingBottom="4dp" + android:paddingTop="2dp"> + + <view class="com.android.systemui.statusbar.policy.RemoteInputView$RemoteEditText" + android:id="@+id/remote_input_text" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:singleLine="true" + android:imeOptions="actionSend" /> + + <ProgressBar + android:id="@+id/remote_input_progress" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:visibility="invisible" + android:indeterminate="true" + style="?android:attr/progressBarStyleHorizontal" /> + +</com.android.systemui.statusbar.policy.RemoteInputView> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 94f77c6d5246..07fcb8217c87 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -211,6 +211,11 @@ <item name="android:colorControlActivated">@color/system_accent_color</item> </style> + <style name="systemui_theme_light" parent="@android:style/Theme.DeviceDefault.Light"> + <item name="android:colorPrimary">@color/system_primary_color</item> + <item name="android:colorControlActivated">@color/system_accent_color</item> + </style> + <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"> <item name="android:colorPrimary">@color/system_primary_color</item> <item name="android:colorControlActivated">@color/system_accent_color</item> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index 37d9a7362336..8347a22652c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -24,6 +24,7 @@ import android.app.ActivityManagerNative; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.RemoteInput; import android.app.TaskStackBuilder; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; @@ -49,6 +50,7 @@ import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -98,6 +100,7 @@ import com.android.systemui.statusbar.phone.NavigationBarView; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.HeadsUpNotificationView; import com.android.systemui.statusbar.policy.PreviewInflater; +import com.android.systemui.statusbar.policy.RemoteInputView; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; import java.util.ArrayList; @@ -117,6 +120,9 @@ public abstract class BaseStatusBar extends SystemUI implements // STOPSHIP disable once we resolve b/18102199 private static final boolean NOTIFICATION_CLICK_DEBUG = true; + public static final boolean ENABLE_REMOTE_INPUT = + Build.IS_DEBUGGABLE && SystemProperties.getBoolean("debug.enable_remote_input", false); + protected static final int MSG_SHOW_RECENT_APPS = 1019; protected static final int MSG_HIDE_RECENT_APPS = 1020; protected static final int MSG_TOGGLE_RECENTS_APPS = 1021; @@ -410,6 +416,7 @@ public abstract class BaseStatusBar extends SystemUI implements @Override public void run() { for (StatusBarNotification sbn : notifications) { + processForRemoteInput(sbn.getNotification()); addNotification(sbn, currentRanking); } } @@ -424,6 +431,7 @@ public abstract class BaseStatusBar extends SystemUI implements mHandler.post(new Runnable() { @Override public void run() { + processForRemoteInput(sbn.getNotification()); Notification n = sbn.getNotification(); boolean isUpdate = mNotificationData.get(sbn.getKey()) != null || isHeadsUp(sbn.getKey()); @@ -1357,6 +1365,9 @@ public abstract class BaseStatusBar extends SystemUI implements (NotificationContentView) row.findViewById(R.id.expandedPublic); row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); + if (ENABLE_REMOTE_INPUT) { + row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + } PendingIntent contentIntent = sbn.getNotification().contentIntent; if (contentIntent != null) { @@ -1518,9 +1529,103 @@ public abstract class BaseStatusBar extends SystemUI implements } row.setUserLocked(userLocked); row.setStatusBarNotification(entry.notification); + applyRemoteInput(entry); return true; } + /** + * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this + * via first-class API. + * + * TODO: Remove once enough apps specify remote inputs on their own. + */ + private void processForRemoteInput(Notification n) { + if (!ENABLE_REMOTE_INPUT) return; + + if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") && + (n.actions == null || n.actions.length == 0)) { + Notification.Action viableAction = null; + Notification.WearableExtender we = new Notification.WearableExtender(n); + + List<Notification.Action> actions = we.getActions(); + final int numActions = actions.size(); + + for (int i = 0; i < numActions; i++) { + Notification.Action action = actions.get(i); + RemoteInput[] remoteInputs = action.getRemoteInputs(); + for (RemoteInput ri : action.getRemoteInputs()) { + if (ri.getAllowFreeFormInput()) { + viableAction = action; + break; + } + } + if (viableAction != null) { + break; + } + } + + if (viableAction != null) { + Notification stripped = n.clone(); + Notification.Builder.stripForDelivery(stripped); + stripped.actions = new Notification.Action[] { viableAction }; + stripped.extras.putBoolean("android.rebuild.contentView", true); + stripped.contentView = null; + stripped.extras.putBoolean("android.rebuild.bigView", true); + stripped.bigContentView = null; + + // Don't create the HUN input view for now because input doesn't work there yet. + // TODO: Enable once HUNs can take remote input correctly. + if (false) { + stripped.extras.putBoolean("android.rebuild.hudView", true); + stripped.headsUpContentView = null; + } + + Notification rebuilt = Notification.Builder.rebuild(mContext, stripped); + + n.actions = rebuilt.actions; + n.bigContentView = rebuilt.bigContentView; + n.headsUpContentView = rebuilt.headsUpContentView; + n.publicVersion = rebuilt.publicVersion; + } + } + } + + private void applyRemoteInput(final Entry entry) { + if (!ENABLE_REMOTE_INPUT) return; + + RemoteInput remoteInput = null; + + // See if the notification has exactly one action and this action allows free-form input + // TODO: relax restrictions once we support more than one remote input action. + Notification.Action[] actions = entry.notification.getNotification().actions; + if (actions != null && actions.length == 1) { + if (actions[0].getRemoteInputs() != null) { + for (RemoteInput ri : actions[0].getRemoteInputs()) { + if (ri.getAllowFreeFormInput()) { + remoteInput = ri; + break; + } + } + } + } + + // See if we have somewhere to put that remote input + ViewGroup actionContainer = null; + if (remoteInput != null && entry.expandedBig != null) { + View actionContainerCandidate = entry.expandedBig + .findViewById(com.android.internal.R.id.actions); + if (actionContainerCandidate instanceof ViewGroup) { + actionContainer = (ViewGroup) actionContainerCandidate; + } + } + + if (actionContainer != null) { + actionContainer.removeAllViews(); + actionContainer.addView( + RemoteInputView.inflate(mContext, actionContainer, actions[0], remoteInput)); + } + } + public NotificationClicker makeClicker(PendingIntent intent, String notificationKey) { return new NotificationClicker(intent, notificationKey); } @@ -2037,6 +2142,8 @@ public abstract class BaseStatusBar extends SystemUI implements entry.row.setStatusBarNotification(notification); entry.row.notifyContentUpdated(); entry.row.resetHeight(); + + applyRemoteInput(entry); } protected void notifyHeadsUpScreenOn(boolean screenOn) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java index eba7d9ffb948..63bbf973dcd6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java @@ -115,7 +115,8 @@ public class StatusBarWindowManager { private void applyFocusableFlag(State state) { if (state.isKeyguardShowingAndNotOccluded() && state.keyguardNeedsInput - && state.bouncerShowing) { + && state.bouncerShowing + || BaseStatusBar.ENABLE_REMOTE_INPUT && state.statusBarExpanded) { mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } else if (state.isKeyguardShowingAndNotOccluded() || state.statusBarFocusable) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java new file mode 100644 index 000000000000..7d721c22415b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.policy; + +import com.android.systemui.R; + +import android.annotation.NonNull; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +/** + * Host for the remote input. + */ +public class RemoteInputView extends FrameLayout implements View.OnClickListener { + + private static final String TAG = "RemoteInput"; + + private RemoteEditText mEditText; + private ProgressBar mProgressBar; + private PendingIntent mPendingIntent; + private RemoteInput mRemoteInput; + private Notification.Action mAction; + + public RemoteInputView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mProgressBar = (ProgressBar) findViewById(R.id.remote_input_progress); + + mEditText = (RemoteEditText) getChildAt(0); + mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + + // Check if this was the result of hitting the enter key + final boolean isSoftImeEvent = event == null + && (actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT + || actionId == EditorInfo.IME_ACTION_SEND); + final boolean isKeyboardEnterKey = event != null + && KeyEvent.isConfirmKey(event.getKeyCode()) + && event.getAction() == KeyEvent.ACTION_DOWN; + + if (isSoftImeEvent || isKeyboardEnterKey) { + sendRemoteInput(); + return true; + } + return false; + } + }); + mEditText.setOnClickListener(this); + mEditText.setInnerFocusable(false); + } + + private void sendRemoteInput() { + Bundle results = new Bundle(); + results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString()); + Intent fillInIntent = new Intent(); + RemoteInput.addResultsToIntent(mAction.getRemoteInputs(), fillInIntent, + results); + + mEditText.setEnabled(false); + mProgressBar.setVisibility(VISIBLE); + + try { + mPendingIntent.send(mContext, 0, fillInIntent); + } catch (PendingIntent.CanceledException e) { + Log.i(TAG, "Unable to send remote input result", e); + } + } + + public static RemoteInputView inflate(Context context, ViewGroup root, + Notification.Action action, RemoteInput remoteInput) { + RemoteInputView v = (RemoteInputView) + LayoutInflater.from(context).inflate(R.layout.remote_input, root, false); + + v.mEditText.setHint(action.title); + v.mPendingIntent = action.actionIntent; + v.mRemoteInput = remoteInput; + v.mAction = action; + + return v; + } + + @Override + public void onClick(View v) { + if (v == mEditText) { + if (!mEditText.isFocusable()) { + mEditText.setInnerFocusable(true); + InputMethodManager imm = InputMethodManager.getInstance(); + if (imm != null) { + imm.viewClicked(mEditText); + imm.showSoftInput(mEditText, 0); + } + } + } + } + + /** + * An EditText that changes appearance based on whether it's focusable and becomes + * un-focusable whenever the user navigates away from it or it becomes invisible. + */ + public static class RemoteEditText extends EditText { + + private final Drawable mBackground; + + public RemoteEditText(Context context, AttributeSet attrs) { + super(context, attrs); + mBackground = getBackground(); + } + + private void defocusIfNeeded() { + if (isFocusable() && isEnabled()) { + setInnerFocusable(false); + } + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + + if (!isShown()) { + defocusIfNeeded(); + } + } + + @Override + protected void onFocusLost() { + super.onFocusLost(); + defocusIfNeeded(); + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + defocusIfNeeded(); + } + return super.onKeyPreIme(keyCode, event); + } + + + void setInnerFocusable(boolean focusable) { + setFocusableInTouchMode(focusable); + setFocusable(focusable); + setCursorVisible(focusable); + + if (focusable) { + requestFocus(); + setBackground(mBackground); + } else { + setBackground(null); + } + + } + } +} diff --git a/wifi/java/android/net/wifi/WifiActivityEnergyInfo.java b/wifi/java/android/net/wifi/WifiActivityEnergyInfo.java index 533b8bca10ac..626346304437 100644 --- a/wifi/java/android/net/wifi/WifiActivityEnergyInfo.java +++ b/wifi/java/android/net/wifi/WifiActivityEnergyInfo.java @@ -21,37 +21,37 @@ import android.os.Parcelable; /** * Record of energy and activity information from controller and - * underlying wifi stack state.Timestamp the record with system - * time + * underlying wifi stack state. Timestamp the record with elapsed + * real-time. * @hide */ public final class WifiActivityEnergyInfo implements Parcelable { + private final long mTimestamp; private final int mStackState; private final int mControllerTxTimeMs; private final int mControllerRxTimeMs; private final int mControllerIdleTimeMs; private final int mControllerEnergyUsed; - private final long timestamp; public static final int STACK_STATE_INVALID = 0; public static final int STACK_STATE_STATE_ACTIVE = 1; public static final int STACK_STATE_STATE_SCANNING = 2; public static final int STACK_STATE_STATE_IDLE = 3; - public WifiActivityEnergyInfo(int stackState, int txTime, int rxTime, - int idleTime, int energyUsed) { + public WifiActivityEnergyInfo(long timestamp, int stackState, + int txTime, int rxTime, int idleTime, int energyUsed) { + mTimestamp = timestamp; mStackState = stackState; mControllerTxTimeMs = txTime; mControllerRxTimeMs = rxTime; mControllerIdleTimeMs = idleTime; mControllerEnergyUsed = energyUsed; - timestamp = System.currentTimeMillis(); } @Override public String toString() { return "WifiActivityEnergyInfo{" - + " timestamp=" + timestamp + + " timestamp=" + mTimestamp + " mStackState=" + mStackState + " mControllerTxTimeMs=" + mControllerTxTimeMs + " mControllerRxTimeMs=" + mControllerRxTimeMs @@ -63,13 +63,14 @@ public final class WifiActivityEnergyInfo implements Parcelable { public static final Parcelable.Creator<WifiActivityEnergyInfo> CREATOR = new Parcelable.Creator<WifiActivityEnergyInfo>() { public WifiActivityEnergyInfo createFromParcel(Parcel in) { + long timestamp = in.readLong(); int stackState = in.readInt(); int txTime = in.readInt(); int rxTime = in.readInt(); int idleTime = in.readInt(); int energyUsed = in.readInt(); - return new WifiActivityEnergyInfo(stackState, txTime, rxTime, - idleTime, energyUsed); + return new WifiActivityEnergyInfo(timestamp, stackState, + txTime, rxTime, idleTime, energyUsed); } public WifiActivityEnergyInfo[] newArray(int size) { return new WifiActivityEnergyInfo[size]; @@ -77,6 +78,7 @@ public final class WifiActivityEnergyInfo implements Parcelable { }; public void writeToParcel(Parcel out, int flags) { + out.writeLong(mTimestamp); out.writeInt(mStackState); out.writeInt(mControllerTxTimeMs); out.writeInt(mControllerRxTimeMs); @@ -127,7 +129,7 @@ public final class WifiActivityEnergyInfo implements Parcelable { * @return timestamp(wall clock) of record creation */ public long getTimeStamp() { - return timestamp; + return mTimestamp; } /** |