Merge "MIDI Manager changes:"
diff --git a/api/current.txt b/api/current.txt
index b22dd1c1..612d16d 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -10973,6 +10973,8 @@
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 2b8ce98..ff9809f 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -11248,6 +11248,8 @@
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 @@
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 @@
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 @@
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 @@
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/bluetooth/BluetoothActivityEnergyInfo.java b/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java
index ce87329..161c339 100644
--- a/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java
+++ b/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java
@@ -26,32 +26,32 @@
* @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 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 void writeToParcel(Parcel out, int flags) {
+ out.writeLong(mTimestamp);
out.writeInt(mBluetoothStackState);
out.writeInt(mControllerTxTimeMs);
out.writeInt(mControllerRxTimeMs);
@@ -123,11 +125,12 @@
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 410849a..1fca920 100644
--- a/core/java/android/content/res/TypedArray.java
+++ b/core/java/android/content/res/TypedArray.java
@@ -444,8 +444,10 @@
}
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 @@
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 @@
&& 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 87ec06a..a0217c2 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -2493,6 +2493,83 @@
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 50a58ed..d286d38 100644
--- a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl
+++ b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl
@@ -66,7 +66,7 @@
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 ec450bd1..78aefa5 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -373,9 +373,7 @@
// 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 fcf172c..26cd498 100644
--- a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java
+++ b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java
@@ -504,7 +504,7 @@
}
@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 0fee4b3..d96a0e9 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -86,10 +86,10 @@
*/
public static final int WIFI_SCAN = 6;
- /**
- * A constant indicating a wifi multicast timer
- */
- public static final int WIFI_MULTICAST_ENABLED = 7;
+ /**
+ * A constant indicating a wifi multicast timer
+ */
+ public static final int WIFI_MULTICAST_ENABLED = 7;
/**
* A constant indicating a video turn on timer
@@ -353,7 +353,9 @@
public abstract long getWifiRunningTime(long elapsedRealtimeUs, int which);
public abstract long getFullWifiLockTime(long elapsedRealtimeUs, int which);
public abstract long getWifiScanTime(long elapsedRealtimeUs, int which);
+ public abstract int getWifiScanCount(int which);
public abstract long getWifiBatchedScanTime(int csphBin, long elapsedRealtimeUs, int which);
+ public abstract int getWifiBatchedScanCount(int csphBin, int which);
public abstract long getWifiMulticastTime(long elapsedRealtimeUs, int which);
public abstract long getAudioTurnedOnTime(long elapsedRealtimeUs, int which);
public abstract long getVideoTurnedOnTime(long elapsedRealtimeUs, int which);
@@ -439,14 +441,14 @@
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.
*/
@@ -474,14 +476,14 @@
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.
@@ -1856,6 +1858,15 @@
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.
*/
@@ -2140,13 +2151,6 @@
}
}
- public final static void formatTime(StringBuilder sb, long time) {
- long sec = time / 100;
- formatTimeRaw(sb, sec);
- sb.append((time - (sec * 100)) * 10);
- sb.append("ms ");
- }
-
public final static void formatTimeMs(StringBuilder sb, long time) {
long sec = time / 1000;
formatTimeRaw(sb, sec);
@@ -2572,6 +2576,7 @@
long wifiPacketsTx = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which);
long fullWifiLockOnTime = u.getFullWifiLockTime(rawRealtime, which);
long wifiScanTime = u.getWifiScanTime(rawRealtime, which);
+ int wifiScanCount = u.getWifiScanCount(which);
long uidWifiRunningTime = u.getWifiRunningTime(rawRealtime, which);
if (mobileBytesRx > 0 || mobileBytesTx > 0 || wifiBytesRx > 0 || wifiBytesTx > 0
@@ -2584,10 +2589,10 @@
mobileActiveTime, mobileActiveCount);
}
- if (fullWifiLockOnTime != 0 || wifiScanTime != 0
+ if (fullWifiLockOnTime != 0 || wifiScanTime != 0 || wifiScanCount != 0
|| uidWifiRunningTime != 0) {
dumpLine(pw, uid, category, WIFI_DATA,
- fullWifiLockOnTime, wifiScanTime, uidWifiRunningTime);
+ fullWifiLockOnTime, wifiScanTime, uidWifiRunningTime, wifiScanCount);
}
if (u.hasUserActivity()) {
@@ -2705,9 +2710,9 @@
: processStats.entrySet()) {
Uid.Proc ps = ent.getValue();
- final long userMillis = ps.getUserTime(which) * 10;
- final long systemMillis = ps.getSystemTime(which) * 10;
- final long foregroundMillis = ps.getForegroundTime(which) * 10;
+ final long userMillis = ps.getUserTime(which);
+ final long systemMillis = ps.getSystemTime(which);
+ final long foregroundMillis = ps.getForegroundTime(which);
final int starts = ps.getStarts(which);
final int numCrashes = ps.getNumCrashes(which);
final int numAnrs = ps.getNumAnrs(which);
@@ -3146,6 +3151,35 @@
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);
@@ -3173,9 +3207,41 @@
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) {
@@ -3420,6 +3486,7 @@
long wifiTxPackets = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which);
long fullWifiLockOnTime = u.getFullWifiLockTime(rawRealtime, which);
long wifiScanTime = u.getWifiScanTime(rawRealtime, which);
+ int wifiScanCount = u.getWifiScanCount(which);
long uidWifiRunningTime = u.getWifiRunningTime(rawRealtime, which);
if (mobileRxBytes > 0 || mobileTxBytes > 0
@@ -3455,7 +3522,7 @@
pw.print(" received, "); pw.print(wifiTxPackets); pw.println(" sent)");
}
- if (fullWifiLockOnTime != 0 || wifiScanTime != 0
+ if (fullWifiLockOnTime != 0 || wifiScanTime != 0 || wifiScanCount != 0
|| uidWifiRunningTime != 0) {
sb.setLength(0);
sb.append(prefix); sb.append(" Wifi Running: ");
@@ -3469,7 +3536,9 @@
sb.append(prefix); sb.append(" Wifi Scan: ");
formatTimeMs(sb, wifiScanTime / 1000);
sb.append("("); sb.append(formatRatioLocked(wifiScanTime,
- whichBatteryRealtime)); sb.append(")");
+ whichBatteryRealtime)); sb.append(") ");
+ sb.append(wifiScanCount);
+ sb.append("x");
pw.println(sb.toString());
}
@@ -3728,9 +3797,9 @@
sb.append(prefix); sb.append(" Proc ");
sb.append(ent.getKey()); sb.append(":\n");
sb.append(prefix); sb.append(" CPU: ");
- formatTime(sb, userTime); sb.append("usr + ");
- formatTime(sb, systemTime); sb.append("krn ; ");
- formatTime(sb, foregroundTime); sb.append("fg");
+ formatTimeMs(sb, userTime); sb.append("usr + ");
+ formatTimeMs(sb, systemTime); sb.append("krn ; ");
+ formatTimeMs(sb, foregroundTime); sb.append("fg");
if (starts != 0 || numCrashes != 0 || numAnrs != 0) {
sb.append("\n"); sb.append(prefix); sb.append(" ");
boolean hasOne = false;
diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java
index 1014573..1a07aee 100644
--- a/core/java/android/view/LayoutInflater.java
+++ b/core/java/android/view/LayoutInflater.java
@@ -532,10 +532,10 @@
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 8197b3d..0f99e88 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -3326,6 +3326,8 @@
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 cfeca08..4cd959f 100644
--- a/core/java/com/android/internal/os/BatterySipper.java
+++ b/core/java/com/android/internal/os/BatterySipper.java
@@ -26,12 +26,15 @@
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 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 @@
}
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 eae4427..d3611bf 100644
--- a/core/java/com/android/internal/os/BatteryStatsHelper.java
+++ b/core/java/com/android/internal/os/BatteryStatsHelper.java
@@ -344,6 +344,7 @@
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 @@
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 @@
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 * 10; // convert to millis
- final long tmpCpuTime = (userTime + systemTime) * 10; // convert to millis
- 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
+ app.wakeLockTime = wakeLockTimeUs / 1000; // convert to millis
+ totalAppWakelockTimeUs += wakeLockTimeUs;
- // 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;
+ // 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);
- // 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;
-
- // 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;
- for (int bin = 0; bin < BatteryStats.Uid.NUM_WIFI_BATCHED_SCAN_BINS; bin++) {
- long batchScanTimeMs = u.getWifiBatchedScanTime(bin, mRawRealtime, which) / 1000;
- p = ((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;
+ 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.
+ 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
+ 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++) {
+ 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 && 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);
+
+ 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)));
}
- 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.
+ if (totalUnnormalizedPower != 0 || u.getUid() == 0) {
+ appList.add(app);
+ }
+ }
- // 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);
- }
+ // 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);
+ }
+ list.add(app);
+
+ Double userPower = mUserPower.get(userId);
+ if (userPower == null) {
+ userPower = power;
} else {
- mUsageList.add(app);
- if (power > mMaxPower) mMaxPower = power;
- if (power > mMaxRealPower) mMaxRealPower = power;
- mComputedPower += power;
+ userPower += power;
}
- if (u.getUid() == 0) {
- osApp = app;
- }
+ 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 @@
// 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 @@
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 @@
}
}
+ /**
+ * 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);
+ }
+
+ 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");
+ }
+ }
+
+ /**
+ * Bluetooth usage is not attributed to any apps yet, so the entire blame goes to the
+ * Bluetooth Category.
+ */
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));
+ 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));
}
- 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));
- }
- btPower += pingPower;
- if ((btPower+mBluetoothPower) != 0) {
- BatterySipper bs = addEntry(BatterySipper.DrainType.BLUETOOTH, btOnTimeMs,
- btPower + mBluetoothPower);
+
+ 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 f6c5dc7..f9b1ca1 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 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.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 @@
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 @@
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);
}
@@ -4749,6 +4775,14 @@
}
@Override
+ public int getWifiScanCount(int which) {
+ if (mWifiScanTimer == null) {
+ return 0;
+ }
+ return mWifiScanTimer.getCountLocked(which);
+ }
+
+ @Override
public long getWifiBatchedScanTime(int csphBin, long elapsedRealtimeUs, int which) {
if (csphBin < 0 || csphBin >= NUM_WIFI_BATCHED_SCAN_BINS) return 0;
if (mWifiBatchedScanTimer[csphBin] == null) {
@@ -4758,6 +4792,15 @@
}
@Override
+ public int getWifiBatchedScanCount(int csphBin, int which) {
+ if (csphBin < 0 || csphBin >= NUM_WIFI_BATCHED_SCAN_BINS) return 0;
+ if (mWifiBatchedScanTimer[csphBin] == null) {
+ return 0;
+ }
+ return mWifiBatchedScanTimer[csphBin].getCountLocked(which);
+ }
+
+ @Override
public long getWifiMulticastTime(long elapsedRealtimeUs, int which) {
if (mWifiMulticastTimer == null) {
return 0;
@@ -5601,17 +5644,17 @@
boolean mActive = true;
/**
- * Total time (in 1/100 sec) spent executing in user code.
+ * Total time (in ms) spent executing in user code.
*/
long mUserTime;
/**
- * Total time (in 1/100 sec) spent executing in kernel code.
+ * Total time (in ms) spent executing in kernel code.
*/
long mSystemTime;
/**
- * Amount of time the process was running in the foreground.
+ * Amount of time (in ms) the process was running in the foreground.
*/
long mForegroundTime;
@@ -6635,6 +6678,10 @@
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);
@@ -7222,6 +7269,10 @@
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++) {
@@ -7308,6 +7359,9 @@
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);
@@ -7744,6 +7798,65 @@
}
}
+ 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);
}
@@ -8458,6 +8571,15 @@
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);
@@ -8746,6 +8868,12 @@
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);
@@ -9050,6 +9178,15 @@
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();
@@ -9195,6 +9332,12 @@
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 b3bafa1..944eb5a 100644
--- a/core/java/com/android/internal/os/PowerProfile.java
+++ b/core/java/com/android/internal/os/PowerProfile.java
@@ -76,6 +76,11 @@
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 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 @@
}
/**
- * 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 @@
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/os/ProcessCpuTracker.java b/core/java/com/android/internal/os/ProcessCpuTracker.java
index 501e0ec..2983047 100644
--- a/core/java/com/android/internal/os/ProcessCpuTracker.java
+++ b/core/java/com/android/internal/os/ProcessCpuTracker.java
@@ -22,11 +22,13 @@
import android.os.Process;
import android.os.StrictMode;
import android.os.SystemClock;
+import android.system.OsConstants;
import android.util.Slog;
import com.android.internal.util.FastPrintWriter;
import libcore.io.IoUtils;
+import libcore.io.Libcore;
import java.io.File;
import java.io.FileInputStream;
@@ -130,6 +132,9 @@
private final boolean mIncludeThreads;
+ // How long a CPU jiffy is in milliseconds.
+ private final long mJiffyMillis;
+
private float mLoad1 = 0;
private float mLoad5 = 0;
private float mLoad15 = 0;
@@ -269,6 +274,8 @@
public ProcessCpuTracker(boolean includeThreads) {
mIncludeThreads = includeThreads;
+ long jiffyHz = Libcore.os.sysconf(OsConstants._SC_CLK_TCK);
+ mJiffyMillis = 1000/jiffyHz;
}
public void onLoadChanged(float load1, float load5, float load15) {
@@ -294,15 +301,15 @@
if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT,
null, sysCpu, null)) {
// Total user time is user + nice time.
- final long usertime = sysCpu[0]+sysCpu[1];
+ final long usertime = (sysCpu[0]+sysCpu[1]) * mJiffyMillis;
// Total system time is simply system time.
- final long systemtime = sysCpu[2];
+ final long systemtime = sysCpu[2] * mJiffyMillis;
// Total idle time is simply idle time.
- final long idletime = sysCpu[3];
+ final long idletime = sysCpu[3] * mJiffyMillis;
// Total irq time is iowait + irq + softirq time.
- final long iowaittime = sysCpu[4];
- final long irqtime = sysCpu[5];
- final long softirqtime = sysCpu[6];
+ final long iowaittime = sysCpu[4] * mJiffyMillis;
+ final long irqtime = sysCpu[5] * mJiffyMillis;
+ final long softirqtime = sysCpu[6] * mJiffyMillis;
// This code is trying to avoid issues with idle time going backwards,
// but currently it gets into situations where it triggers most of the time. :(
@@ -318,10 +325,11 @@
mRelStatsAreGood = true;
if (DEBUG) {
- Slog.i("Load", "Total U:" + sysCpu[0] + " N:" + sysCpu[1]
- + " S:" + sysCpu[2] + " I:" + sysCpu[3]
- + " W:" + sysCpu[4] + " Q:" + sysCpu[5]
- + " O:" + sysCpu[6]);
+ Slog.i("Load", "Total U:" + (sysCpu[0]*mJiffyMillis)
+ + " N:" + (sysCpu[1]*mJiffyMillis)
+ + " S:" + (sysCpu[2]*mJiffyMillis) + " I:" + (sysCpu[3]*mJiffyMillis)
+ + " W:" + (sysCpu[4]*mJiffyMillis) + " Q:" + (sysCpu[5]*mJiffyMillis)
+ + " O:" + (sysCpu[6]*mJiffyMillis));
Slog.i("Load", "Rel U:" + mRelUserTime + " S:" + mRelSystemTime
+ " I:" + mRelIdleTime + " Q:" + mRelIrqTime);
}
@@ -414,8 +422,8 @@
final long minfaults = procStats[PROCESS_STAT_MINOR_FAULTS];
final long majfaults = procStats[PROCESS_STAT_MAJOR_FAULTS];
- final long utime = procStats[PROCESS_STAT_UTIME];
- final long stime = procStats[PROCESS_STAT_STIME];
+ final long utime = procStats[PROCESS_STAT_UTIME] * mJiffyMillis;
+ final long stime = procStats[PROCESS_STAT_STIME] * mJiffyMillis;
if (utime == st.base_utime && stime == st.base_stime) {
st.rel_utime = 0;
@@ -489,8 +497,8 @@
st.baseName = procStatsString[0];
st.base_minfaults = procStats[PROCESS_FULL_STAT_MINOR_FAULTS];
st.base_majfaults = procStats[PROCESS_FULL_STAT_MAJOR_FAULTS];
- st.base_utime = procStats[PROCESS_FULL_STAT_UTIME];
- st.base_stime = procStats[PROCESS_FULL_STAT_STIME];
+ st.base_utime = procStats[PROCESS_FULL_STAT_UTIME] * mJiffyMillis;
+ st.base_stime = procStats[PROCESS_FULL_STAT_STIME] * mJiffyMillis;
} else {
Slog.i(TAG, "Skipping kernel process pid " + pid
+ " name " + procStatsString[0]);
@@ -576,7 +584,7 @@
null, statsData, null)) {
long time = statsData[PROCESS_STAT_UTIME]
+ statsData[PROCESS_STAT_STIME];
- return time;
+ return time * mJiffyMillis;
}
return 0;
}
@@ -777,7 +785,7 @@
for (int i=0; i<N; i++) {
Stats st = mWorkingProcs.get(i);
printProcessCPU(pw, st.added ? " +" : (st.removed ? " -": " "),
- st.pid, st.name, (int)(st.rel_uptime+5)/10,
+ st.pid, st.name, (int)st.rel_uptime,
st.rel_utime, st.rel_stime, 0, 0, 0, st.rel_minfaults, st.rel_majfaults);
if (!st.removed && st.workingThreads != null) {
int M = st.workingThreads.size();
@@ -785,7 +793,7 @@
Stats tst = st.workingThreads.get(j);
printProcessCPU(pw,
tst.added ? " +" : (tst.removed ? " -": " "),
- tst.pid, tst.name, (int)(st.rel_uptime+5)/10,
+ tst.pid, tst.name, (int)st.rel_uptime,
tst.rel_utime, tst.rel_stime, 0, 0, 0, 0, 0);
}
}
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 0000000..910a720
--- /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 0000000..f916e6f
--- /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 bfa0534..39064ed 100644
--- a/core/jni/android_view_Surface.cpp
+++ b/core/jni/android_view_Surface.cpp
@@ -130,6 +130,100 @@
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/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java
index 3efb9c0..49c4247 100644
--- a/graphics/java/android/graphics/ImageFormat.java
+++ b/graphics/java/android/graphics/ImageFormat.java
@@ -356,6 +356,38 @@
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 @@
case Y8:
return 8;
case Y16:
+ case DEPTH16:
return 16;
case NV21:
return 12;
@@ -412,6 +445,8 @@
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 138d73a..0000000
--- 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 ef35289..6d1b1fe 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;
+ private float mOpacity = 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;
-
- private boolean mHasPendingHardwareExit;
- private int mPendingOpacityDuration;
- private int mPendingInflectionDuration;
- private int mPendingInflectionOpacity;
-
- /**
- * Creates a new ripple.
- */
public RippleBackground(RippleDrawable owner, Rect bounds) {
- mOwner = owner;
- mBounds = bounds;
+ super(owner, 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;
+ public boolean isVisible() {
+ return mOpacity > 0 || isHardwareAnimating();
}
- 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 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();
+ return opacity;
}
- /**
- * Starts the exit animation.
- */
- public void exit() {
- cancel();
+ @Override
+ protected Animator createSoftwareExit() {
+ final AnimatorSet set = new AnimatorSet();
- // 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);
+ // 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);
- // 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);
+ final AnimatorSet.Builder builder = set.play(exit);
- if (mCanUseHardware) {
- createPendingHardwareExit(opacityDuration, inflectionDuration, inflectionOpacity);
- } else {
- exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity);
+ // 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);
+
+ builder.after(enter);
+ }
+
+ return set;
+ }
+
+ @Override
+ protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
+ final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
+
+ final int targetAlpha = p.getAlpha();
+ final int currentAlpha = (int) (mOpacity * targetAlpha + 0.5f);
+ p.setAlpha(currentAlpha);
+
+ mPropPaint = CanvasProperty.createPaint(p);
+ mPropRadius = CanvasProperty.createFloat(mTargetRadius);
+ mPropX = CanvasProperty.createFloat(0);
+ mPropY = CanvasProperty.createFloat(0);
+
+ // 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);
+ }
+
+ // 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);
+
+ return set;
+ }
+
+ @Override
+ protected void jumpValuesToExit() {
+ mOpacity = 0;
+ }
+
+ private static abstract class BackgroundProperty extends FloatProperty<RippleBackground> {
+ public BackgroundProperty(String name) {
+ super(name);
}
}
- 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();
- }
-
- private void startPendingHardwareExit(HardwareCanvas c, Paint p) {
- mHasPendingHardwareExit = false;
-
- final int opacityDuration = mPendingOpacityDuration;
- final int inflectionDuration = mPendingInflectionDuration;
- final int inflectionOpacity = mPendingInflectionOpacity;
-
- 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 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);
-
- // 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);
- }
-
- outerOpacityAnim.setTarget(c);
- outerOpacityAnim.start();
-
- mRunningAnimations.add(outerOpacityAnim);
-
- mHardwareAnimating = true;
-
- // Set up the software values to match the hardware end values.
- mOuterOpacity = 0;
- }
-
- /**
- * 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);
-
- mAnimOuterOpacity = outerFadeOutAnim;
-
- outerFadeOutAnim.start();
- }
-
- @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);
- }
-
- mAnimOuterOpacity = outerOpacityAnim;
-
- outerOpacityAnim.start();
- }
-
- /**
- * Cancel all animations. The caller is responsible for removing
- * the ripple from the list of animating ripples.
- */
- public void cancel() {
- cancelSoftwareAnimations();
- cancelHardwareAnimations(false);
- }
-
- private void cancelSoftwareAnimations() {
- if (mAnimOuterOpacity != null) {
- mAnimOuterOpacity.cancel();
- mAnimOuterOpacity = 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) {
- mOuterOpacity = 0;
- }
- }
-
- mHardwareAnimating = false;
- }
-
- private void invalidateSelf() {
- mOwner.invalidateSelf();
- }
-
- private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
+ private static final BackgroundProperty OPACITY = new BackgroundProperty("opacity") {
@Override
- public void onAnimationEnd(Animator animation) {
- mHardwareAnimating = false;
+ public void setValue(RippleBackground object, float value) {
+ object.mOpacity = value;
+ object.invalidateSelf();
+ }
+
+ @Override
+ 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 0000000..fd3e06c
--- /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 b028eeb..bc8c7d2 100644
--- a/graphics/java/android/graphics/drawable/RippleDrawable.java
+++ b/graphics/java/android/graphics/drawable/RippleDrawable.java
@@ -137,7 +137,7 @@
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 @@
* 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 @@
super.jumpToCurrentState();
if (mRipple != null) {
- mRipple.jump();
+ mRipple.end();
}
if (mBackground != null) {
- mBackground.jump();
+ mBackground.end();
}
cancelExitingRipples();
@@ -219,10 +219,13 @@
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 @@
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 @@
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 @@
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 @@
*/
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 @@
*/
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 @@
*/
@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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
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 0000000..2023f04
--- /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 53e8b49..a6836a8 100644
--- a/include/android_runtime/android_view_Surface.h
+++ b/include/android_runtime/android_view_Surface.h
@@ -26,6 +26,33 @@
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 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/libs/hwui/Glop.h b/libs/hwui/Glop.h
index 62da6e08..2c6f6c1 100644
--- a/libs/hwui/Glop.h
+++ b/libs/hwui/Glop.h
@@ -78,7 +78,7 @@
// TODO: enforce mutual exclusion with restricted setters and/or unions
struct Vertices {
GLuint bufferObject;
- VertexAttribFlags flags;
+ int attribFlags;
const void* position;
const void* texCoord;
const void* color;
diff --git a/libs/hwui/GlopBuilder.cpp b/libs/hwui/GlopBuilder.cpp
index 711b11c..1d07951 100644
--- a/libs/hwui/GlopBuilder.cpp
+++ b/libs/hwui/GlopBuilder.cpp
@@ -66,7 +66,7 @@
mOutGlop->mesh.indices = { 0, nullptr };
mOutGlop->mesh.vertices = {
mRenderState.meshState().getUnitQuadVBO(),
- VertexAttribFlags::kNone,
+ static_cast<int>(VertexAttribFlags::kNone),
nullptr, nullptr, nullptr,
kTextureVertexStride };
mOutGlop->mesh.elementCount = 4;
@@ -85,7 +85,7 @@
mOutGlop->mesh.indices = { 0, nullptr };
mOutGlop->mesh.vertices = {
mRenderState.meshState().getUnitQuadVBO(),
- VertexAttribFlags::kTextureCoord,
+ static_cast<int>(VertexAttribFlags::kTextureCoord),
nullptr, (const void*) kMeshTextureOffset, nullptr,
kTextureVertexStride };
mOutGlop->mesh.elementCount = 4;
@@ -105,7 +105,7 @@
mOutGlop->mesh.indices = { 0, nullptr };
mOutGlop->mesh.vertices = {
0,
- VertexAttribFlags::kTextureCoord,
+ static_cast<int>(VertexAttribFlags::kTextureCoord),
&textureVertex[0].x, &textureVertex[0].u, nullptr,
kTextureVertexStride };
mOutGlop->mesh.elementCount = 4;
@@ -119,7 +119,7 @@
mOutGlop->mesh.indices = { mRenderState.meshState().getQuadListIBO(), nullptr };
mOutGlop->mesh.vertices = {
0,
- VertexAttribFlags::kNone,
+ static_cast<int>(VertexAttribFlags::kNone),
vertexData, nullptr, nullptr,
kVertexStride };
mOutGlop->mesh.elementCount = 6 * quadCount;
@@ -133,7 +133,7 @@
mOutGlop->mesh.indices = { mRenderState.meshState().getQuadListIBO(), nullptr };
mOutGlop->mesh.vertices = {
0,
- VertexAttribFlags::kTextureCoord,
+ static_cast<int>(VertexAttribFlags::kTextureCoord),
&vertexData[0].x, &vertexData[0].u, nullptr,
kTextureVertexStride };
mOutGlop->mesh.elementCount = elementCount;
@@ -147,7 +147,7 @@
mOutGlop->mesh.indices = { 0, nullptr };
mOutGlop->mesh.vertices = {
0,
- VertexAttribFlags::kTextureCoord,
+ static_cast<int>(VertexAttribFlags::kTextureCoord),
&vertexData[0].x, &vertexData[0].u, nullptr,
kTextureVertexStride };
mOutGlop->mesh.elementCount = elementCount;
@@ -161,7 +161,7 @@
mOutGlop->mesh.indices = { 0, nullptr };
mOutGlop->mesh.vertices = {
0,
- static_cast<VertexAttribFlags>(VertexAttribFlags::kTextureCoord | VertexAttribFlags::kColor),
+ VertexAttribFlags::kTextureCoord | VertexAttribFlags::kColor,
&vertexData[0].x, &vertexData[0].u, &vertexData[0].r,
kColorTextureVertexStride };
mOutGlop->mesh.elementCount = elementCount;
@@ -180,7 +180,7 @@
mOutGlop->mesh.indices = { 0, vertexBuffer.getIndices() };
mOutGlop->mesh.vertices = {
0,
- alphaVertex ? VertexAttribFlags::kAlpha : VertexAttribFlags::kNone,
+ static_cast<int>(alphaVertex ? VertexAttribFlags::kAlpha : VertexAttribFlags::kNone),
vertexBuffer.getBuffer(), nullptr, nullptr,
alphaVertex ? kAlphaVertexStride : kVertexStride };
mOutGlop->mesh.elementCount = indices
@@ -197,7 +197,7 @@
mOutGlop->mesh.indices = { mRenderState.meshState().getQuadListIBO(), nullptr };
mOutGlop->mesh.vertices = {
mCaches.patchCache.getMeshBuffer(),
- VertexAttribFlags::kTextureCoord,
+ static_cast<int>(VertexAttribFlags::kTextureCoord),
(void*)patch.positionOffset, (void*)patch.textureOffset, nullptr,
kTextureVertexStride };
mOutGlop->mesh.elementCount = patch.indexCount;
@@ -230,7 +230,7 @@
mOutGlop->blend = { GL_ZERO, GL_ZERO };
if (mOutGlop->fill.color.a < 1.0f
- || (mOutGlop->mesh.vertices.flags & VertexAttribFlags::kAlpha)
+ || (mOutGlop->mesh.vertices.attribFlags & VertexAttribFlags::kAlpha)
|| (mOutGlop->fill.texture.texture && mOutGlop->fill.texture.texture->blend)
|| mOutGlop->roundRectClipState
|| PaintUtils::isBlendedShader(shader)
@@ -324,7 +324,7 @@
const bool SWAP_SRC_DST = false;
if (alphaScale < 1.0f
- || (mOutGlop->mesh.vertices.flags & VertexAttribFlags::kAlpha)
+ || (mOutGlop->mesh.vertices.attribFlags & VertexAttribFlags::kAlpha)
|| texture.blend
|| mOutGlop->roundRectClipState) {
Blend::getFactors(SkXfermode::kSrcOver_Mode, SWAP_SRC_DST,
@@ -540,12 +540,25 @@
////////////////////////////////////////////////////////////////////////////////
void verify(const ProgramDescription& description, const Glop& glop) {
- bool hasTexture = glop.fill.texture.texture != nullptr;
- LOG_ALWAYS_FATAL_IF(description.hasTexture && description.hasExternalTexture);
- LOG_ALWAYS_FATAL_IF((description.hasTexture || description.hasExternalTexture )!= hasTexture);
- LOG_ALWAYS_FATAL_IF((glop.mesh.vertices.flags & VertexAttribFlags::kTextureCoord) != hasTexture);
+ if (glop.fill.texture.texture != nullptr) {
+ LOG_ALWAYS_FATAL_IF(((description.hasTexture && description.hasExternalTexture)
+ || (!description.hasTexture && !description.hasExternalTexture)
+ || ((glop.mesh.vertices.attribFlags & VertexAttribFlags::kTextureCoord) == 0)),
+ "Texture %p, hT%d, hET %d, attribFlags %x",
+ glop.fill.texture.texture,
+ description.hasTexture, description.hasExternalTexture,
+ glop.mesh.vertices.attribFlags);
+ } else {
+ LOG_ALWAYS_FATAL_IF((description.hasTexture
+ || description.hasExternalTexture
+ || ((glop.mesh.vertices.attribFlags & VertexAttribFlags::kTextureCoord) != 0)),
+ "No texture, hT%d, hET %d, attribFlags %x",
+ description.hasTexture, description.hasExternalTexture,
+ glop.mesh.vertices.attribFlags);
+ }
- if ((glop.mesh.vertices.flags & VertexAttribFlags::kAlpha) && glop.mesh.vertices.bufferObject) {
+ if ((glop.mesh.vertices.attribFlags & VertexAttribFlags::kAlpha)
+ && glop.mesh.vertices.bufferObject) {
LOG_ALWAYS_FATAL("VBO and alpha attributes are not currently compatible");
}
@@ -556,12 +569,12 @@
void GlopBuilder::build() {
REQUIRE_STAGES(kAllStages);
- if (mOutGlop->mesh.vertices.flags & VertexAttribFlags::kTextureCoord) {
+ if (mOutGlop->mesh.vertices.attribFlags & VertexAttribFlags::kTextureCoord) {
mDescription.hasTexture = mOutGlop->fill.texture.target == GL_TEXTURE_2D;
mDescription.hasExternalTexture = mOutGlop->fill.texture.target == GL_TEXTURE_EXTERNAL_OES;
}
- mDescription.hasColors = mOutGlop->mesh.vertices.flags & VertexAttribFlags::kColor;
- mDescription.hasVertexAlpha = mOutGlop->mesh.vertices.flags & VertexAttribFlags::kAlpha;
+ mDescription.hasColors = mOutGlop->mesh.vertices.attribFlags & VertexAttribFlags::kColor;
+ mDescription.hasVertexAlpha = mOutGlop->mesh.vertices.attribFlags & VertexAttribFlags::kAlpha;
// serialize shader info into ShaderData
GLuint textureUnit = mOutGlop->fill.texture.texture ? 1 : 0;
diff --git a/libs/hwui/renderstate/RenderState.cpp b/libs/hwui/renderstate/RenderState.cpp
index ca3a4c2..7b44d6d 100644
--- a/libs/hwui/renderstate/RenderState.cpp
+++ b/libs/hwui/renderstate/RenderState.cpp
@@ -259,7 +259,7 @@
// indices
meshState().bindIndicesBufferInternal(indices.bufferObject);
- if (vertices.flags & VertexAttribFlags::kTextureCoord) {
+ if (vertices.attribFlags & VertexAttribFlags::kTextureCoord) {
const Glop::Fill::TextureData& texture = fill.texture;
// texture always takes slot 0, shader samplers increment from there
mCaches->textureState().activateTexture(0);
@@ -283,13 +283,13 @@
meshState().disableTexCoordsVertexArray();
}
int colorLocation = -1;
- if (vertices.flags & VertexAttribFlags::kColor) {
+ if (vertices.attribFlags & VertexAttribFlags::kColor) {
colorLocation = fill.program->getAttrib("colors");
glEnableVertexAttribArray(colorLocation);
glVertexAttribPointer(colorLocation, 4, GL_FLOAT, GL_FALSE, vertices.stride, vertices.color);
}
int alphaLocation = -1;
- if (vertices.flags & VertexAttribFlags::kAlpha) {
+ if (vertices.attribFlags & VertexAttribFlags::kAlpha) {
// NOTE: alpha vertex position is computed assuming no VBO
const void* alphaCoords = ((const GLbyte*) vertices.position) + kVertexAlphaOffset;
alphaLocation = fill.program->getAttrib("vtxAlpha");
@@ -317,7 +317,7 @@
// rebind pointers without forcing, since initial bind handled above
meshState().bindPositionVertexPointer(false, vertexData, vertices.stride);
- if (vertices.flags & VertexAttribFlags::kTextureCoord) {
+ if (vertices.attribFlags & VertexAttribFlags::kTextureCoord) {
meshState().bindTexCoordsVertexPointer(false,
vertexData + kMeshTextureOffset, vertices.stride);
}
@@ -335,10 +335,10 @@
// -----------------------------------
// ---------- Mesh teardown ----------
// -----------------------------------
- if (vertices.flags & VertexAttribFlags::kAlpha) {
+ if (vertices.attribFlags & VertexAttribFlags::kAlpha) {
glDisableVertexAttribArray(alphaLocation);
}
- if (vertices.flags & VertexAttribFlags::kColor) {
+ if (vertices.attribFlags & VertexAttribFlags::kColor) {
glDisableVertexAttribArray(colorLocation);
}
}
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index 09f4bac..4526839 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -279,6 +279,7 @@
* Internal use only
* @return a combined mask of all flags
*/
+ @SystemApi
public int getAllFlags() {
return (mFlags & FLAG_ALL);
}
@@ -541,14 +542,15 @@
/**
* @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 de10ef9..259fe37 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -20,6 +20,7 @@
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 @@
/**
* @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 @@
* construction.
* @throws IllegalArgumentException
*/
+ @SystemApi
public AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int sessionId) throws IllegalArgumentException {
mRecordingState = RECORDSTATE_STOPPED;
@@ -376,7 +377,7 @@
// 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 8d6a588..824a7ad 100644
--- a/media/java/android/media/ImageReader.java
+++ b/media/java/android/media/ImageReader.java
@@ -483,6 +483,8 @@
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 97b3f63..58c86f2 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 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 @@
* This is a hidden audio source.
* @hide
*/
- protected static final int HOTWORD = 1999;
+ @SystemApi
+ public static final int HOTWORD = 1999;
}
/**
diff --git a/media/jni/android_media_ImageReader.cpp b/media/jni/android_media_ImageReader.cpp
index cf69b8f..b247493 100644
--- a/media/jni/android_media_ImageReader.cpp
+++ b/media/jni/android_media_ImageReader.cpp
@@ -95,6 +95,9 @@
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 @@
jobject mWeakThiz;
jclass mClazz;
int mFormat;
+ android_dataspace mDataSpace;
int mWidth;
int mHeight;
};
@@ -263,29 +267,6 @@
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 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 @@
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_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 @@
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 @@
{
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 @@
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 @@
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 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 @@
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 @@
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 @@
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 dfe2844..1205f9d 100644
--- a/media/jni/soundpool/SoundPool.cpp
+++ b/media/jni/soundpool/SoundPool.cpp
@@ -256,7 +256,7 @@
dump();
// allocate a channel
- channel = allocateChannel_l(priority, sampleID);
+ channel = allocateChannel_l(priority);
// no channel allocated - return 0
if (!channel) {
@@ -271,25 +271,13 @@
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 @@
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 @@
void SoundChannel::init(SoundPool* soundPool)
{
mSoundPool = soundPool;
- mPrevSampleID = -1;
}
// call with sound pool lock held
@@ -669,7 +656,7 @@
{
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 @@
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 @@
}
#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);
+ // 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());
+ // 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 @@
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 f520406..d19cd91 100644
--- a/media/jni/soundpool/SoundPool.h
+++ b/media/jni/soundpool/SoundPool.h
@@ -72,8 +72,8 @@
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 @@
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 @@
int mAudioBufferSize;
unsigned long mToggle;
bool mAutoPaused;
- int mPrevSampleID;
};
// application object for managing a pool of sounds
@@ -195,7 +193,7 @@
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 3cae19d..d756d05 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 @@
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 @@
@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 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 0000000..8ca5634
--- /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 94f77c6..07fcb82 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 37d9a73..8347a22 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.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.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.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 @@
// 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 @@
@Override
public void run() {
for (StatusBarNotification sbn : notifications) {
+ processForRemoteInput(sbn.getNotification());
addNotification(sbn, currentRanking);
}
}
@@ -424,6 +431,7 @@
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 @@
(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 @@
}
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 @@
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 eba7d9f..63bbf97 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 @@
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 0000000..7d721c2
--- /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/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index d8a9d3e..46f07cc 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -2401,7 +2401,7 @@
}
ps.addCpuTimeLocked(st.rel_utime - otherUTime,
st.rel_stime - otherSTime, cpuSpeedTimes);
- pr.curCpuTime += (st.rel_utime+st.rel_stime) * 10;
+ pr.curCpuTime += st.rel_utime + st.rel_stime;
} else {
BatteryStatsImpl.Uid.Proc ps = st.batteryStats;
if (ps == null || !ps.isActive()) {
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index 7a29a88..8fe1238 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -305,17 +305,16 @@
r.resultExtras, r.resultAbort, false);
reschedule = true;
}
-
- r = mPendingBroadcast;
- if (r != null && r.curApp == app) {
+ if (r == null && mPendingBroadcast != null && mPendingBroadcast.curApp == app) {
if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST,
"[" + mQueueName + "] skip & discard pending app " + r);
+ r = mPendingBroadcast;
+ }
+
+ if (r != null) {
logBroadcastReceiverDiscardLocked(r);
finishReceiverLocked(r, r.resultCode, r.resultData,
r.resultExtras, r.resultAbort, false);
- reschedule = true;
- }
- if (reschedule) {
scheduleBroadcastsLocked();
}
}
@@ -512,10 +511,11 @@
}
}
try {
- if (DEBUG_BROADCAST_LIGHT) Slog.i(TAG_BROADCAST, "Delivering to " + filter + " : " + r);
+ if (DEBUG_BROADCAST_LIGHT) Slog.i(TAG_BROADCAST,
+ "Delivering to " + filter + " : " + r);
performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver,
- new Intent(r.intent), r.resultCode, r.resultData,
- r.resultExtras, r.ordered, r.initialSticky, r.userId);
+ new Intent(r.intent), r.resultCode, r.resultData,
+ r.resultExtras, r.ordered, r.initialSticky, r.userId);
if (ordered) {
r.state = BroadcastRecord.CALL_DONE_RECEIVE;
}
@@ -574,11 +574,9 @@
// broadcast, then do nothing at this point. Just in case, we
// check that the process we're waiting for still exists.
if (mPendingBroadcast != null) {
- if (DEBUG_BROADCAST_LIGHT) {
- Slog.v(TAG_BROADCAST, "processNextBroadcast ["
- + mQueueName + "]: waiting for "
- + mPendingBroadcast.curApp);
- }
+ if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST,
+ "processNextBroadcast [" + mQueueName + "]: waiting for "
+ + mPendingBroadcast.curApp);
boolean isDead;
synchronized (mService.mPidsSelfLocked) {
@@ -677,8 +675,8 @@
if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Cancelling BROADCAST_TIMEOUT_MSG");
cancelBroadcastTimeoutLocked();
- if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST, "Finished with ordered broadcast "
- + r);
+ if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST,
+ "Finished with ordered broadcast " + r);
// ... and on to the next...
addBroadcastToHistoryLocked(r);
@@ -834,19 +832,18 @@
+ info.activityInfo.packageName, e);
}
if (!isAvailable) {
- if (DEBUG_BROADCAST) {
- Slog.v(TAG_BROADCAST, "Skipping delivery to " + info.activityInfo.packageName
- + " / " + info.activityInfo.applicationInfo.uid
- + " : package no longer available");
- }
+ if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST,
+ "Skipping delivery to " + info.activityInfo.packageName + " / "
+ + info.activityInfo.applicationInfo.uid
+ + " : package no longer available");
skip = true;
}
}
if (skip) {
if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST,
- "Skipping delivery of ordered ["
- + mQueueName + "] " + r + " for whatever reason");
+ "Skipping delivery of ordered [" + mQueueName + "] "
+ + r + " for whatever reason");
r.receiver = null;
r.curFilter = null;
r.state = BroadcastRecord.IDLE;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 5d386bd..61a7263 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -3620,7 +3620,9 @@
pw.print(" Devices: ");
final int devices = AudioSystem.getDevicesForStream(mStreamType);
int device, i = 0, n = 0;
- while ((device = 1 << i) <= AudioSystem.DEVICE_OUT_DEFAULT) {
+ // iterate all devices from 1 to DEVICE_OUT_DEFAULT exclusive
+ // (the default device is not returned by getDevicesForStream)
+ while ((device = 1 << i) != AudioSystem.DEVICE_OUT_DEFAULT) {
if ((devices & device) != 0) {
if (n++ > 0) {
pw.print(", ");
diff --git a/wifi/java/android/net/wifi/WifiActivityEnergyInfo.java b/wifi/java/android/net/wifi/WifiActivityEnergyInfo.java
index 533b8bc..6263463 100644
--- a/wifi/java/android/net/wifi/WifiActivityEnergyInfo.java
+++ b/wifi/java/android/net/wifi/WifiActivityEnergyInfo.java
@@ -21,37 +21,37 @@
/**
* 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 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 void writeToParcel(Parcel out, int flags) {
+ out.writeLong(mTimestamp);
out.writeInt(mStackState);
out.writeInt(mControllerTxTimeMs);
out.writeInt(mControllerRxTimeMs);
@@ -127,7 +129,7 @@
* @return timestamp(wall clock) of record creation
*/
public long getTimeStamp() {
- return timestamp;
+ return mTimestamp;
}
/**