Merge "Cache photo URI too in calllog (contract)"
diff --git a/api/current.txt b/api/current.txt
index cdbf2ef1..489fd05 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 d080a63..7fa8d9b 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/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 4a3650f..e23ffe4 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -3681,9 +3681,14 @@
/**
* Check whether the current user has been blocked by device policy from uninstalling a package.
* Requires the caller to be the profile owner if checking a specific admin's policy.
+ * <p>
+ * <strong>Note:</strong> Starting from {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}, the
+ * behavior of this API is changed such that passing <code>null</code> as the <code>admin</code>
+ * parameter will return if any admin has blocked the uninstallation. Before L MR1, passing
+ * <code>null</code> will cause a NullPointerException to be raised.
*
* @param admin The name of the admin component whose blocking policy will be checked, or null
- * to check if any admin has blocked the uninstallation.
+ * to check if any admin has blocked the uninstallation.
* @param packageName package to check.
* @return true if uninstallation is blocked.
*/
diff --git a/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java b/core/java/android/bluetooth/BluetoothActivityEnergyInfo.java
index 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/GLES20Canvas.java b/core/java/android/view/DisplayListCanvas.java
similarity index 85%
rename from core/java/android/view/GLES20Canvas.java
rename to core/java/android/view/DisplayListCanvas.java
index 06e196d..90e1f86 100644
--- a/core/java/android/view/GLES20Canvas.java
+++ b/core/java/android/view/DisplayListCanvas.java
@@ -16,6 +16,7 @@
package android.view;
+import android.annotation.NonNull;
import android.graphics.Bitmap;
import android.graphics.CanvasProperty;
import android.graphics.NinePatch;
@@ -24,19 +25,50 @@
import android.graphics.Picture;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.util.Pools.SynchronizedPool;
/**
- * An implementation of Canvas on top of OpenGL ES 2.0.
+ * An implementation of a GL canvas that records drawing operations.
+ * This is intended for use with a DisplayList. This class keeps a list of all the Paint and
+ * Bitmap objects that it draws, preventing the backing memory of Bitmaps from being freed while
+ * the DisplayList is still holding a native reference to the memory.
*/
-class GLES20Canvas extends HardwareCanvas {
+class DisplayListCanvas extends HardwareCanvas {
+ // The recording canvas pool should be large enough to handle a deeply nested
+ // view hierarchy because display lists are generated recursively.
+ private static final int POOL_LIMIT = 25;
+
+ private static final SynchronizedPool<DisplayListCanvas> sPool =
+ new SynchronizedPool<DisplayListCanvas>(POOL_LIMIT);
+
+ RenderNode mNode;
private int mWidth;
private int mHeight;
- private float[] mPoint;
- private float[] mLine;
- private Rect mClipBounds;
- private RectF mPathBounds;
+ static DisplayListCanvas obtain(@NonNull RenderNode node) {
+ if (node == null) throw new IllegalArgumentException("node cannot be null");
+ DisplayListCanvas canvas = sPool.acquire();
+ if (canvas == null) {
+ canvas = new DisplayListCanvas();
+ }
+ canvas.mNode = node;
+ return canvas;
+ }
+
+ void recycle() {
+ mNode = null;
+ sPool.release(this);
+ }
+
+ long finishRecording() {
+ return nFinishRecording(mNativeCanvasWrapper);
+ }
+
+ @Override
+ public boolean isRecordingFor(Object o) {
+ return o == mNode;
+ }
///////////////////////////////////////////////////////////////////////////
// JNI
@@ -53,8 +85,8 @@
// Constructors
///////////////////////////////////////////////////////////////////////////
- // TODO: Merge with GLES20RecordingCanvas
- protected GLES20Canvas() {
+
+ private DisplayListCanvas() {
super(nCreateDisplayListRenderer());
}
diff --git a/core/java/android/view/GLES20RecordingCanvas.java b/core/java/android/view/GLES20RecordingCanvas.java
deleted file mode 100644
index 6c780c9..0000000
--- a/core/java/android/view/GLES20RecordingCanvas.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2010 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.view;
-
-import android.annotation.NonNull;
-import android.util.Pools.SynchronizedPool;
-
-/**
- * An implementation of a GL canvas that records drawing operations.
- * This is intended for use with a DisplayList. This class keeps a list of all the Paint and
- * Bitmap objects that it draws, preventing the backing memory of Bitmaps from being freed while
- * the DisplayList is still holding a native reference to the memory.
- */
-class GLES20RecordingCanvas extends GLES20Canvas {
- // The recording canvas pool should be large enough to handle a deeply nested
- // view hierarchy because display lists are generated recursively.
- private static final int POOL_LIMIT = 25;
-
- private static final SynchronizedPool<GLES20RecordingCanvas> sPool =
- new SynchronizedPool<GLES20RecordingCanvas>(POOL_LIMIT);
-
- RenderNode mNode;
-
- private GLES20RecordingCanvas() {
- super();
- }
-
- static GLES20RecordingCanvas obtain(@NonNull RenderNode node) {
- if (node == null) throw new IllegalArgumentException("node cannot be null");
- GLES20RecordingCanvas canvas = sPool.acquire();
- if (canvas == null) {
- canvas = new GLES20RecordingCanvas();
- }
- canvas.mNode = node;
- return canvas;
- }
-
- void recycle() {
- mNode = null;
- sPool.release(this);
- }
-
- long finishRecording() {
- return nFinishRecording(mNativeCanvasWrapper);
- }
-
- @Override
- public boolean isRecordingFor(Object o) {
- return o == mNode;
- }
-}
diff --git a/core/java/android/view/HardwareCanvas.java b/core/java/android/view/HardwareCanvas.java
index cdb350f..fc2b55b 100644
--- a/core/java/android/view/HardwareCanvas.java
+++ b/core/java/android/view/HardwareCanvas.java
@@ -119,6 +119,6 @@
CanvasProperty<Paint> paint);
public static void setProperty(String name, String value) {
- GLES20Canvas.setProperty(name, value);
+ DisplayListCanvas.setProperty(name, value);
}
}
diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java
index 9921be2..afa7f51 100644
--- a/core/java/android/view/HardwareRenderer.java
+++ b/core/java/android/view/HardwareRenderer.java
@@ -205,7 +205,7 @@
* false otherwise
*/
public static boolean isAvailable() {
- return GLES20Canvas.isAvailable();
+ return DisplayListCanvas.isAvailable();
}
/**
@@ -423,7 +423,7 @@
*/
static HardwareRenderer create(Context context, boolean translucent) {
HardwareRenderer renderer = null;
- if (GLES20Canvas.isAvailable()) {
+ if (DisplayListCanvas.isAvailable()) {
renderer = new ThreadedRenderer(context, translucent);
}
return renderer;
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/view/RenderNode.java b/core/java/android/view/RenderNode.java
index 09eb486..38867a8 100644
--- a/core/java/android/view/RenderNode.java
+++ b/core/java/android/view/RenderNode.java
@@ -225,7 +225,7 @@
* @see #isValid()
*/
public HardwareCanvas start(int width, int height) {
- HardwareCanvas canvas = GLES20RecordingCanvas.obtain(this);
+ HardwareCanvas canvas = DisplayListCanvas.obtain(this);
canvas.setViewport(width, height);
// The dirty rect should always be null for a display list
canvas.onPreDraw(null);
@@ -241,11 +241,11 @@
* @see #isValid()
*/
public void end(HardwareCanvas endCanvas) {
- if (!(endCanvas instanceof GLES20RecordingCanvas)) {
+ if (!(endCanvas instanceof DisplayListCanvas)) {
throw new IllegalArgumentException("Passed an invalid canvas to end!");
}
- GLES20RecordingCanvas canvas = (GLES20RecordingCanvas) endCanvas;
+ DisplayListCanvas canvas = (DisplayListCanvas) endCanvas;
canvas.onPostDraw();
long renderNodeData = canvas.finishRecording();
nSetDisplayListData(mNativeRenderNode, renderNodeData);
diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java
index 7b35a3b..379796d 100644
--- a/core/java/android/view/RenderNodeAnimator.java
+++ b/core/java/android/view/RenderNodeAnimator.java
@@ -283,10 +283,10 @@
}
public void setTarget(Canvas canvas) {
- if (!(canvas instanceof GLES20RecordingCanvas)) {
+ if (!(canvas instanceof DisplayListCanvas)) {
throw new IllegalArgumentException("Not a GLES20RecordingCanvas");
}
- final GLES20RecordingCanvas recordingCanvas = (GLES20RecordingCanvas) canvas;
+ final DisplayListCanvas recordingCanvas = (DisplayListCanvas) canvas;
setTarget(recordingCanvas.mNode);
}
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 8197b3d..cc44577 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;
@@ -4885,9 +4887,10 @@
text.insert(newTextInsertAt, newText);
}
}
- // Restore the cursor position.
+ // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
+ // don't explicitly set it and rely on SpannableStringBuilder to position it.
// TODO: Select all the text that was undone.
- if (newCursorPos <= text.length()) {
+ if (0 <= newCursorPos && newCursorPos <= text.length()) {
Selection.setSelection(text, newCursorPos);
}
}
diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java
index a224f5e..d12739f 100644
--- a/core/java/android/widget/RelativeLayout.java
+++ b/core/java/android/widget/RelativeLayout.java
@@ -515,6 +515,23 @@
}
}
+ // Use the top-start-most laid out view as the baseline. RTL offsets are
+ // applied later, so we can use the left-most edge as the starting edge.
+ View baselineView = null;
+ LayoutParams baselineParams = null;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final LayoutParams childParams = (LayoutParams) child.getLayoutParams();
+ if (baselineView == null || baselineParams == null
+ || compareLayoutPosition(childParams, baselineParams) < 0) {
+ baselineView = child;
+ baselineParams = childParams;
+ }
+ }
+ }
+ mBaselineView = baselineView;
+
if (isWrapContentWidth) {
// Width already has left padding in it since it was calculated by looking at
// the right of each child view
@@ -616,25 +633,24 @@
}
}
- // Use the bottom-most laid out view as the baseline.
- View baselineView = null;
- int baseline = 0;
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() != GONE) {
- final int childBaseline = child.getBaseline();
- if (childBaseline >= baseline) {
- baselineView = child;
- baseline = childBaseline;
- }
- }
- }
- mBaselineView = baselineView;
-
setMeasuredDimension(width, height);
}
/**
+ * @return a negative number if the top of {@code p1} is above the top of
+ * {@code p2} or if they have identical top values and the left of
+ * {@code p1} is to the left of {@code p2}, or a positive number
+ * otherwise
+ */
+ private int compareLayoutPosition(LayoutParams p1, LayoutParams p2) {
+ final int topDiff = p1.mTop - p2.mTop;
+ if (topDiff != 0) {
+ return topDiff;
+ }
+ return p1.mLeft - p2.mLeft;
+ }
+
+ /**
* Measure a child. The child should have left, top, right and bottom information
* stored in its LayoutParams. If any of these values is VALUE_NOT_SET it means
* that the view can extend up to the corresponding edge.
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.mk b/core/jni/Android.mk
index 97744ea..30a7e68 100644
--- a/core/jni/Android.mk
+++ b/core/jni/Android.mk
@@ -18,6 +18,8 @@
LOCAL_CFLAGS += -DGL_GLEXT_PROTOTYPES -DEGL_EGLEXT_PROTOTYPES
+LOCAL_CFLAGS += -DU_USING_ICU_NAMESPACE=0
+
LOCAL_SRC_FILES:= \
AndroidRuntime.cpp \
com_android_internal_content_NativeLibraryHelper.cpp \
@@ -41,25 +43,25 @@
android_database_SQLiteDebug.cpp \
android_emoji_EmojiFactory.cpp \
android_view_DisplayEventReceiver.cpp \
- android_view_Surface.cpp \
- android_view_SurfaceControl.cpp \
- android_view_SurfaceSession.cpp \
- android_view_TextureView.cpp \
+ android_view_DisplayListCanvas.cpp \
+ android_view_GraphicBuffer.cpp \
+ android_view_HardwareLayer.cpp \
android_view_InputChannel.cpp \
android_view_InputDevice.cpp \
android_view_InputEventReceiver.cpp \
android_view_InputEventSender.cpp \
android_view_InputQueue.cpp \
- android_view_KeyEvent.cpp \
android_view_KeyCharacterMap.cpp \
- android_view_GraphicBuffer.cpp \
- android_view_GLES20Canvas.cpp \
- android_view_HardwareLayer.cpp \
- android_view_ThreadedRenderer.cpp \
+ android_view_KeyEvent.cpp \
android_view_MotionEvent.cpp \
android_view_PointerIcon.cpp \
android_view_RenderNode.cpp \
android_view_RenderNodeAnimator.cpp \
+ android_view_Surface.cpp \
+ android_view_SurfaceControl.cpp \
+ android_view_SurfaceSession.cpp \
+ android_view_TextureView.cpp \
+ android_view_ThreadedRenderer.cpp \
android_view_VelocityTracker.cpp \
android_text_AndroidCharacter.cpp \
android_text_AndroidBidi.cpp \
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 77afd05..ad52e3f 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -127,16 +127,16 @@
extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env);
extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env);
extern int register_android_view_DisplayEventReceiver(JNIEnv* env);
+extern int register_android_view_DisplayListCanvas(JNIEnv* env);
+extern int register_android_view_GraphicBuffer(JNIEnv* env);
+extern int register_android_view_HardwareLayer(JNIEnv* env);
extern int register_android_view_RenderNode(JNIEnv* env);
extern int register_android_view_RenderNodeAnimator(JNIEnv* env);
-extern int register_android_view_GraphicBuffer(JNIEnv* env);
-extern int register_android_view_GLES20Canvas(JNIEnv* env);
-extern int register_android_view_HardwareLayer(JNIEnv* env);
-extern int register_android_view_ThreadedRenderer(JNIEnv* env);
extern int register_android_view_Surface(JNIEnv* env);
extern int register_android_view_SurfaceControl(JNIEnv* env);
extern int register_android_view_SurfaceSession(JNIEnv* env);
extern int register_android_view_TextureView(JNIEnv* env);
+extern int register_android_view_ThreadedRenderer(JNIEnv* env);
extern int register_com_android_internal_view_animation_NativeInterpolatorFactoryHelper(JNIEnv *env);
extern int register_android_database_CursorWindow(JNIEnv* env);
extern int register_android_database_SQLiteConnection(JNIEnv* env);
@@ -1179,7 +1179,7 @@
REG_JNI(register_android_view_RenderNode),
REG_JNI(register_android_view_RenderNodeAnimator),
REG_JNI(register_android_view_GraphicBuffer),
- REG_JNI(register_android_view_GLES20Canvas),
+ REG_JNI(register_android_view_DisplayListCanvas),
REG_JNI(register_android_view_HardwareLayer),
REG_JNI(register_android_view_ThreadedRenderer),
REG_JNI(register_android_view_Surface),
diff --git a/core/jni/android_text_StaticLayout.cpp b/core/jni/android_text_StaticLayout.cpp
index a6f19b1..e5ae147 100644
--- a/core/jni/android_text_StaticLayout.cpp
+++ b/core/jni/android_text_StaticLayout.cpp
@@ -48,10 +48,10 @@
delete mBreakIterator;
}
- void setLocale(const Locale& locale) {
+ void setLocale(const icu::Locale& locale) {
delete mBreakIterator;
UErrorCode status = U_ZERO_ERROR;
- mBreakIterator = BreakIterator::createLineInstance(locale, status);
+ mBreakIterator = icu::BreakIterator::createLineInstance(locale, status);
// TODO: check status
}
@@ -77,13 +77,13 @@
}
}
- BreakIterator* breakIterator() const {
+ icu::BreakIterator* breakIterator() const {
return mBreakIterator;
}
private:
const size_t MAX_TEXT_BUF_RETAIN = 32678;
- BreakIterator* mBreakIterator = nullptr;
+ icu::BreakIterator* mBreakIterator = nullptr;
UText mUText = UTEXT_INITIALIZER;
std::vector<uint16_t>mTextBuf;
};
@@ -560,9 +560,9 @@
// TODO: this array access is pretty inefficient, but we'll replace it anyway
ScopedFloatArrayRO widthsScopedArr(env, widths);
- BreakIterator* breakIterator = b->breakIterator();
+ icu::BreakIterator* breakIterator = b->breakIterator();
int loc = breakIterator->first();
- while ((loc = breakIterator->next()) != BreakIterator::DONE) {
+ while ((loc = breakIterator->next()) != icu::BreakIterator::DONE) {
breaks.push_back(loc);
}
diff --git a/core/jni/android_view_GLES20Canvas.cpp b/core/jni/android_view_DisplayListCanvas.cpp
similarity index 82%
rename from core/jni/android_view_GLES20Canvas.cpp
rename to core/jni/android_view_DisplayListCanvas.cpp
index 0bee7ae..f2e6c4b 100644
--- a/core/jni/android_view_GLES20Canvas.cpp
+++ b/core/jni/android_view_DisplayListCanvas.cpp
@@ -50,43 +50,43 @@
// Setup
// ----------------------------------------------------------------------------
-static void android_view_GLES20Canvas_setViewport(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_setViewport(JNIEnv* env, jobject clazz,
jlong rendererPtr, jint width, jint height) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
renderer->setViewport(width, height);
}
-static void android_view_GLES20Canvas_setHighContrastText(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_setHighContrastText(JNIEnv* env, jobject clazz,
jlong rendererPtr, jboolean highContrastText) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
renderer->setHighContrastText(highContrastText);
}
-static void android_view_GLES20Canvas_insertReorderBarrier(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_insertReorderBarrier(JNIEnv* env, jobject clazz,
jlong rendererPtr, jboolean reorderEnable) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
renderer->insertReorderBarrier(reorderEnable);
}
-static void android_view_GLES20Canvas_prepare(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_prepare(JNIEnv* env, jobject clazz,
jlong rendererPtr) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
renderer->prepare();
}
-static void android_view_GLES20Canvas_prepareDirty(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_prepareDirty(JNIEnv* env, jobject clazz,
jlong rendererPtr, jint left, jint top, jint right, jint bottom) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
renderer->prepareDirty(left, top, right, bottom);
}
-static void android_view_GLES20Canvas_finish(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_finish(JNIEnv* env, jobject clazz,
jlong rendererPtr) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
renderer->finish();
}
-static void android_view_GLES20Canvas_setProperty(JNIEnv* env,
+static void android_view_DisplayListCanvas_setProperty(JNIEnv* env,
jobject clazz, jstring name, jstring value) {
if (!Caches::hasInstance()) {
ALOGW("can't set property, no Caches instance");
@@ -108,7 +108,7 @@
// Functor
// ----------------------------------------------------------------------------
-static void android_view_GLES20Canvas_callDrawGLFunction(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_callDrawGLFunction(JNIEnv* env, jobject clazz,
jlong rendererPtr, jlong functorPtr) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
Functor* functor = reinterpret_cast<Functor*>(functorPtr);
@@ -120,11 +120,11 @@
// Misc
// ----------------------------------------------------------------------------
-static jint android_view_GLES20Canvas_getMaxTextureWidth(JNIEnv* env, jobject clazz) {
+static jint android_view_DisplayListCanvas_getMaxTextureWidth(JNIEnv* env, jobject clazz) {
return Caches::getInstance().maxTextureSize;
}
-static jint android_view_GLES20Canvas_getMaxTextureHeight(JNIEnv* env, jobject clazz) {
+static jint android_view_DisplayListCanvas_getMaxTextureHeight(JNIEnv* env, jobject clazz) {
return Caches::getInstance().maxTextureSize;
}
@@ -132,7 +132,7 @@
// Drawing
// ----------------------------------------------------------------------------
-static void android_view_GLES20Canvas_drawPatch(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_drawPatch(JNIEnv* env, jobject clazz,
jlong rendererPtr, jlong bitmapPtr, jlong patchPtr,
float left, float top, float right, float bottom, jlong paintPtr) {
SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapPtr);
@@ -143,7 +143,7 @@
renderer->drawPatch(bitmap, patch, left, top, right, bottom, paint);
}
-static void android_view_GLES20Canvas_drawRoundRectProps(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_drawRoundRectProps(JNIEnv* env, jobject clazz,
jlong rendererPtr, jlong leftPropPtr, jlong topPropPtr, jlong rightPropPtr,
jlong bottomPropPtr, jlong rxPropPtr, jlong ryPropPtr, jlong paintPropPtr) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
@@ -157,7 +157,7 @@
renderer->drawRoundRect(leftProp, topProp, rightProp, bottomProp, rxProp, ryProp, paintProp);
}
-static void android_view_GLES20Canvas_drawCircleProps(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_drawCircleProps(JNIEnv* env, jobject clazz,
jlong rendererPtr, jlong xPropPtr, jlong yPropPtr, jlong radiusPropPtr, jlong paintPropPtr) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
CanvasPropertyPrimitive* xProp = reinterpret_cast<CanvasPropertyPrimitive*>(xPropPtr);
@@ -167,7 +167,7 @@
renderer->drawCircle(xProp, yProp, radiusProp, paintProp);
}
-static void android_view_GLES20Canvas_drawRegionAsRects(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_drawRegionAsRects(JNIEnv* env, jobject clazz,
jlong rendererPtr, jlong regionPtr, jlong paintPtr) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
SkRegion* region = reinterpret_cast<SkRegion*>(regionPtr);
@@ -201,17 +201,17 @@
// Display lists
// ----------------------------------------------------------------------------
-static jlong android_view_GLES20Canvas_finishRecording(JNIEnv* env,
+static jlong android_view_DisplayListCanvas_finishRecording(JNIEnv* env,
jobject clazz, jlong rendererPtr) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
return reinterpret_cast<jlong>(renderer->finishRecording());
}
-static jlong android_view_GLES20Canvas_createDisplayListRenderer(JNIEnv* env, jobject clazz) {
+static jlong android_view_DisplayListCanvas_createDisplayListRenderer(JNIEnv* env, jobject clazz) {
return reinterpret_cast<jlong>(new DisplayListRenderer);
}
-static void android_view_GLES20Canvas_drawRenderNode(JNIEnv* env,
+static void android_view_DisplayListCanvas_drawRenderNode(JNIEnv* env,
jobject clazz, jlong rendererPtr, jlong renderNodePtr,
jint flags) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
@@ -224,7 +224,7 @@
// Layers
// ----------------------------------------------------------------------------
-static void android_view_GLES20Canvas_drawLayer(JNIEnv* env, jobject clazz,
+static void android_view_DisplayListCanvas_drawLayer(JNIEnv* env, jobject clazz,
jlong rendererPtr, jlong layerPtr, jfloat x, jfloat y) {
DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
DeferredLayerUpdater* layer = reinterpret_cast<DeferredLayerUpdater*>(layerPtr);
@@ -235,7 +235,7 @@
// Common
// ----------------------------------------------------------------------------
-static jboolean android_view_GLES20Canvas_isAvailable(JNIEnv* env, jobject clazz) {
+static jboolean android_view_DisplayListCanvas_isAvailable(JNIEnv* env, jobject clazz) {
char prop[PROPERTY_VALUE_MAX];
if (property_get("ro.kernel.qemu", prop, NULL) == 0) {
// not in the emulator
@@ -261,36 +261,36 @@
// JNI Glue
// ----------------------------------------------------------------------------
-const char* const kClassPathName = "android/view/GLES20Canvas";
+const char* const kClassPathName = "android/view/DisplayListCanvas";
static JNINativeMethod gMethods[] = {
- { "nIsAvailable", "()Z", (void*) android_view_GLES20Canvas_isAvailable },
- { "nSetViewport", "(JII)V", (void*) android_view_GLES20Canvas_setViewport },
- { "nSetHighContrastText","(JZ)V", (void*) android_view_GLES20Canvas_setHighContrastText },
- { "nInsertReorderBarrier","(JZ)V", (void*) android_view_GLES20Canvas_insertReorderBarrier },
- { "nPrepare", "(J)V", (void*) android_view_GLES20Canvas_prepare },
- { "nPrepareDirty", "(JIIII)V", (void*) android_view_GLES20Canvas_prepareDirty },
- { "nFinish", "(J)V", (void*) android_view_GLES20Canvas_finish },
+ { "nIsAvailable", "()Z", (void*) android_view_DisplayListCanvas_isAvailable },
+ { "nSetViewport", "(JII)V", (void*) android_view_DisplayListCanvas_setViewport },
+ { "nSetHighContrastText","(JZ)V", (void*) android_view_DisplayListCanvas_setHighContrastText },
+ { "nInsertReorderBarrier","(JZ)V", (void*) android_view_DisplayListCanvas_insertReorderBarrier },
+ { "nPrepare", "(J)V", (void*) android_view_DisplayListCanvas_prepare },
+ { "nPrepareDirty", "(JIIII)V", (void*) android_view_DisplayListCanvas_prepareDirty },
+ { "nFinish", "(J)V", (void*) android_view_DisplayListCanvas_finish },
{ "nSetProperty", "(Ljava/lang/String;Ljava/lang/String;)V",
- (void*) android_view_GLES20Canvas_setProperty },
+ (void*) android_view_DisplayListCanvas_setProperty },
- { "nCallDrawGLFunction", "(JJ)V", (void*) android_view_GLES20Canvas_callDrawGLFunction },
+ { "nCallDrawGLFunction", "(JJ)V", (void*) android_view_DisplayListCanvas_callDrawGLFunction },
- { "nDrawPatch", "(JJJFFFFJ)V", (void*) android_view_GLES20Canvas_drawPatch },
+ { "nDrawPatch", "(JJJFFFFJ)V", (void*) android_view_DisplayListCanvas_drawPatch },
- { "nDrawRects", "(JJJ)V", (void*) android_view_GLES20Canvas_drawRegionAsRects },
- { "nDrawRoundRect", "(JJJJJJJJ)V", (void*) android_view_GLES20Canvas_drawRoundRectProps },
- { "nDrawCircle", "(JJJJJ)V", (void*) android_view_GLES20Canvas_drawCircleProps },
+ { "nDrawRects", "(JJJ)V", (void*) android_view_DisplayListCanvas_drawRegionAsRects },
+ { "nDrawRoundRect", "(JJJJJJJJ)V", (void*) android_view_DisplayListCanvas_drawRoundRectProps },
+ { "nDrawCircle", "(JJJJJ)V", (void*) android_view_DisplayListCanvas_drawCircleProps },
- { "nFinishRecording", "(J)J", (void*) android_view_GLES20Canvas_finishRecording },
- { "nDrawRenderNode", "(JJI)V", (void*) android_view_GLES20Canvas_drawRenderNode },
+ { "nFinishRecording", "(J)J", (void*) android_view_DisplayListCanvas_finishRecording },
+ { "nDrawRenderNode", "(JJI)V", (void*) android_view_DisplayListCanvas_drawRenderNode },
- { "nCreateDisplayListRenderer", "()J", (void*) android_view_GLES20Canvas_createDisplayListRenderer },
+ { "nCreateDisplayListRenderer", "()J", (void*) android_view_DisplayListCanvas_createDisplayListRenderer },
- { "nDrawLayer", "(JJFF)V", (void*) android_view_GLES20Canvas_drawLayer },
+ { "nDrawLayer", "(JJFF)V", (void*) android_view_DisplayListCanvas_drawLayer },
- { "nGetMaximumTextureWidth", "()I", (void*) android_view_GLES20Canvas_getMaxTextureWidth },
- { "nGetMaximumTextureHeight", "()I", (void*) android_view_GLES20Canvas_getMaxTextureHeight },
+ { "nGetMaximumTextureWidth", "()I", (void*) android_view_DisplayListCanvas_getMaxTextureWidth },
+ { "nGetMaximumTextureHeight", "()I", (void*) android_view_DisplayListCanvas_getMaxTextureHeight },
};
static JNINativeMethod gActivityThreadMethods[] = {
@@ -298,7 +298,7 @@
(void*) android_app_ActivityThread_dumpGraphics }
};
-int register_android_view_GLES20Canvas(JNIEnv* env) {
+int register_android_view_DisplayListCanvas(JNIEnv* env) {
jclass clazz = FindClassOrDie(env, "android/graphics/Rect");
gRectClassInfo.set = GetMethodIDOrDie(env, clazz, "set", "(IIII)V");
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/docs/html/images/training/geofence.png b/docs/html/images/training/geofence.png
new file mode 100644
index 0000000..2d5d3aa
--- /dev/null
+++ b/docs/html/images/training/geofence.png
Binary files differ
diff --git a/docs/html/images/training/geofence@2x.png b/docs/html/images/training/geofence@2x.png
new file mode 100644
index 0000000..2f83105
--- /dev/null
+++ b/docs/html/images/training/geofence@2x.png
Binary files differ
diff --git a/docs/html/training/location/geofencing.jd b/docs/html/training/location/geofencing.jd
index 748b6ec..59fc4c6 100644
--- a/docs/html/training/location/geofencing.jd
+++ b/docs/html/training/location/geofencing.jd
@@ -9,9 +9,11 @@
<h2>This lesson teaches you to</h2>
<ol>
- <li><a href="#RequestGeofences">Request Geofence Monitoring</a></li>
+ <li><a href="#RequestGeofences">Set up for Geofence Monitoring</a></li>
+ <li><a href="#CreateAdd">Create and Add Geofences</a></li>
<li><a href="#HandleGeofenceTransitions">Handle Geofence Transitions</a></li>
<li><a href="#StopGeofenceMonitoring">Stop Geofence Monitoring</a></li>
+
</ol>
<h2>You should also read</h2>
@@ -23,577 +25,148 @@
<h2>Try it out</h2>
-<div class="download-box">
- <a href="http://developer.android.com/shareables/training/GeofenceDetection.zip" class="button">Download the sample</a>
- <p class="filename">GeofenceDetection.zip</p>
-</div>
+ <ul>
+ <li>
+ <a href="https://github.com/googlesamples/android-play-location/tree/master/Geofencing"
+ class="external-link">Geofencing</a>
+ </li>
+ </ul>
</div>
</div>
<p>
- Geofencing combines awareness of the user's current location with awareness of nearby
- features, defined as the user's proximity to locations that may be of interest. To mark a
+ Geofencing combines awareness of the user's current location with awareness of the user's
+ proximity to locations that may be of interest. To mark a
location of interest, you specify its latitude and longitude. To adjust the proximity for the
- location, you add a radius. The latitude, longitude, and radius define a geofence.
- You can have multiple active geofences at one time.
+ location, you add a radius. The latitude, longitude, and radius define a geofence, creating a
+ circular area, or fence, around the location of interest.
</p>
<p>
- Location Services treats a geofences as an area rather than as a points and proximity. This
- allows it to detect when the user enters or exits a geofence. For each geofence, you can ask
- Location Services to send you entrance events or exit events or both. You can also limit the
- duration of a geofence by specifying an expiration duration in milliseconds. After the geofence
- expires, Location Services automatically removes it.
+ You can have multiple active geofences, with a limit of 100 per device user. For each geofence,
+ you can ask Location Services to send you entrance and exit events, or you can specify a
+ duration within the geofence area to wait, or <em>dwell</em>, before triggering an event. You
+ can limit the duration of any geofence by specifying an expiration duration in milliseconds.
+ After the geofence expires, Location Services automatically removes it.
</p>
-<!--
- Send geofences to Location Services
- -->
-<h2 id="RequestGeofences">Request Geofence Monitoring</h2>
+
+<img src="{@docRoot}images/training/geofence@2x.png"
+srcset="{@docRoot}images/training/geofence.png 1x, {@docRoot}images/training/geofence@2x.png 2x" alt=""
+ width="400" height="400"/>
+<p>
+ This lesson shows you how to add and remove geofences, and then listen for geofence transitions
+ using an {@link android.app.IntentService}.</p>
+
+<h2 id="RequestGeofences">Set up for Geofence Monitoring</h2>
<p>
The first step in requesting geofence monitoring is to request the necessary permission.
To use geofencing, your app must request
{@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION}. To request this
permission, add the following element as a child element of the
<code><a href="{@docRoot}guide/topics/manifest/manifest-element.html"><manifest></a></code>
- element:
+ element in your app manifest:
</p>
<pre>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
</pre>
-<!-- Check for Google Play services -->
-<h3>Check for Google Play Services</h3>
-<p>
- Location Services is part of the Google Play services APK. Since it's hard to anticipate the
- state of the user's device, you should always check that the APK is installed before you attempt
- to connect to Location Services. To check that the APK is installed, call
-<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesUtil.html#isGooglePlayServicesAvailable(android.content.Context)">GooglePlayServicesUtil.isGooglePlayServicesAvailable()</a></code>,
- which returns one of the
- integer result codes listed in the API reference documentation. If you encounter an error,
- call
-<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesUtil.html#getErrorDialog(int, android.app.Activity, int)">GooglePlayServicesUtil.getErrorDialog()</a></code>
- to retrieve localized dialog that prompts users to take the correct action, then display
- the dialog in a {@link android.support.v4.app.DialogFragment}. The dialog may allow the
- user to correct the problem, in which case Google Play services may send a result back to your
- activity. To handle this result, override the method
- {@link android.support.v4.app.FragmentActivity#onActivityResult onActivityResult()}
-</p>
-<p class="note">
- <strong>Note:</strong> To make your app compatible with
- platform version 1.6 and later, the activity that displays the
- {@link android.support.v4.app.DialogFragment} must subclass
- {@link android.support.v4.app.FragmentActivity} instead of {@link android.app.Activity}. Using
- {@link android.support.v4.app.FragmentActivity} also allows you to call
- {@link android.support.v4.app.FragmentActivity#getSupportFragmentManager
- getSupportFragmentManager()} to display the {@link android.support.v4.app.DialogFragment}.
-</p>
<p>
- Since you usually need to check for Google Play services in more than one place in your code,
- define a method that encapsulates the check, then call the method before each connection
- attempt. The following snippet contains all of the code required to check for Google
- Play services:
+ If you want to use an {@link android.app.IntentService} to listen for geofence transitions,
+ add an element specifying the service name. This element must be
+ a child of the <code><a href="{@docRoot}guide/topics/manifest/application-element.html">
+ <application></a></code> element:
+</p>
+
+<pre>
+<application
+ android:allowBackup="true">
+ ...
+ <service android:name=".GeofenceTransitionsIntentService"/>
+<application/>
+</pre>
+
+<p>To access the location APIs, you need to create an instance of the
+ Google Play services API client. To learn how to connect your client, see
+ <a href="{@docRoot}training/location/retrieve-current.html#play-services">Connect
+ to Google Play Services</a>.</p>
+
+<h2 id="CreateAdd">Create and Add Geofences</h2>
+
+<p>Your app needs to create and add geofences using the location API's builder class for
+ creating Geofence objects, and the convenience class for adding them. Also, to handle the
+ intents sent from Location Services when geofence transitions occur, you can define a
+ {@link android.app.PendingIntent} as shown in this section.
+</p>
+
+<h3>Create geofence objects</h3>
+
+<p>
+ First, use <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.Builder.
+ html">Geofence.Builder</a></code> to create a geofence, setting the desired radius, duration, and
+ transition types for the geofence. For example, to populate a list object named
+ {@code mGeofenceList}:
+ </p>
+
+<pre>
+mGeofenceList.add(new Geofence.Builder()
+ // Set the request ID of the geofence. This is a string to identify this
+ // geofence.
+ .setRequestId(entry.getKey())
+
+ .setCircularRegion(
+ entry.getValue().latitude,
+ entry.getValue().longitude,
+ Constants.GEOFENCE_RADIUS_IN_METERS
+ )
+ .setExpirationDuration(Constants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)
+ .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER |
+ Geofence.GEOFENCE_TRANSITION_EXIT)
+ .build());
+</pre>
+
+<p>This example pulls data from a constants file. In actual practice, apps might
+ dynamically create geofences based on the user's location.</p>
+
+<h3>Specify geofences and initial triggers</h3>
+
+<p>
+ The following snippet uses the <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingRequest.html">
+ GeofencingRequest</a></code> class
+ and its nested <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingRequest.Builder.html">
+ GeofencingRequestBuilder</a></code> class to
+ specify the geofences to monitor and to set how related geofence events are triggered:
</p>
<pre>
-public class MainActivity extends FragmentActivity {
- ...
- // Global constants
- /*
- * Define a request code to send to Google Play services
- * This code is returned in Activity.onActivityResult
- */
- private final static int
- CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000;
- ...
- // Define a DialogFragment that displays the error dialog
- public static class ErrorDialogFragment extends DialogFragment {
- // Global field to contain the error dialog
- private Dialog mDialog;
- ...
- // Default constructor. Sets the dialog field to null
- public ErrorDialogFragment() {
- super();
- mDialog = null;
- }
- ...
- // Set the dialog to display
- public void setDialog(Dialog dialog) {
- mDialog = dialog;
- }
- ...
- // Return a Dialog to the DialogFragment.
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- return mDialog;
- }
- ...
- }
- ...
- /*
- * Handle results returned to the FragmentActivity
- * by Google Play services
- */
- @Override
- protected void onActivityResult(
- int requestCode, int resultCode, Intent data) {
- // Decide what to do based on the original request code
- switch (requestCode) {
- ...
- case CONNECTION_FAILURE_RESOLUTION_REQUEST :
- /*
- * If the result code is Activity.RESULT_OK, try
- * to connect again
- */
- switch (resultCode) {
- ...
- case Activity.RESULT_OK :
- /*
- * Try the request again
- */
- ...
- break;
- }
- ...
- }
- ...
- }
- ...
- private boolean servicesConnected() {
- // Check that Google Play services is available
- int resultCode =
- GooglePlayServicesUtil.
- isGooglePlayServicesAvailable(this);
- // If Google Play services is available
- if (ConnectionResult.SUCCESS == resultCode) {
- // In debug mode, log the status
- Log.d("Geofence Detection",
- "Google Play services is available.");
- // Continue
- return true;
- // Google Play services was not available for some reason
- } else {
- // Get the error code
- int errorCode = connectionResult.getErrorCode();
- // Get the error dialog from Google Play services
- Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog(
- errorCode,
- this,
- CONNECTION_FAILURE_RESOLUTION_REQUEST);
-
- // If Google Play services can provide an error dialog
- if (errorDialog != null) {
- // Create a new DialogFragment for the error dialog
- ErrorDialogFragment errorFragment =
- new ErrorDialogFragment();
- // Set the dialog in the DialogFragment
- errorFragment.setDialog(errorDialog);
- // Show the error dialog in the DialogFragment
- errorFragment.show(
- getSupportFragmentManager(),
- "Geofence Detection");
- }
- }
- }
- ...
+private GeofencingRequest getGeofencingRequest() {
+ GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
+ builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER);
+ builder.addGeofences(mGeofenceList);
+ return builder.build();
}
</pre>
-<p>
- Snippets in the following sections call this method to verify that Google Play services is
- available.
-</p>
-<p>
- To use geofencing, start by defining the geofences you want to monitor. Although you usually
- store geofence data in a local database or download it from the network, you need to send
- a geofence to Location Services as an instance of
-<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code>,
- which you create with
-<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.Builder.html">Geofence.Builder</a></code>.
- Each
-<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code>
- object contains the following information:
-</p>
-<dl>
- <dt>Latitude, longitude, and radius</dt>
- <dd>
- Define a circular area for the geofence. Use the latitude and longitude to mark a location
- of interest, and then use the radius to adjust how close the user needs to approach the
- location before the geofence is detected. The larger the radius, the more likely the
- user will trigger a geofence transition alert by approaching the geofence. For example,
- providing a large radius for a geofencing app that turns on lights in the user's house as
- the user returns home might cause the lights to go on even if the user is simply passing by.
- </dd>
- <dt>Expiration time</dt>
- <dd>
- How long the geofence should remain active. Once the expiration time is reached, Location
- Services deletes the geofence. Most of the time, you should specify an expiration time, but
- you may want to keep permanent geofences for the user's home or place of work.
- </dd>
- <dt>Transition type</dt>
- <dd>
- Location Services can detect when the user steps within the radius of the geofence ("entry")
- and when the user steps outside the radius of the geofence ("exit"), or both.
- </dd>
- <dt>Geofence ID</dt>
- <dd>
- A string that is stored with the geofence. You should make this unique, so that you can
- use it to remove a geofence from Location Services tracking.
- </dd>
-</dl>
-<h3>Define geofence storage</h3>
-<p>
- A geofencing app needs to read and write geofence data to persistent storage. You shouldn't use
-<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code>
- objects to do this; instead, use storage techniques such as databases that can store groups of
- related data.
-</p>
-<p>
- As an example of storing geofence data, the following snippet defines two classes that use
- the app's {@link android.content.SharedPreferences} instance for persistent storage. The class
- {@code SimpleGeofence}, analogous to a database record, stores the
- data for a single
-<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code>
- object in a "flattened" form. The class {@code SimpleGeofenceStore}, analogous to a database,
- reads and writes {@code SimpleGeofence} data to the
- {@link android.content.SharedPreferences} instance.
-</p>
-<pre>
-public class MainActivity extends FragmentActivity {
- ...
- /**
- * A single Geofence object, defined by its center and radius.
- */
- public class SimpleGeofence {
- // Instance variables
- private final String mId;
- private final double mLatitude;
- private final double mLongitude;
- private final float mRadius;
- private long mExpirationDuration;
- private int mTransitionType;
- /**
- * @param geofenceId The Geofence's request ID
- * @param latitude Latitude of the Geofence's center.
- * @param longitude Longitude of the Geofence's center.
- * @param radius Radius of the geofence circle.
- * @param expiration Geofence expiration duration
- * @param transition Type of Geofence transition.
- */
- public SimpleGeofence(
- String geofenceId,
- double latitude,
- double longitude,
- float radius,
- long expiration,
- int transition) {
- // Set the instance fields from the constructor
- this.mId = geofenceId;
- this.mLatitude = latitude;
- this.mLongitude = longitude;
- this.mRadius = radius;
- this.mExpirationDuration = expiration;
- this.mTransitionType = transition;
- }
- // Instance field getters
- public String getId() {
- return mId;
- }
- public double getLatitude() {
- return mLatitude;
- }
- public double getLongitude() {
- return mLongitude;
- }
- public float getRadius() {
- return mRadius;
- }
- public long getExpirationDuration() {
- return mExpirationDuration;
- }
- public int getTransitionType() {
- return mTransitionType;
- }
- /**
- * Creates a Location Services Geofence object from a
- * SimpleGeofence.
- *
- * @return A Geofence object
- */
- public Geofence toGeofence() {
- // Build a new Geofence object
- return new Geofence.Builder()
- .setRequestId(getId())
- .setTransitionTypes(mTransitionType)
- .setCircularRegion(
- getLatitude(), getLongitude(), getRadius())
- .setExpirationDuration(mExpirationDuration)
- .build();
- }
- }
- ...
- /**
- * Storage for geofence values, implemented in SharedPreferences.
- */
- public class SimpleGeofenceStore {
- // Keys for flattened geofences stored in SharedPreferences
- public static final String KEY_LATITUDE =
- "com.example.android.geofence.KEY_LATITUDE";
- public static final String KEY_LONGITUDE =
- "com.example.android.geofence.KEY_LONGITUDE";
- public static final String KEY_RADIUS =
- "com.example.android.geofence.KEY_RADIUS";
- public static final String KEY_EXPIRATION_DURATION =
- "com.example.android.geofence.KEY_EXPIRATION_DURATION";
- public static final String KEY_TRANSITION_TYPE =
- "com.example.android.geofence.KEY_TRANSITION_TYPE";
- // The prefix for flattened geofence keys
- public static final String KEY_PREFIX =
- "com.example.android.geofence.KEY";
- /*
- * Invalid values, used to test geofence storage when
- * retrieving geofences
- */
- public static final long INVALID_LONG_VALUE = -999l;
- public static final float INVALID_FLOAT_VALUE = -999.0f;
- public static final int INVALID_INT_VALUE = -999;
- // The SharedPreferences object in which geofences are stored
- private final SharedPreferences mPrefs;
- // The name of the SharedPreferences
- private static final String SHARED_PREFERENCES =
- "SharedPreferences";
- // Create the SharedPreferences storage with private access only
- public SimpleGeofenceStore(Context context) {
- mPrefs =
- context.getSharedPreferences(
- SHARED_PREFERENCES,
- Context.MODE_PRIVATE);
- }
- /**
- * Returns a stored geofence by its id, or returns {@code null}
- * if it's not found.
- *
- * @param id The ID of a stored geofence
- * @return A geofence defined by its center and radius. See
- */
- public SimpleGeofence getGeofence(String id) {
- /*
- * Get the latitude for the geofence identified by id, or
- * INVALID_FLOAT_VALUE if it doesn't exist
- */
- double lat = mPrefs.getFloat(
- getGeofenceFieldKey(id, KEY_LATITUDE),
- INVALID_FLOAT_VALUE);
- /*
- * Get the longitude for the geofence identified by id, or
- * INVALID_FLOAT_VALUE if it doesn't exist
- */
- double lng = mPrefs.getFloat(
- getGeofenceFieldKey(id, KEY_LONGITUDE),
- INVALID_FLOAT_VALUE);
- /*
- * Get the radius for the geofence identified by id, or
- * INVALID_FLOAT_VALUE if it doesn't exist
- */
- float radius = mPrefs.getFloat(
- getGeofenceFieldKey(id, KEY_RADIUS),
- INVALID_FLOAT_VALUE);
- /*
- * Get the expiration duration for the geofence identified
- * by id, or INVALID_LONG_VALUE if it doesn't exist
- */
- long expirationDuration = mPrefs.getLong(
- getGeofenceFieldKey(id, KEY_EXPIRATION_DURATION),
- INVALID_LONG_VALUE);
- /*
- * Get the transition type for the geofence identified by
- * id, or INVALID_INT_VALUE if it doesn't exist
- */
- int transitionType = mPrefs.getInt(
- getGeofenceFieldKey(id, KEY_TRANSITION_TYPE),
- INVALID_INT_VALUE);
- // If none of the values is incorrect, return the object
- if (
- lat != GeofenceUtils.INVALID_FLOAT_VALUE &&
- lng != GeofenceUtils.INVALID_FLOAT_VALUE &&
- radius != GeofenceUtils.INVALID_FLOAT_VALUE &&
- expirationDuration !=
- GeofenceUtils.INVALID_LONG_VALUE &&
- transitionType != GeofenceUtils.INVALID_INT_VALUE) {
-
- // Return a true Geofence object
- return new SimpleGeofence(
- id, lat, lng, radius, expirationDuration,
- transitionType);
- // Otherwise, return null.
- } else {
- return null;
- }
- }
- /**
- * Save a geofence.
- * @param geofence The SimpleGeofence containing the
- * values you want to save in SharedPreferences
- */
- public void setGeofence(String id, SimpleGeofence geofence) {
- /*
- * Get a SharedPreferences editor instance. Among other
- * things, SharedPreferences ensures that updates are atomic
- * and non-concurrent
- */
- Editor editor = mPrefs.edit();
- // Write the Geofence values to SharedPreferences
- editor.putFloat(
- getGeofenceFieldKey(id, KEY_LATITUDE),
- (float) geofence.getLatitude());
- editor.putFloat(
- getGeofenceFieldKey(id, KEY_LONGITUDE),
- (float) geofence.getLongitude());
- editor.putFloat(
- getGeofenceFieldKey(id, KEY_RADIUS),
- geofence.getRadius());
- editor.putLong(
- getGeofenceFieldKey(id, KEY_EXPIRATION_DURATION),
- geofence.getExpirationDuration());
- editor.putInt(
- getGeofenceFieldKey(id, KEY_TRANSITION_TYPE),
- geofence.getTransitionType());
- // Commit the changes
- editor.commit();
- }
- public void clearGeofence(String id) {
- /*
- * Remove a flattened geofence object from storage by
- * removing all of its keys
- */
- Editor editor = mPrefs.edit();
- editor.remove(getGeofenceFieldKey(id, KEY_LATITUDE));
- editor.remove(getGeofenceFieldKey(id, KEY_LONGITUDE));
- editor.remove(getGeofenceFieldKey(id, KEY_RADIUS));
- editor.remove(getGeofenceFieldKey(id,
- KEY_EXPIRATION_DURATION));
- editor.remove(getGeofenceFieldKey(id, KEY_TRANSITION_TYPE));
- editor.commit();
- }
- /**
- * Given a Geofence object's ID and the name of a field
- * (for example, KEY_LATITUDE), return the key name of the
- * object's values in SharedPreferences.
- *
- * @param id The ID of a Geofence object
- * @param fieldName The field represented by the key
- * @return The full key name of a value in SharedPreferences
- */
- private String getGeofenceFieldKey(String id,
- String fieldName) {
- return KEY_PREFIX + "_" + id + "_" + fieldName;
- }
- }
- ...
-}
-</pre>
-<h3>Create Geofence objects</h3>
<p>
- The following snippet uses the {@code SimpleGeofence} and {@code SimpleGeofenceStore} classes
- gets geofence data from the UI, stores it in {@code SimpleGeofence} objects, stores these
- objects in a {@code SimpleGeofenceStore} object, and then creates
-<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code>
- objects:
+ This example shows the use of two geofence triggers. The <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html#GEOFENCE_TRANSITION_ENTER">
+ GEOFENCE_TRANSITION_ENTER</a></code>
+ transition triggers when a device enters a geofence, and the <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html#GEOFENCE_TRANSITION_EXIT">
+ GEOFENCE_TRANSITION_EXIT</a></code>
+ transition triggers when a device exits a geofence. Specifying
+ <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingRequest.html#INITIAL_TRIGGER_ENTER">
+ INITIAL_TRIGGER_ENTER</a></code> tells Location services that
+ <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html#GEOFENCE_TRANSITION_ENTER">
+ GEOFENCE_TRANSITION_ENTER</a></code>
+ should be triggered if the the device is already inside the geofence.</p>
</p>
-<pre>
-public class MainActivity extends FragmentActivity {
- ...
- /*
- * Use to set an expiration time for a geofence. After this amount
- * of time Location Services will stop tracking the geofence.
- */
- private static final long SECONDS_PER_HOUR = 60;
- private static final long MILLISECONDS_PER_SECOND = 1000;
- private static final long GEOFENCE_EXPIRATION_IN_HOURS = 12;
- private static final long GEOFENCE_EXPIRATION_TIME =
- GEOFENCE_EXPIRATION_IN_HOURS *
- SECONDS_PER_HOUR *
- MILLISECONDS_PER_SECOND;
- ...
- /*
- * Handles to UI views containing geofence data
- */
- // Handle to geofence 1 latitude in the UI
- private EditText mLatitude1;
- // Handle to geofence 1 longitude in the UI
- private EditText mLongitude1;
- // Handle to geofence 1 radius in the UI
- private EditText mRadius1;
- // Handle to geofence 2 latitude in the UI
- private EditText mLatitude2;
- // Handle to geofence 2 longitude in the UI
- private EditText mLongitude2;
- // Handle to geofence 2 radius in the UI
- private EditText mRadius2;
- /*
- * Internal geofence objects for geofence 1 and 2
- */
- private SimpleGeofence mUIGeofence1;
- private SimpleGeofence mUIGeofence2;
- ...
- // Internal List of Geofence objects
- List<Geofence> mGeofenceList;
- // Persistent storage for geofences
- private SimpleGeofenceStore mGeofenceStorage;
- ...
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ...
- // Instantiate a new geofence storage area
- mGeofenceStorage = new SimpleGeofenceStore(this);
- // Instantiate the current List of geofences
- mCurrentGeofences = new ArrayList<Geofence>();
- }
- ...
- /**
- * Get the geofence parameters for each geofence from the UI
- * and add them to a List.
- */
- public void createGeofences() {
- /*
- * Create an internal object to store the data. Set its
- * ID to "1". This is a "flattened" object that contains
- * a set of strings
- */
- mUIGeofence1 = new SimpleGeofence(
- "1",
- Double.valueOf(mLatitude1.getText().toString()),
- Double.valueOf(mLongitude1.getText().toString()),
- Float.valueOf(mRadius1.getText().toString()),
- GEOFENCE_EXPIRATION_TIME,
- // This geofence records only entry transitions
- Geofence.GEOFENCE_TRANSITION_ENTER);
- // Store this flat version
- mGeofenceStorage.setGeofence("1", mUIGeofence1);
- // Create another internal object. Set its ID to "2"
- mUIGeofence2 = new SimpleGeofence(
- "2",
- Double.valueOf(mLatitude2.getText().toString()),
- Double.valueOf(mLongitude2.getText().toString()),
- Float.valueOf(mRadius2.getText().toString()),
- GEOFENCE_EXPIRATION_TIME,
- // This geofence records both entry and exit transitions
- Geofence.GEOFENCE_TRANSITION_ENTER |
- Geofence.GEOFENCE_TRANSITION_EXIT);
- // Store this flat version
- mGeofenceStorage.setGeofence(2, mUIGeofence2);
- mGeofenceList.add(mUIGeofence1.toGeofence());
- mGeofenceList.add(mUIGeofence2.toGeofence());
- }
- ...
-}
-</pre>
-<p>
- In addition to the {@link java.util.List} of
-<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code>
- objects you want to monitor, you need to provide Location Services with the
- {@link android.content.Intent} that it sends to your app when it detects geofence
- transitions.
-<h4>Define a Intent for geofence transitions</h4>
+<p>In many cases, it may be preferable to use instead <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingRequest.html#INITIAL_TRIGGER_DWELL">
+ INITIAL_TRIGGER_DWELL</a></code>,
+ which triggers events only when the user stops for a defined duration within a geofence.
+ This approach can help reduce "alert spam" resulting from large numbers notifications when a
+ device briefly enters and exits geofences. Another strategy for getting best results from your
+ geofences is to set a minimum radius of 100 meters. This helps account for the location accuracy
+ of typical WiFi networks, and also helps reduce device power consumption.
+</p>
+
+<h3>Define an Intent for geofence transitions</h3>
<p>
The {@link android.content.Intent} sent from Location Services can trigger various actions in
your app, but you should <i>not</i> have it start an activity or fragment, because components
@@ -601,807 +174,133 @@
{@link android.app.IntentService} is a good way to handle the intent. An
{@link android.app.IntentService} can post a notification, do long-running background work,
send intents to other services, or send a broadcast intent. The following snippet shows how
- how to define a {@link android.app.PendingIntent} that starts an
- {@link android.app.IntentService}:
+ to define a {@link android.app.PendingIntent} that starts an {@link android.app.IntentService}:
</p>
<pre>
public class MainActivity extends FragmentActivity {
...
- /*
- * Create a PendingIntent that triggers an IntentService in your
- * app when a geofence transition occurs.
- */
- private PendingIntent getTransitionPendingIntent() {
- // Create an explicit Intent
- Intent intent = new Intent(this,
- ReceiveTransitionsIntentService.class);
- /*
- * Return the PendingIntent
- */
- return PendingIntent.getService(
- this,
- 0,
- intent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- }
- ...
-}
-</pre>
-<p>
- Now you have all the code you need to send a request to monitor geofences to Location
- Services.
-</p>
-<!-- Send the monitoring request -->
-<h3 id="requestmonitoring">Send the monitoring request</h3>
-<p>
- Sending the monitoring request requires two asynchronous operations. The first operation gets a
- location client for the request, and the second makes the request using the client. In both
- cases, Location Services invokes a callback method when it finishes the operation. The best way
- to handle these operations is to chain together the method calls. The following snippets
- demonstrate how to set up an activity, define the methods, and call them in the proper order.
-</p>
-<p>
- First, modify the activity's class definition to implement the necessary callback interfaces.
- Add the following interfaces:
-</p>
-<dl>
- <dt>
-<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html">ConnectionCallbacks</a></code>
- </dt>
- <dd>
- Specifies methods that Location Services calls when a location client is connected or
- disconnected.
- </dd>
- <dt>
-<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.OnConnectionFailedListener.html">OnConnectionFailedListener</a></code>
- </dt>
- <dd>
- Specifies a method that Location Services calls if an error occurs while attempting to
- connect the location client.
- </dd>
- <dt>
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnAddGeofencesResultListener.html">OnAddGeofencesResultListener</a></code>
- </dt>
- <dd>
- Specifies a method that Location Services calls once it has added the geofences.
- </dd>
-</dl>
-<p>
- For example:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
-}
-</pre>
-<h4>Start the request process</h4>
-<p>
- Next, define a method that starts the request process by connecting to Location Services.
- Mark this as a request to add a geofence by setting a global variable. This allows you to
- use the callback
-<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html#onConnected(android.os.Bundle)">ConnectionCallbacks.onConnected()</a></code>
- to add geofences and to remove them, as described in succeeding sections.
-</p>
-<p>
-<p>
- To guard against race conditions that might arise if your app tries to start another request
- before the first one finishes, define a boolean flag that tracks the state of the current
- request:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- // Holds the location client
- private LocationClient mLocationClient;
- // Stores the PendingIntent used to request geofence monitoring
- private PendingIntent mGeofenceRequestIntent;
- // Defines the allowable request types.
- public enum REQUEST_TYPE = {ADD}
- private REQUEST_TYPE mRequestType;
- // Flag that indicates if a request is underway.
- private boolean mInProgress;
- ...
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- ...
- // Start with the request flag set to false
- mInProgress = false;
- ...
- }
- ...
- /**
- * Start a request for geofence monitoring by calling
- * LocationClient.connect().
- */
- public void addGeofences() {
- // Start a request to add geofences
- mRequestType = ADD;
- /*
- * Test for Google Play services after setting the request type.
- * If Google Play services isn't present, the proper request
- * can be restarted.
- */
- if (!servicesConnected()) {
- return;
+ private PendingIntent getGeofencePendingIntent() {
+ // Reuse the PendingIntent if we already have it.
+ if (mGeofencePendingIntent != null) {
+ return mGeofencePendingIntent;
}
- /*
- * Create a new location client object. Since the current
- * activity class implements ConnectionCallbacks and
- * OnConnectionFailedListener, pass the current activity object
- * as the listener for both parameters
- */
- mLocationClient = new LocationClient(this, this, this)
- // If a request is not already underway
- if (!mInProgress) {
- // Indicate that a request is underway
- mInProgress = true;
- // Request a connection from the client to Location Services
- mLocationClient.connect();
- } else {
- /*
- * A request is already underway. You can handle
- * this situation by disconnecting the client,
- * re-setting the flag, and then re-trying the
- * request.
- */
- }
+ Intent intent = new Intent(this, GeofenceTransitionsIntentService.class);
+ // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when
+ // calling addGeofences() and removeGeofences().
+ return PendingIntent.getService(this, 0, intent, PendingIntent.
+ FLAG_UPDATE_CURRENT);
}
- ...
-}
</pre>
-<h4>Send a request to add the geofences</h4>
+
+<h3>Add geofences</h3>
+
<p>
- In your implementation of
-<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html#onConnected(android.os.Bundle)">ConnectionCallbacks.onConnected()</a></code>,
- call
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#addGeofences(java.util.List<com.google.android.gms.location.Geofence>, android.app.PendingIntent, com.google.android.gms.location.LocationClient.OnAddGeofencesResultListener)">LocationClient.addGeofences()</a></code>.
- Notice that if the connection fails,
-<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html#onConnected(android.os.Bundle)">onConnected()</a></code>
- isn't called, and the request stops.
+ To add geofences, use the <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingApi.html#addGeofences(com.google.android.gms.common.api.GoogleApiClient, com.google.android.gms.location.GeofencingRequest, android.app.PendingIntent)">{@code GeoencingApi.addGeofences()}</a></code> method.
+ Provide the Google API client, the <code><a href="{@docRoot}reference/com/google/android/gms/location/GeofencingRequest">
+ GeofencingRequest</a></code> object, and the {@link android.app.PendingIntent}.
+ The following snippet, which processes the results in <code><a href="{@docRoot}reference/com/google/android/gms/common/api/ResultCallback.html#onResult(R)">
+ onResult()</a></code>, assumes that the main activity implements <code><a href="{@docRoot}reference/com/google/android/gms/common/api/ResultCallback.html">
+ ResultCallback</a></code>:
</p>
<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
+public class MainActivity extends FragmentActivity {
...
- /*
- * Provide the implementation of ConnectionCallbacks.onConnected()
- * Once the connection is available, send a request to add the
- * Geofences
- */
- @Override
- private void onConnected(Bundle dataBundle) {
- ...
- switch (mRequestType) {
- case ADD :
- // Get the PendingIntent for the request
- mTransitionPendingIntent =
- getTransitionPendingIntent();
- // Send a request to add the current geofences
- mLocationClient.addGeofences(
- mCurrentGeofences, pendingIntent, this);
- ...
- }
- }
- ...
-}
+ LocationServices.GeofencingApi.addGeofences(
+ mGoogleApiClient,
+ getGeofencingRequest(),
+ getGeofencePendingIntent()
+ ).setResultCallback(this);
</pre>
-<p>
- Notice that
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#addGeofences(java.util.List<com.google.android.gms.location.Geofence>, android.app.PendingIntent, com.google.android.gms.location.LocationClient.OnAddGeofencesResultListener)">addGeofences()</a></code>
- returns immediately, but the status of the request is indeterminate until Location Services
- calls
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnAddGeofencesResultListener.html#onAddGeofencesResult(int, java.lang.String[])">onAddGeofencesResult()</a></code>
- Once this method is called, you can determine if the request was successful or not.
-</p>
-<h4>Check the result returned by Location Services</h4>
-<p>
- When Location Services invokes your implementation of the callback method
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnAddGeofencesResultListener.html#onAddGeofencesResult(int, java.lang.String[])">onAddGeofencesResult()</a></code>,
- indicating that the request is complete, examine the incoming status code. If the request
- was successful, the geofences you requested are active. If the request was unsuccessful,
- the geofences aren't active, and you need to re-try the request or report an error. For example:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- /*
- * Provide the implementation of
- * OnAddGeofencesResultListener.onAddGeofencesResult.
- * Handle the result of adding the geofences
- *
- */
- @Override
- public void onAddGeofencesResult(
- int statusCode, String[] geofenceRequestIds) {
- // If adding the geofences was successful
- if (LocationStatusCodes.SUCCESS == statusCode) {
- /*
- * Handle successful addition of geofences here.
- * You can send out a broadcast intent or update the UI.
- * geofences into the Intent's extended data.
- */
- } else {
- // If adding the geofences failed
- /*
- * Report errors here.
- * You can log the error using Log.e() or update
- * the UI.
- */
- }
- // Turn off the in progress flag and disconnect the client
- mInProgress = false;
- mLocationClient.disconnect();
- }
- ...
-}
-</pre>
-<!-- Handle disconnections -->
-<h3>Handle disconnections</h3>
-<p>
- In some cases, Location Services may disconnect from the activity recognition client before
- you call
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#disconnect()">disconnect()</a></code>.
- To handle this situation, implement <code>
-<a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html#onDisconnected()">onDisconnected()</a></code>.
- In this method, set the request flag to indicate that a request is not in progress, and
- delete the client:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- /*
- * Implement ConnectionCallbacks.onDisconnected()
- * Called by Location Services once the location client is
- * disconnected.
- */
- @Override
- public void onDisconnected() {
- // Turn off the request flag
- mInProgress = false;
- // Destroy the current location client
- mLocationClient = null;
- }
- ...
-}
-</pre>
-<!-- Handle connection errors -->
-<h3>Handle connection errors</h3>
-<p>
- Besides handling the normal callbacks from Location Services, you have to provide a callback
- method that Location Services calls if a connection error occurs. This callback method
- can re-use the {@link android.support.v4.app.DialogFragment} class that you defined to
- handle the check for Google Play services. It can also re-use the override you defined
- for {@link android.support.v4.app.FragmentActivity#onActivityResult onActivityResult()} that
- receives any Google Play services results that occur when the user interacts with the
- error dialog. The following snippet shows you a sample implementation of the callback method:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- // Implementation of OnConnectionFailedListener.onConnectionFailed
- @Override
- public void onConnectionFailed(ConnectionResult connectionResult) {
- // Turn off the request flag
- mInProgress = false;
- /*
- * If the error has a resolution, start a Google Play services
- * activity to resolve it.
- */
- if (connectionResult.hasResolution()) {
- try {
- connectionResult.startResolutionForResult(
- this,
- CONNECTION_FAILURE_RESOLUTION_REQUEST);
- } catch (SendIntentException e) {
- // Log the error
- e.printStackTrace();
- }
- // If no resolution is available, display an error dialog
- } else {
- // Get the error code
- int errorCode = connectionResult.getErrorCode();
- // Get the error dialog from Google Play services
- Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog(
- errorCode,
- this,
- CONNECTION_FAILURE_RESOLUTION_REQUEST);
- // If Google Play services can provide an error dialog
- if (errorDialog != null) {
- // Create a new DialogFragment for the error dialog
- ErrorDialogFragment errorFragment =
- new ErrorDialogFragment();
- // Set the dialog in the DialogFragment
- errorFragment.setDialog(errorDialog);
- // Show the error dialog in the DialogFragment
- errorFragment.show(
- getSupportFragmentManager(),
- "Geofence Detection");
- }
- }
- }
- ...
-}
-</pre>
-<!--
- Handle Geofence Transitions
- -->
+
+
<h2 id="HandleGeofenceTransitions">Handle Geofence Transitions</h2>
<p>
When Location Services detects that the user has entered or exited a geofence, it
sends out the {@link android.content.Intent} contained in the {@link android.app.PendingIntent}
- you included in the request to add geofences. This {@link android.content.Intent} is
+ you included in the request to add geofences. This {@link android.content.Intent} is received
+ by a service like <code>GeofenceTransitionsIntentService</code>,
+ which obtains the geofencing event from the intent, determines the type of Geofence transition(s),
+ and determines which of the defined geofences was triggered. It then sends a notification as
+ the output.
</p>
-<h3>Define an IntentService</h3>
<p>
The following snippet shows how to define an {@link android.app.IntentService} that posts a
notification when a geofence transition occurs. When the user clicks the notification, the
app's main activity appears:
</p>
<pre>
-public class ReceiveTransitionsIntentService extends IntentService {
- ...
- /**
- * Sets an identifier for the service
- */
- public ReceiveTransitionsIntentService() {
- super("ReceiveTransitionsIntentService");
- }
- /**
- * Handles incoming intents
- *@param intent The Intent sent by Location Services. This
- * Intent is provided
- * to Location Services (inside a PendingIntent) when you call
- * addGeofences()
- */
- @Override
+public class GeofenceTransitionsIntentService extends IntentService {
+ ...
protected void onHandleIntent(Intent intent) {
- // First check for errors
- if (LocationClient.hasError(intent)) {
- // Get the error code with a static method
- int errorCode = LocationClient.getErrorCode(intent);
- // Log the error
- Log.e("ReceiveTransitionsIntentService",
- "Location Services error: " +
- Integer.toString(errorCode));
- /*
- * You can also send the error code to an Activity or
- * Fragment with a broadcast Intent
- */
- /*
- * If there's no error, get the transition type and the IDs
- * of the geofence or geofences that triggered the transition
- */
- } else {
- // Get the type of transition (entry or exit)
- int transitionType =
- LocationClient.getGeofenceTransition(intent);
- // Test that a valid transition was reported
- if (
- (transitionType == Geofence.GEOFENCE_TRANSITION_ENTER)
- ||
- (transitionType == Geofence.GEOFENCE_TRANSITION_EXIT)
- ) {
- List <Geofence> triggerList =
- getTriggeringGeofences(intent);
+ GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
+ if (geofencingEvent.hasError()) {
+ String errorMessage = GeofenceErrorMessages.getErrorString(this,
+ geofencingEvent.getErrorCode());
+ Log.e(TAG, errorMessage);
+ return;
+ }
- String[] triggerIds = new String[geofenceList.size()];
+ // Get the transition type.
+ int geofenceTransition = geofencingEvent.getGeofenceTransition();
- for (int i = 0; i < triggerIds.length; i++) {
- // Store the Id of each geofence
- triggerIds[i] = triggerList.get(i).getRequestId();
- }
- /*
- * At this point, you can store the IDs for further use
- * display them, or display the details associated with
- * them.
- */
- }
- // An invalid transition was reported
+ // Test that the reported transition was of interest.
+ if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
+ geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
+
+ // Get the geofences that were triggered. A single event can trigger
+ // multiple geofences.
+ List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();
+
+ // Get the transition details as a String.
+ String geofenceTransitionDetails = getGeofenceTransitionDetails(
+ this,
+ geofenceTransition,
+ triggeringGeofences
+ );
+
+ // Send notification and log the transition details.
+ sendNotification(geofenceTransitionDetails);
+ Log.i(TAG, geofenceTransitionDetails);
} else {
- Log.e("ReceiveTransitionsIntentService",
- "Geofence transition error: " +
- Integer.toString()transitionType));
+ // Log the error.
+ Log.e(TAG, getString(R.string.geofence_transition_invalid_type,
+ geofenceTransition));
}
}
- ...
-}
</pre>
-<!-- Specify the IntentService in the manifest -->
-<h3>Specify the IntentService in the manifest</h3>
-<p>
- To identify the {@link android.app.IntentService} to the system, add a
- <code><a href="{@docRoot}guide/topics/manifest/service-element.html"><service></a></code>
- element to the app manifest. For example:
-</p>
-<pre>
-<service
- android:name="com.example.android.location.ReceiveTransitionsIntentService"
- android:label="@string/app_name"
- android:exported="false">
-</service>
-</pre>
-<p>
- Notice that you don't have to specify intent filters for the service, because it only receives
- explicit intents. How the incoming geofence transition intents are created is described in the
- section <a href="#requestmonitoring">Send the monitoring request</a>.
-</p>
+
+<p>After detecting the transition event via the {@link android.app.PendingIntent},
+ this {@link android.app.IntentService} gets the geofence transition type and tests whether
+ it is one of the events the app uses to trigger notifications -- either
+ <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html#GEOFENCE_TRANSITION_ENTER">GEOFENCE_TRANSITION_ENTER</a></code>
+ or <code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html#GEOFENCE_TRANSITION_EXIT">GEOFENCE_TRANSITION_EXIT</a></code>
+ in this case. The service then sends a notification and logs the transition details.</p>
<!--
Remove Geofences
-->
<h2 id="StopGeofenceMonitoring">Stop Geofence Monitoring</h2>
-<p>
- To stop geofence monitoring, you remove the geofences themselves. You can remove a specific
- set of geofences or all the geofences associated with a {@link android.app.PendingIntent}. The
- procedure is similar to adding geofences. The first operation gets a location
- client for the removal request, and the second makes the request using the client.
+
+<p>Stopping geofence monitoring when it is no longer needed or desired can help save battery
+ power and CPU cycles on the device. You can stop geofence monitoring
+ in the main activity used to add and remove geofences; removing a geofence stops it
+ immediately. The API provides methods to
+ remove geofences either by request IDs, or by removing geofences associated with a given
+ {@link android.app.PendingIntent}.
</p>
<p>
- The callback methods that Location Services invokes when it has finished removing geofences
- are defined in the interface
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnRemoveGeofencesResultListener.html">LocationClient.OnRemoveGeofencesResultListener</a></code>. Declare
- this interface as part of your class definition, and then add definitions for its two methods:
-</p>
-<dl>
- <dt>
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnRemoveGeofencesResultListener.html#onRemoveGeofencesByPendingIntentResult(int, android.app.PendingIntent)">onRemoveGeofencesByPendingIntentResult()</a></code>
- </dt>
- <dd>
- Callback invoked when Location Services finishes a request to remove all geofences made
- by the method
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#removeGeofences(android.app.PendingIntent, com.google.android.gms.location.LocationClient.OnRemoveGeofencesResultListener)">removeGeofences(PendingIntent, LocationClient.OnRemoveGeofencesResultListener)</a></code>.
- </dd>
- <dt>
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnRemoveGeofencesResultListener.html#onRemoveGeofencesByRequestIdsResult(int, java.lang.String[])">onRemoveGeofencesByRequestIdsResult(List<String>, LocationClient.OnRemoveGeofencesResultListener)</a></code>
- </dt>
- <dd>
- Callback invoked when Location Services finished a request to remove a set of geofences,
- specified by their geofence IDs, by the method
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#removeGeofences(java.util.List<java.lang.String>, com.google.android.gms.location.LocationClient.OnRemoveGeofencesResultListener)">removeGeofences(List<String>, LocationClient.OnRemoveGeofencesResultListener)</a></code>.
- </dd>
-</dl>
-<p>
- Examples of implementing these methods are shown in the next snippets.
-</p>
-<h3>Remove all geofences</h3>
-<p>
- Since removing geofences uses some of the methods you use to add geofences, start by defining
- another request type:
+ The following snippet removes geofences by {@link android.app.PendingIntent}, stopping all
+ further notification when the device enters or exits previously added geofences:
</p>
<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- // Enum type for controlling the type of removal requested
- public enum REQUEST_TYPE = {ADD, REMOVE_INTENT}
- ...
+LocationServices.GeofencingApi.removeGeofences(
+ mGoogleApiClient,
+ // This is the same pending intent that was used in addGeofences().
+ getGeofencePendingIntent()
+ ).setResultCallback(this); // Result processed in onResult().
}
</pre>
+
<p>
- Start the removal request by getting a connection to Location Services. If the connection fails,
-<code><a href="{@docRoot}reference/com/google/android/gms/common/GooglePlayServicesClient.ConnectionCallbacks.html#onConnected(android.os.Bundle)">onConnected()</a></code> isn't called,
- and the request stops. The following snippet shows how to start the request:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- /**
- * Start a request to remove geofences by calling
- * LocationClient.connect()
- */
- public void removeGeofences(PendingIntent requestIntent) {
- // Record the type of removal request
- mRequestType = REMOVE_INTENT;
- /*
- * Test for Google Play services after setting the request type.
- * If Google Play services isn't present, the request can be
- * restarted.
- */
- if (!servicesConnected()) {
- return;
- }
- // Store the PendingIntent
- mGeofenceRequestIntent = requestIntent;
- /*
- * Create a new location client object. Since the current
- * activity class implements ConnectionCallbacks and
- * OnConnectionFailedListener, pass the current activity object
- * as the listener for both parameters
- */
- mLocationClient = new LocationClient(this, this, this);
- // If a request is not already underway
- if (!mInProgress) {
- // Indicate that a request is underway
- mInProgress = true;
- // Request a connection from the client to Location Services
- mLocationClient.connect();
- } else {
- /*
- * A request is already underway. You can handle
- * this situation by disconnecting the client,
- * re-setting the flag, and then re-trying the
- * request.
- */
- }
- }
- ...
-}
-</pre>
-<p>
- When Location Services invokes the callback method indicating that the connection is open,
- make the request to remove all geofences. Disconnect the client after making the request.
- For example:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- /**
- * Once the connection is available, send a request to remove the
- * Geofences. The method signature used depends on which type of
- * remove request was originally received.
- */
- private void onConnected(Bundle dataBundle) {
- /*
- * Choose what to do based on the request type set in
- * removeGeofences
- */
- switch (mRequestType) {
- ...
- case REMOVE_INTENT :
- mLocationClient.removeGeofences(
- mGeofenceRequestIntent, this);
- break;
- ...
- }
- }
- ...
-}
-</pre>
-<p>
- Although the call to
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.html#removeGeofences(android.app.PendingIntent, com.google.android.gms.location.LocationClient.OnRemoveGeofencesResultListener)">removeGeofences(PendingIntent, LocationClient.OnRemoveGeofencesResultListener)</a></code> Services calls
- returns immediately, the result of the removal request is indeterminate until Location Services
- calls
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnRemoveGeofencesResultListener.html#onRemoveGeofencesByPendingIntentResult(int, android.app.PendingIntent)">onRemoveGeofencesByPendingIntentResult()</a></code>.
- The following snippet shows how to define this method:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- /**
- * When the request to remove geofences by PendingIntent returns,
- * handle the result.
- *
- *@param statusCode the code returned by Location Services
- *@param requestIntent The Intent used to request the removal.
- */
- @Override
- public void onRemoveGeofencesByPendingIntentResult(int statusCode,
- PendingIntent requestIntent) {
- // If removing the geofences was successful
- if (statusCode == LocationStatusCodes.SUCCESS) {
- /*
- * Handle successful removal of geofences here.
- * You can send out a broadcast intent or update the UI.
- * geofences into the Intent's extended data.
- */
- } else {
- // If adding the geocodes failed
- /*
- * Report errors here.
- * You can log the error using Log.e() or update
- * the UI.
- */
- }
- /*
- * Disconnect the location client regardless of the
- * request status, and indicate that a request is no
- * longer in progress
- */
- mInProgress = false;
- mLocationClient.disconnect();
- }
- ...
-}
-</pre>
-<h3>Remove individual geofences</h3>
-<p>
- The procedure for removing an individual geofence or set of geofences is similar to the
- removal of all geofences. To specify the geofences you want remove, add their geofence ID
- values to a {@link java.util.List} of String objects. Pass this {@link java.util.List} to a
- different definition of {@code removeGeofences} with the appropriate signature. This method
- then starts the removal process.
-</p>
-<p>
- Start by adding a request type for removing geofences by a list, and also add a global variable
- for storing the list of geofences:
-</p>
-<pre>
- ...
- // Enum type for controlling the type of removal requested
- public enum REQUEST_TYPE = {ADD, REMOVE_INTENT, REMOVE_LIST}
- // Store the list of geofence Ids to remove
- String<List> mGeofencesToRemove;
-</pre>
-<p>
- Next, define a list of geofences you want to remove. For example, this snippet removes the
-<code><a href="{@docRoot}reference/com/google/android/gms/location/Geofence.html">Geofence</a></code>
- defined by the geofence ID "1":
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- List<String> listOfGeofences =
- Collections.singletonList("1");
- removeGeofences(listOfGeofences);
- ...
-}
-</pre>
-<p>
- The following snippet defines the {@code removeGeofences()} method:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- /**
- * Start a request to remove monitoring by
- * calling LocationClient.connect()
- *
- */
- public void removeGeofences(List<String> geofenceIds) {
- // If Google Play services is unavailable, exit
- // Record the type of removal request
- mRequestType = REMOVE_LIST;
- /*
- * Test for Google Play services after setting the request type.
- * If Google Play services isn't present, the request can be
- * restarted.
- */
- if (!servicesConnected()) {
- return;
- }
- // Store the list of geofences to remove
- mGeofencesToRemove = geofenceIds;
- /*
- * Create a new location client object. Since the current
- * activity class implements ConnectionCallbacks and
- * OnConnectionFailedListener, pass the current activity object
- * as the listener for both parameters
- */
- mLocationClient = new LocationClient(this, this, this);
- // If a request is not already underway
- if (!mInProgress) {
- // Indicate that a request is underway
- mInProgress = true;
- // Request a connection from the client to Location Services
- mLocationClient.connect();
- } else {
- /*
- * A request is already underway. You can handle
- * this situation by disconnecting the client,
- * re-setting the flag, and then re-trying the
- * request.
- */
- }
- }
- ...
-}
-</pre>
-<p>
- When Location Services invokes the callback method indicating that the connection is open,
- make the request to remove the list of geofences. Disconnect the client after making the request.
- For example:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- private void onConnected(Bundle dataBundle) {
- ...
- switch (mRequestType) {
- ...
- // If removeGeofencesById was called
- case REMOVE_LIST :
- mLocationClient.removeGeofences(
- mGeofencesToRemove, this);
- break;
- ...
- }
- ...
- }
- ...
-}
-</pre>
-<p>
- Define an implementation of
-<code><a href="{@docRoot}reference/com/google/android/gms/location/LocationClient.OnRemoveGeofencesResultListener.html#onRemoveGeofencesByRequestIdsResult(int, java.lang.String[])">onRemoveGeofencesByRequestIdsResult()</a></code>.
- Location Services invokes this callback method to indicate that the request to remove a list of
- geofences is complete. In this method, examine the incoming status code and take the
- appropriate action:
-</p>
-<pre>
-public class MainActivity extends FragmentActivity implements
- ConnectionCallbacks,
- OnConnectionFailedListener,
- OnAddGeofencesResultListener {
- ...
- /**
- * When the request to remove geofences by IDs returns, handle the
- * result.
- *
- * @param statusCode The code returned by Location Services
- * @param geofenceRequestIds The IDs removed
- */
- @Override
- public void onRemoveGeofencesByRequestIdsResult(
- int statusCode, String[] geofenceRequestIds) {
- // If removing the geocodes was successful
- if (LocationStatusCodes.SUCCESS == statusCode) {
- /*
- * Handle successful removal of geofences here.
- * You can send out a broadcast intent or update the UI.
- * geofences into the Intent's extended data.
- */
- } else {
- // If removing the geofences failed
- /*
- * Report errors here.
- * You can log the error using Log.e() or update
- * the UI.
- */
- }
- // Indicate that a request is no longer in progress
- mInProgress = false;
- // Disconnect the location client
- mLocationClient.disconnect();
- }
- ...
-}
-</pre>
-<p>
- You can combine geofencing with other location-aware features, such as periodic location updates
- or activity recognition, which are described in other lessons in this class.
-</p>
-<p>
- The next lesson,
- <a href="activity-recognition.html">Recognizing the User's Current Activity</a>, shows you how
- to request and receive activity updates. At regular intervals, Location Services can send you
- information about the user's current physical activity. Based on this information, you can
- change your app's behavior; for example, you can switch to a longer update interval if you
- detect that the user is walking instead of driving.
+ You can combine geofencing with other location-aware features, such as periodic location updates.
+ For more information, see the other lessons in this class.
</p>
diff --git a/docs/html/training/location/index.jd b/docs/html/training/location/index.jd
index 35e177f..c4dec99 100644
--- a/docs/html/training/location/index.jd
+++ b/docs/html/training/location/index.jd
@@ -81,4 +81,10 @@
Learn how to convert a location's latitude and longitude into an address
(reverse geocoding).
</dd>
+ <dt>
+ <b><a href="geofencing.html">Creating and Monitoring Geofences</a></b>
+ </dt> <dd>
+ Learn how to define one or more geographic areas as locations of interest,
+ called geofences, and detect when the user is close to or inside a geofence.
+ </dd>
</dl>
diff --git a/docs/html/training/training_toc.cs b/docs/html/training/training_toc.cs
index 89e72f1..11ae1a6 100644
--- a/docs/html/training/training_toc.cs
+++ b/docs/html/training/training_toc.cs
@@ -736,6 +736,10 @@
Displaying a Location Address
</a>
</li>
+ <li><a href="<?cs var:toroot ?>training/location/geofencing.html">
+ Creating and Monitoring Geofences
+ </a>
+ </li>
</ul>
</li>
</ul>
diff --git a/docs/html/wear/images/partners/acer.png b/docs/html/wear/images/partners/acer.png
new file mode 100644
index 0000000..439de80
--- /dev/null
+++ b/docs/html/wear/images/partners/acer.png
Binary files differ
diff --git a/docs/html/wear/images/partners/arm.png b/docs/html/wear/images/partners/arm.png
new file mode 100644
index 0000000..3e2f642
--- /dev/null
+++ b/docs/html/wear/images/partners/arm.png
Binary files differ
diff --git a/docs/html/wear/images/partners/huawei.png b/docs/html/wear/images/partners/huawei.png
new file mode 100644
index 0000000..9099ed4
--- /dev/null
+++ b/docs/html/wear/images/partners/huawei.png
Binary files differ
diff --git a/docs/html/wear/index.jd b/docs/html/wear/index.jd
index 27e8098..316f5ca 100644
--- a/docs/html/wear/index.jd
+++ b/docs/html/wear/index.jd
@@ -202,6 +202,12 @@
<div class="landing-partners cols">
<div class="col-4">
+ <img src="/wear/images/partners/acer.png" alt="Acer">
+ </div>
+ <div class="col-4">
+ <img src="/wear/images/partners/arm.png" alt="ARM">
+ </div>
+ <div class="col-4">
<img src="/wear/images/partners/asus.png" alt="Asus">
</div>
<div class="col-4">
@@ -214,6 +220,9 @@
<img src="/wear/images/partners/htc.png" alt="HTC">
</div>
<div class="col-4">
+ <img src="/wear/images/partners/huawei.png" alt="Huawei">
+ </div>
+ <div class="col-4">
<img src="/wear/images/partners/intel.png" alt="Intel">
</div>
<div class="col-4">
diff --git a/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java
index 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..a9ce7f4 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;
@@ -208,7 +208,8 @@
// Fill
////////////////////////////////////////////////////////////////////////////////
-void GlopBuilder::setFill(int color, float alphaScale, SkXfermode::Mode mode,
+void GlopBuilder::setFill(int color, float alphaScale,
+ SkXfermode::Mode mode, Blend::ModeOrderSwap modeUsage,
const SkShader* shader, const SkColorFilter* colorFilter) {
if (mode != SkXfermode::kClear_Mode) {
float alpha = (SkColorGetA(color) / 255.0f) * alphaScale;
@@ -226,18 +227,17 @@
} else {
mOutGlop->fill.color = { 0, 0, 0, 1 };
}
- const bool SWAP_SRC_DST = false;
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)
|| PaintUtils::isBlendedColorFilter(colorFilter)
|| mode != SkXfermode::kSrcOver_Mode) {
if (CC_LIKELY(mode <= SkXfermode::kScreen_Mode)) {
- Blend::getFactors(mode, SWAP_SRC_DST,
+ Blend::getFactors(mode, modeUsage,
&mOutGlop->blend.src, &mOutGlop->blend.dst);
} else {
// These blend modes are not supported by OpenGL directly and have
@@ -247,11 +247,11 @@
// back to the default SrcOver blend mode instead
if (CC_UNLIKELY(mCaches.extensions().hasFramebufferFetch())) {
mDescription.framebufferMode = mode;
- mDescription.swapSrcDst = SWAP_SRC_DST;
+ mDescription.swapSrcDst = (modeUsage == Blend::ModeOrderSwap::Swap);
// blending in shader, don't enable
} else {
// unsupported
- Blend::getFactors(SkXfermode::kSrcOver_Mode, SWAP_SRC_DST,
+ Blend::getFactors(SkXfermode::kSrcOver_Mode, modeUsage,
&mOutGlop->blend.src, &mOutGlop->blend.dst);
}
}
@@ -317,17 +317,17 @@
color |= 0x00FFFFFF;
shader = nullptr;
}
- setFill(color, alphaScale, PaintUtils::getXfermode(paint->getXfermode()),
+ setFill(color, alphaScale,
+ PaintUtils::getXfermode(paint->getXfermode()), Blend::ModeOrderSwap::NoSwap,
shader, paint->getColorFilter());
} else {
mOutGlop->fill.color = { alphaScale, alphaScale, alphaScale, alphaScale };
- 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,
+ Blend::getFactors(SkXfermode::kSrcOver_Mode, Blend::ModeOrderSwap::NoSwap,
&mOutGlop->blend.src, &mOutGlop->blend.dst);
} else {
mOutGlop->blend = { GL_ZERO, GL_ZERO };
@@ -349,7 +349,8 @@
mOutGlop->fill.texture = { nullptr, GL_INVALID_ENUM, GL_INVALID_ENUM, GL_INVALID_ENUM, nullptr };
- setFill(paint.getColor(), alphaScale, PaintUtils::getXfermode(paint.getXfermode()),
+ setFill(paint.getColor(), alphaScale,
+ PaintUtils::getXfermode(paint.getXfermode()), Blend::ModeOrderSwap::NoSwap,
paint.getShader(), paint.getColorFilter());
mDescription.modulate = mOutGlop->fill.color.a < 1.0f;
return *this;
@@ -363,7 +364,8 @@
//specify invalid filter/clamp, since these are always static for PathTextures
mOutGlop->fill.texture = { &texture, GL_TEXTURE_2D, GL_INVALID_ENUM, GL_INVALID_ENUM, nullptr };
- setFill(paint.getColor(), alphaScale, PaintUtils::getXfermode(paint.getXfermode()),
+ setFill(paint.getColor(), alphaScale,
+ PaintUtils::getXfermode(paint.getXfermode()), Blend::ModeOrderSwap::NoSwap,
paint.getShader(), paint.getColorFilter());
mDescription.hasAlpha8Texture = true;
@@ -386,7 +388,8 @@
shadowColor &= paint.getColor() | COLOR_BITMASK;
}
- setFill(shadowColor, alphaScale, PaintUtils::getXfermode(paint.getXfermode()),
+ setFill(shadowColor, alphaScale,
+ PaintUtils::getXfermode(paint.getXfermode()), Blend::ModeOrderSwap::NoSwap,
paint.getShader(), paint.getColorFilter());
mDescription.hasAlpha8Texture = true;
@@ -399,7 +402,8 @@
REQUIRE_STAGES(kMeshStage);
mOutGlop->fill.texture = { nullptr, GL_INVALID_ENUM, GL_INVALID_ENUM, GL_INVALID_ENUM, nullptr };
- setFill(SK_ColorBLACK, 1.0f, SkXfermode::kSrcOver_Mode, nullptr, nullptr);
+ setFill(SK_ColorBLACK, 1.0f, SkXfermode::kSrcOver_Mode, Blend::ModeOrderSwap::NoSwap,
+ nullptr, nullptr);
return *this;
}
@@ -408,12 +412,13 @@
REQUIRE_STAGES(kMeshStage);
mOutGlop->fill.texture = { nullptr, GL_INVALID_ENUM, GL_INVALID_ENUM, GL_INVALID_ENUM, nullptr };
- setFill(SK_ColorBLACK, 1.0f, SkXfermode::kClear_Mode, nullptr, nullptr);
+ setFill(SK_ColorBLACK, 1.0f, SkXfermode::kClear_Mode, Blend::ModeOrderSwap::NoSwap,
+ nullptr, nullptr);
return *this;
}
GlopBuilder& GlopBuilder::setFillLayer(Texture& texture, const SkColorFilter* colorFilter,
- float alpha, SkXfermode::Mode mode) {
+ float alpha, SkXfermode::Mode mode, Blend::ModeOrderSwap modeUsage) {
TRIGGER_STAGE(kFillStage);
REQUIRE_STAGES(kMeshStage);
@@ -421,7 +426,7 @@
GL_TEXTURE_2D, GL_LINEAR, GL_CLAMP_TO_EDGE, nullptr };
mOutGlop->fill.color = { alpha, alpha, alpha, alpha };
- setFill(SK_ColorWHITE, alpha, mode, nullptr, colorFilter);
+ setFill(SK_ColorWHITE, alpha, mode, modeUsage, nullptr, colorFilter);
mDescription.modulate = mOutGlop->fill.color.a < 1.0f;
return *this;
@@ -435,7 +440,8 @@
layer.getRenderTarget(), GL_LINEAR, GL_CLAMP_TO_EDGE, &layer.getTexTransform() };
mOutGlop->fill.color = { alpha, alpha, alpha, alpha };
- setFill(SK_ColorWHITE, alpha, layer.getMode(), nullptr, layer.getColorFilter());
+ setFill(SK_ColorWHITE, alpha, layer.getMode(), Blend::ModeOrderSwap::NoSwap,
+ nullptr, layer.getColorFilter());
mDescription.modulate = mOutGlop->fill.color.a < 1.0f;
mDescription.hasTextureTransform = true;
@@ -540,12 +546,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 +575,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/GlopBuilder.h b/libs/hwui/GlopBuilder.h
index 74d4889..b335a2c 100644
--- a/libs/hwui/GlopBuilder.h
+++ b/libs/hwui/GlopBuilder.h
@@ -18,6 +18,7 @@
#include "OpenGLRenderer.h"
#include "Program.h"
+#include "renderstate/Blend.h"
#include "utils/Macros.h"
class SkPaint;
@@ -65,7 +66,7 @@
GlopBuilder& setFillBlack();
GlopBuilder& setFillClear();
GlopBuilder& setFillLayer(Texture& texture, const SkColorFilter* colorFilter,
- float alpha, SkXfermode::Mode mode);
+ float alpha, SkXfermode::Mode mode, Blend::ModeOrderSwap modeUsage);
GlopBuilder& setFillTextureLayer(Layer& layer, float alpha);
GlopBuilder& setTransform(const Matrix4& ortho, const Matrix4& transform, bool fudgingOffset);
@@ -94,7 +95,8 @@
void build();
private:
- void setFill(int color, float alphaScale, SkXfermode::Mode mode,
+ void setFill(int color, float alphaScale,
+ SkXfermode::Mode mode, Blend::ModeOrderSwap modeUsage,
const SkShader* shader, const SkColorFilter* colorFilter);
enum StageFlags {
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index a6f6e18..622b570 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -908,15 +908,35 @@
resetDrawTextureTexCoords(0.0f, 0.0f, 1.0f, 1.0f);
} else {
EVENT_LOGD("composeHardwareLayerRect");
+
+ if (USE_GLOPS) {
+ Blend::ModeOrderSwap modeUsage = swap ?
+ Blend::ModeOrderSwap::Swap : Blend::ModeOrderSwap::NoSwap;
+ const Matrix4& transform = swap ? Matrix4::identity() : *currentTransform();
+ bool snap = !swap
+ && layer->getWidth() == static_cast<uint32_t>(rect.getWidth())
+ && layer->getHeight() == static_cast<uint32_t>(rect.getHeight());
+ Glop glop;
+ GlopBuilder(mRenderState, mCaches, &glop)
+ .setMeshTexturedUvQuad(nullptr, layer->texCoords)
+ .setFillLayer(layer->getTexture(), layer->getColorFilter(), getLayerAlpha(layer), layer->getMode(), modeUsage)
+ .setTransform(currentSnapshot()->getOrthoMatrix(), transform, false)
+ .setModelViewMapUnitToRectOptionalSnap(snap, rect)
+ .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+ .build();
+ renderGlop(glop);
+ return;
+ }
+
const Rect& texCoords = layer->texCoords;
resetDrawTextureTexCoords(texCoords.left, texCoords.top,
texCoords.right, texCoords.bottom);
float x = rect.left;
float y = rect.top;
- bool simpleTransform = currentTransform()->isPureTranslate() &&
- layer->getWidth() == (uint32_t) rect.getWidth() &&
- layer->getHeight() == (uint32_t) rect.getHeight();
+ bool simpleTransform = currentTransform()->isPureTranslate()
+ && layer->getWidth() == (uint32_t) rect.getWidth()
+ && layer->getHeight() == (uint32_t) rect.getHeight();
if (simpleTransform) {
// When we're swapping, the layer is already in screen coordinates
@@ -1053,11 +1073,42 @@
rects = safeRegion.getArray(&count);
}
- const float alpha = getLayerAlpha(layer);
const float texX = 1.0f / float(layer->getWidth());
const float texY = 1.0f / float(layer->getHeight());
const float height = rect.getHeight();
+ if (USE_GLOPS) {
+ TextureVertex quadVertices[count * 4];
+ //std::unique_ptr<TextureVertex[]> quadVertices(new TextureVertex[count * 4]);
+ TextureVertex* mesh = &quadVertices[0];
+ for (size_t i = 0; i < count; i++) {
+ const android::Rect* r = &rects[i];
+
+ const float u1 = r->left * texX;
+ const float v1 = (height - r->top) * texY;
+ const float u2 = r->right * texX;
+ const float v2 = (height - r->bottom) * texY;
+
+ // TODO: Reject quads outside of the clip
+ TextureVertex::set(mesh++, r->left, r->top, u1, v1);
+ TextureVertex::set(mesh++, r->right, r->top, u2, v1);
+ TextureVertex::set(mesh++, r->left, r->bottom, u1, v2);
+ TextureVertex::set(mesh++, r->right, r->bottom, u2, v2);
+ }
+ Glop glop;
+ GlopBuilder(mRenderState, mCaches, &glop)
+ .setMeshTexturedIndexedQuads(&quadVertices[0], count * 6)
+ .setFillLayer(layer->getTexture(), layer->getColorFilter(), getLayerAlpha(layer), layer->getMode(), Blend::ModeOrderSwap::NoSwap)
+ .setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
+ .setModelViewOffsetRectSnap(0, 0, rect)
+ .setRoundRectClipState(currentSnapshot()->roundRectClipState)
+ .build();
+ DRAW_DOUBLE_STENCIL_IF(!layer->hasDrawnSinceUpdate, renderGlop(glop));
+ return;
+ }
+
+ const float alpha = getLayerAlpha(layer);
+
setupDraw();
// We must get (and therefore bind) the region mesh buffer
@@ -3110,7 +3161,7 @@
Glop glop;
GlopBuilder(mRenderState, mCaches, &glop)
.setMeshTexturedIndexedQuads(layer->mesh, layer->meshElementCount)
- .setFillLayer(layer->getTexture(), layer->getColorFilter(), getLayerAlpha(layer), layer->getMode())
+ .setFillLayer(layer->getTexture(), layer->getColorFilter(), getLayerAlpha(layer), layer->getMode(), Blend::ModeOrderSwap::NoSwap)
.setTransform(currentSnapshot()->getOrthoMatrix(), *currentTransform(), false)
.setModelViewOffsetRectSnap(x, y, Rect(0, 0, layer->layer.getWidth(), layer->layer.getHeight()))
.setRoundRectClipState(currentSnapshot()->roundRectClipState)
@@ -3596,7 +3647,8 @@
mode = SkXfermode::kSrcOver_Mode;
}
}
- mRenderState.blend().enable(mode, swapSrcDst);
+ mRenderState.blend().enable(mode,
+ swapSrcDst ? Blend::ModeOrderSwap::Swap : Blend::ModeOrderSwap::NoSwap);
} else {
mRenderState.blend().disable();
}
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index c6fdd3f..e009451 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -139,7 +139,7 @@
void RenderNode::prepareLayer(TreeInfo& info, uint32_t dirtyMask) {
LayerType layerType = properties().layerProperties().type();
- if (CC_UNLIKELY(layerType == kLayerTypeRenderLayer)) {
+ if (CC_UNLIKELY(layerType == LayerType::RenderLayer)) {
// Damage applied so far needs to affect our parent, but does not require
// the layer to be updated. So we pop/push here to clear out the current
// damage and get a clean state for display list or children updates to
@@ -156,7 +156,7 @@
LayerType layerType = properties().layerProperties().type();
// If we are not a layer OR we cannot be rendered (eg, view was detached)
// we need to destroy any Layers we may have had previously
- if (CC_LIKELY(layerType != kLayerTypeRenderLayer) || CC_UNLIKELY(!isRenderable())) {
+ if (CC_LIKELY(layerType != LayerType::RenderLayer) || CC_UNLIKELY(!isRenderable())) {
if (CC_UNLIKELY(mLayer)) {
LayerRenderer::destroyLayer(mLayer);
mLayer = nullptr;
@@ -384,7 +384,7 @@
renderer.concatMatrix(*properties().getTransformMatrix());
}
}
- const bool isLayer = properties().layerProperties().type() != kLayerTypeNone;
+ const bool isLayer = properties().layerProperties().type() != LayerType::None;
int clipFlags = properties().getClippingFlags();
if (properties().getAlpha() < 1) {
if (isLayer) {
diff --git a/libs/hwui/RenderProperties.cpp b/libs/hwui/RenderProperties.cpp
index bb6d087..9f1ceed 100644
--- a/libs/hwui/RenderProperties.cpp
+++ b/libs/hwui/RenderProperties.cpp
@@ -33,14 +33,12 @@
namespace android {
namespace uirenderer {
-LayerProperties::LayerProperties()
- : mType(kLayerTypeNone)
- , mColorFilter(nullptr) {
+LayerProperties::LayerProperties() {
reset();
}
LayerProperties::~LayerProperties() {
- setType(kLayerTypeNone);
+ setType(LayerType::None);
}
void LayerProperties::reset() {
@@ -146,7 +144,7 @@
}
}
- const bool isLayer = layerProperties().type() != kLayerTypeNone;
+ const bool isLayer = layerProperties().type() != LayerType::None;
int clipFlags = getClippingFlags();
if (mPrimitiveFields.mAlpha < 1) {
if (isLayer) {
diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h
index f0e22d6..61e98d2 100644
--- a/libs/hwui/RenderProperties.h
+++ b/libs/hwui/RenderProperties.h
@@ -49,12 +49,12 @@
#define RP_SET_AND_DIRTY(a, b) RP_SET(a, b, mPrimitiveFields.mMatrixOrPivotDirty = true)
// Keep in sync with View.java:LAYER_TYPE_*
-enum LayerType {
- kLayerTypeNone = 0,
+enum class LayerType {
+ None = 0,
// Although we cannot build the software layer directly (must be done at
// record time), this information is used when applying alpha.
- kLayerTypeSoftware = 1,
- kLayerTypeRenderLayer = 2,
+ Software = 1,
+ RenderLayer = 2,
// TODO: LayerTypeSurfaceTexture? Maybe?
};
@@ -124,12 +124,12 @@
friend class RenderProperties;
- LayerType mType;
+ LayerType mType = LayerType::None;
// Whether or not that Layer's content is opaque, doesn't include alpha
bool mOpaque;
uint8_t mAlpha;
SkXfermode::Mode mMode;
- SkColorFilter* mColorFilter;
+ SkColorFilter* mColorFilter = nullptr;
};
/*
diff --git a/libs/hwui/renderstate/Blend.cpp b/libs/hwui/renderstate/Blend.cpp
index 789f6cc..29927ed 100644
--- a/libs/hwui/renderstate/Blend.cpp
+++ b/libs/hwui/renderstate/Blend.cpp
@@ -78,10 +78,10 @@
// gl blending off by default
}
-void Blend::enable(SkXfermode::Mode mode, bool swapSrcDst) {
+void Blend::enable(SkXfermode::Mode mode, ModeOrderSwap modeUsage) {
GLenum srcMode;
GLenum dstMode;
- getFactors(mode, swapSrcDst, &srcMode, &dstMode);
+ getFactors(mode, modeUsage, &srcMode, &dstMode);
setFactors(srcMode, dstMode);
}
@@ -105,9 +105,9 @@
}
}
-void Blend::getFactors(SkXfermode::Mode mode, bool swapSrcDst, GLenum* outSrc, GLenum* outDst) {
- *outSrc = swapSrcDst ? kBlendsSwap[mode].src : kBlends[mode].src;
- *outDst = swapSrcDst ? kBlendsSwap[mode].dst : kBlends[mode].dst;
+void Blend::getFactors(SkXfermode::Mode mode, ModeOrderSwap modeUsage, GLenum* outSrc, GLenum* outDst) {
+ *outSrc = (modeUsage == ModeOrderSwap::Swap) ? kBlendsSwap[mode].src : kBlends[mode].src;
+ *outDst = (modeUsage == ModeOrderSwap::Swap) ? kBlendsSwap[mode].dst : kBlends[mode].dst;
}
void Blend::setFactors(GLenum srcMode, GLenum dstMode) {
diff --git a/libs/hwui/renderstate/Blend.h b/libs/hwui/renderstate/Blend.h
index 6d0c115c..dcc681d 100644
--- a/libs/hwui/renderstate/Blend.h
+++ b/libs/hwui/renderstate/Blend.h
@@ -29,11 +29,18 @@
class Blend {
friend class RenderState;
public:
- void enable(SkXfermode::Mode mode, bool swapSrcDst);
+ // dictates whether to swap src/dst
+ enum class ModeOrderSwap {
+ NoSwap,
+ Swap,
+ };
+
+ void enable(SkXfermode::Mode mode, ModeOrderSwap modeUsage);
void disable();
void syncEnabled();
- static void getFactors(SkXfermode::Mode mode, bool swapSrcDst, GLenum* outSrc, GLenum* outDst);
+ static void getFactors(SkXfermode::Mode mode, ModeOrderSwap modeUsage,
+ GLenum* outSrc, GLenum* outDst);
void setFactors(GLenum src, GLenum dst);
void dump();
diff --git a/libs/hwui/renderstate/MeshState.cpp b/libs/hwui/renderstate/MeshState.cpp
index 6b00020..0521f65 100644
--- a/libs/hwui/renderstate/MeshState.cpp
+++ b/libs/hwui/renderstate/MeshState.cpp
@@ -37,7 +37,7 @@
mCurrentBuffer = mUnitQuadBuffer;
- std::unique_ptr<uint16_t[]> regionIndices(new uint16_t[kMaxNumberOfQuads * 6]);
+ uint16_t regionIndices[kMaxNumberOfQuads * 6];
for (uint32_t i = 0; i < kMaxNumberOfQuads; i++) {
uint16_t quad = i * 4;
int index = i * 6;
@@ -50,8 +50,7 @@
}
glGenBuffers(1, &mQuadListIndices);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mQuadListIndices);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, kMaxNumberOfQuads * 6 * sizeof(uint16_t),
- regionIndices.get(), GL_STATIC_DRAW);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(regionIndices), regionIndices, GL_STATIC_DRAW);
mCurrentIndicesBuffer = mQuadListIndices;
// position attribute always enabled
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/java/android/media/midi/MidiDevice.java b/media/java/android/media/midi/MidiDevice.java
index 1a39485..af0737d 100644
--- a/media/java/android/media/midi/MidiDevice.java
+++ b/media/java/android/media/midi/MidiDevice.java
@@ -24,11 +24,13 @@
import android.os.RemoteException;
import android.util.Log;
+import dalvik.system.CloseGuard;
+
import java.io.Closeable;
import java.io.IOException;
/**
- * This class is used for sending and receiving data to and from an MIDI device
+ * This class is used for sending and receiving data to and from a MIDI device
* Instances of this class are created by {@link MidiManager#openDevice}.
*
* CANDIDATE FOR PUBLIC API
@@ -42,11 +44,10 @@
private Context mContext;
private ServiceConnection mServiceConnection;
+ private final CloseGuard mGuard = CloseGuard.get();
+
/* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server) {
- mDeviceInfo = deviceInfo;
- mDeviceServer = server;
- mContext = null;
- mServiceConnection = null;
+ this(deviceInfo, server, null, null);
}
/* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server,
@@ -55,6 +56,7 @@
mDeviceServer = server;
mContext = context;
mServiceConnection = serviceConnection;
+ mGuard.open("close");
}
/**
@@ -108,10 +110,23 @@
@Override
public void close() throws IOException {
- if (mContext != null && mServiceConnection != null) {
- mContext.unbindService(mServiceConnection);
- mContext = null;
- mServiceConnection = null;
+ synchronized (mGuard) {
+ mGuard.close();
+ if (mContext != null && mServiceConnection != null) {
+ mContext.unbindService(mServiceConnection);
+ mContext = null;
+ mServiceConnection = null;
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ mGuard.warnIfOpen();
+ close();
+ } finally {
+ super.finalize();
}
}
diff --git a/media/java/android/media/midi/MidiDeviceInfo.java b/media/java/android/media/midi/MidiDeviceInfo.java
index b7756fd..1e97258 100644
--- a/media/java/android/media/midi/MidiDeviceInfo.java
+++ b/media/java/android/media/midi/MidiDeviceInfo.java
@@ -31,7 +31,7 @@
* CANDIDATE FOR PUBLIC API
* @hide
*/
-public class MidiDeviceInfo implements Parcelable {
+public final class MidiDeviceInfo implements Parcelable {
private static final String TAG = "MidiDeviceInfo";
diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java
index 4d59c63..3b4b6f0 100644
--- a/media/java/android/media/midi/MidiDeviceServer.java
+++ b/media/java/android/media/midi/MidiDeviceServer.java
@@ -24,11 +24,14 @@
import android.system.OsConstants;
import android.util.Log;
+import dalvik.system.CloseGuard;
+
import libcore.io.IoUtils;
import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* Internal class used for providing an implementation for a MIDI device.
@@ -54,6 +57,12 @@
// MidiOutputPorts for clients connected to our input ports
private final MidiOutputPort[] mInputPortOutputPorts;
+ // List of all MidiInputPorts we created
+ private final CopyOnWriteArrayList<MidiInputPort> mInputPorts
+ = new CopyOnWriteArrayList<MidiInputPort>();
+
+ private final CloseGuard mGuard = CloseGuard.get();
+
abstract private class PortClient implements IBinder.DeathRecipient {
final IBinder mToken;
@@ -105,6 +114,7 @@
void close() {
mToken.unlinkToDeath(this, 0);
mOutputPortDispatchers[mInputPort.getPortNumber()].getSender().disconnect(mInputPort);
+ mInputPorts.remove(mInputPort);
IoUtils.closeQuietly(mInputPort);
}
}
@@ -169,6 +179,7 @@
OsConstants.SOCK_SEQPACKET);
MidiInputPort inputPort = new MidiInputPort(pair[0], portNumber);
mOutputPortDispatchers[portNumber].getSender().connect(inputPort);
+ mInputPorts.add(inputPort);
OutputPortClient client = new OutputPortClient(token, inputPort);
synchronized (mPortClients) {
mPortClients.put(token, client);
@@ -204,6 +215,8 @@
for (int i = 0; i < numOutputPorts; i++) {
mOutputPortDispatchers[i] = new MidiDispatcher();
}
+
+ mGuard.open("close");
}
/* package */ IMidiDeviceServer getBinderInterface() {
@@ -219,11 +232,35 @@
@Override
public void close() throws IOException {
+ synchronized (mGuard) {
+ mGuard.close();
+
+ for (int i = 0; i < mInputPortCount; i++) {
+ MidiOutputPort outputPort = mInputPortOutputPorts[i];
+ if (outputPort != null) {
+ IoUtils.closeQuietly(outputPort);
+ mInputPortOutputPorts[i] = null;
+ }
+ }
+ for (MidiInputPort inputPort : mInputPorts) {
+ IoUtils.closeQuietly(inputPort);
+ }
+ mInputPorts.clear();
+ try {
+ mMidiManager.unregisterDeviceServer(mServer);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in unregisterDeviceServer");
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
try {
- // FIXME - close input and output ports too?
- mMidiManager.unregisterDeviceServer(mServer);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in unregisterDeviceServer");
+ mGuard.warnIfOpen();
+ close();
+ } finally {
+ super.finalize();
}
}
diff --git a/media/java/android/media/midi/MidiDispatcher.java b/media/java/android/media/midi/MidiDispatcher.java
index b2d8b6f..a5193f1 100644
--- a/media/java/android/media/midi/MidiDispatcher.java
+++ b/media/java/android/media/midi/MidiDispatcher.java
@@ -17,7 +17,7 @@
package android.media.midi;
import java.io.IOException;
-import java.util.ArrayList;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s.
@@ -29,9 +29,10 @@
* CANDIDATE FOR PUBLIC API
* @hide
*/
-public class MidiDispatcher extends MidiReceiver {
+public final class MidiDispatcher extends MidiReceiver {
- private final ArrayList<MidiReceiver> mReceivers = new ArrayList<MidiReceiver>();
+ private final CopyOnWriteArrayList<MidiReceiver> mReceivers
+ = new CopyOnWriteArrayList<MidiReceiver>();
private final MidiSender mSender = new MidiSender() {
/**
@@ -72,17 +73,12 @@
@Override
public void receive(byte[] msg, int offset, int count, long timestamp) throws IOException {
- synchronized (mReceivers) {
- for (int i = 0; i < mReceivers.size(); ) {
- MidiReceiver receiver = mReceivers.get(i);
- try {
- receiver.receive(msg, offset, count, timestamp);
- i++; // increment only on success. on failure we remove the receiver
- // so i should not be incremented
- } catch (IOException e) {
- // if the receiver fails we remove the receiver but do not propogate the exception
- mSender.disconnect(receiver);
- }
+ for (MidiReceiver receiver : mReceivers) {
+ try {
+ receiver.receive(msg, offset, count, timestamp);
+ } catch (IOException e) {
+ // if the receiver fails we remove the receiver but do not propogate the exception
+ mReceivers.remove(receiver);
}
}
}
diff --git a/media/java/android/media/midi/MidiInputPort.java b/media/java/android/media/midi/MidiInputPort.java
index 5d944cb..8674969 100644
--- a/media/java/android/media/midi/MidiInputPort.java
+++ b/media/java/android/media/midi/MidiInputPort.java
@@ -35,15 +35,16 @@
* CANDIDATE FOR PUBLIC API
* @hide
*/
-public class MidiInputPort extends MidiReceiver implements Closeable {
+public final class MidiInputPort extends MidiReceiver implements Closeable {
private static final String TAG = "MidiInputPort";
- private final IMidiDeviceServer mDeviceServer;
+ private IMidiDeviceServer mDeviceServer;
private final IBinder mToken;
private final int mPortNumber;
private final FileOutputStream mOutputStream;
private final CloseGuard mGuard = CloseGuard.get();
+ private boolean mIsClosed;
// buffer to use for sending data out our output stream
private final byte[] mBuffer = new byte[MidiPortImpl.MAX_PACKET_SIZE];
@@ -97,23 +98,27 @@
@Override
public void close() throws IOException {
- mGuard.close();
- mOutputStream.close();
- if (mDeviceServer != null) {
- try {
- mDeviceServer.closePort(mToken);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in MidiInputPort.close()");
+ synchronized (mGuard) {
+ if (mIsClosed) return;
+ mGuard.close();
+ mOutputStream.close();
+ if (mDeviceServer != null) {
+ try {
+ mDeviceServer.closePort(mToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in MidiInputPort.close()");
+ }
}
+ mIsClosed = true;
}
}
@Override
protected void finalize() throws Throwable {
try {
- if (mGuard != null) {
- mGuard.warnIfOpen();
- }
+ mGuard.warnIfOpen();
+ // not safe to make binder calls from finalize()
+ mDeviceServer = null;
close();
} finally {
super.finalize();
diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java
index 08ac25a..d7b8c57 100644
--- a/media/java/android/media/midi/MidiManager.java
+++ b/media/java/android/media/midi/MidiManager.java
@@ -42,7 +42,7 @@
* CANDIDATE FOR PUBLIC API
* @hide
*/
-public class MidiManager {
+public final class MidiManager {
private static final String TAG = "MidiManager";
private final Context mContext;
diff --git a/media/java/android/media/midi/MidiOutputPort.java b/media/java/android/media/midi/MidiOutputPort.java
index d46b202..b31cdd3 100644
--- a/media/java/android/media/midi/MidiOutputPort.java
+++ b/media/java/android/media/midi/MidiOutputPort.java
@@ -35,16 +35,17 @@
* CANDIDATE FOR PUBLIC API
* @hide
*/
-public class MidiOutputPort extends MidiSender implements Closeable {
+public final class MidiOutputPort extends MidiSender implements Closeable {
private static final String TAG = "MidiOutputPort";
- private final IMidiDeviceServer mDeviceServer;
+ private IMidiDeviceServer mDeviceServer;
private final IBinder mToken;
private final int mPortNumber;
private final FileInputStream mInputStream;
private final MidiDispatcher mDispatcher = new MidiDispatcher();
private final CloseGuard mGuard = CloseGuard.get();
+ private boolean mIsClosed;
// This thread reads MIDI events from a socket and distributes them to the list of
// MidiReceivers attached to this device.
@@ -113,23 +114,28 @@
@Override
public void close() throws IOException {
- mGuard.close();
- mInputStream.close();
- if (mDeviceServer != null) {
- try {
- mDeviceServer.closePort(mToken);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in MidiOutputPort.close()");
+ synchronized (mGuard) {
+ if (mIsClosed) return;
+
+ mGuard.close();
+ mInputStream.close();
+ if (mDeviceServer != null) {
+ try {
+ mDeviceServer.closePort(mToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in MidiOutputPort.close()");
+ }
}
+ mIsClosed = true;
}
}
@Override
protected void finalize() throws Throwable {
try {
- if (mGuard != null) {
- mGuard.warnIfOpen();
- }
+ mGuard.warnIfOpen();
+ // not safe to make binder calls from finalize()
+ mDeviceServer = null;
close();
} finally {
super.finalize();
diff --git a/media/jni/android_media_ImageReader.cpp b/media/jni/android_media_ImageReader.cpp
index 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/recents/misc/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
index 925d17e..84544ff 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
@@ -33,7 +33,7 @@
private static Method sPropertyMethod;
static {
try {
- Class<?> c = Class.forName("android.view.GLES20Canvas");
+ Class<?> c = Class.forName("android.view.DisplayListCanvas");
sPropertyMethod = c.getDeclaredMethod("setProperty", String.class, String.class);
if (!sPropertyMethod.isAccessible()) sPropertyMethod.setAccessible(true);
} catch (ClassNotFoundException e) {
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/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/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 7e69e87..a4f3f23 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -496,7 +496,12 @@
int mLastDisplayFreezeDuration = 0;
Object mLastFinishedFreezeSource = null;
boolean mWaitingForConfig = false;
- boolean mWindowsFreezingScreen = false;
+
+ final static int WINDOWS_FREEZING_SCREENS_NONE = 0;
+ final static int WINDOWS_FREEZING_SCREENS_ACTIVE = 1;
+ final static int WINDOWS_FREEZING_SCREENS_TIMEOUT = 2;
+ private int mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_NONE;
+
boolean mClientFreezingScreen = false;
int mAppsFreezingScreen = 0;
int mLastWindowForcedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@@ -4699,7 +4704,7 @@
if (mAppsFreezingScreen == 1) {
startFreezingDisplayLocked(false, 0, 0);
mH.removeMessages(H.APP_FREEZE_TIMEOUT);
- mH.sendEmptyMessageDelayed(H.APP_FREEZE_TIMEOUT, 5000);
+ mH.sendEmptyMessageDelayed(H.APP_FREEZE_TIMEOUT, 2000);
}
}
final int N = wtoken.allAppWindows.size();
@@ -6415,7 +6420,7 @@
mAltOrientation = altOrientation;
mPolicy.setRotationLw(mRotation);
- mWindowsFreezingScreen = true;
+ mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_ACTIVE;
mH.removeMessages(H.WINDOW_FREEZE_TIMEOUT);
mH.sendEmptyMessageDelayed(H.WINDOW_FREEZE_TIMEOUT, WINDOW_FREEZE_TIMEOUT_DURATION);
mWaitingForConfig = true;
@@ -7784,6 +7789,7 @@
// TODO(multidisplay): Can non-default displays rotate?
synchronized (mWindowMap) {
Slog.w(TAG, "Window freeze timeout expired.");
+ mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_TIMEOUT;
final WindowList windows = getDefaultWindowListLocked();
int i = windows.size();
while (i > 0) {
@@ -7851,6 +7857,7 @@
case APP_FREEZE_TIMEOUT: {
synchronized (mWindowMap) {
Slog.w(TAG, "App freeze timeout expired.");
+ mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_TIMEOUT;
final int numStacks = mStackIdToStack.size();
for (int stackNdx = 0; stackNdx < numStacks; ++stackNdx) {
final TaskStack stack = mStackIdToStack.valueAt(stackNdx);
@@ -8914,8 +8921,8 @@
w.mOrientationChanging = true;
w.mLastFreezeDuration = 0;
mInnerFields.mOrientationChangeComplete = false;
- if (!mWindowsFreezingScreen) {
- mWindowsFreezingScreen = true;
+ if (mWindowsFreezingScreen == WINDOWS_FREEZING_SCREENS_NONE) {
+ mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_ACTIVE;
// XXX should probably keep timeout from
// when we first froze the display.
mH.removeMessages(H.WINDOW_FREEZE_TIMEOUT);
@@ -9905,8 +9912,8 @@
"With display frozen, orientationChangeComplete="
+ mInnerFields.mOrientationChangeComplete);
if (mInnerFields.mOrientationChangeComplete) {
- if (mWindowsFreezingScreen) {
- mWindowsFreezingScreen = false;
+ if (mWindowsFreezingScreen != WINDOWS_FREEZING_SCREENS_NONE) {
+ mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_NONE;
mLastFinishedFreezeSource = mInnerFields.mLastWindowFreezeSource;
mH.removeMessages(H.WINDOW_FREEZE_TIMEOUT);
}
@@ -10188,7 +10195,7 @@
} else {
mInnerFields.mOrientationChangeComplete = true;
mInnerFields.mLastWindowFreezeSource = mAnimator.mLastWindowFreezeSource;
- if (mWindowsFreezingScreen) {
+ if (mWindowsFreezingScreen != WINDOWS_FREEZING_SCREENS_NONE) {
doRequest = true;
}
}
@@ -10524,7 +10531,8 @@
return;
}
- if (mWaitingForConfig || mAppsFreezingScreen > 0 || mWindowsFreezingScreen
+ if (mWaitingForConfig || mAppsFreezingScreen > 0
+ || mWindowsFreezingScreen == WINDOWS_FREEZING_SCREENS_ACTIVE
|| mClientFreezingScreen) {
if (DEBUG_ORIENTATION) Slog.d(TAG,
"stopFreezingDisplayLocked: Returning mWaitingForConfig=" + mWaitingForConfig
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;
}
/**