diff options
148 files changed, 7670 insertions, 3676 deletions
diff --git a/api/current.txt b/api/current.txt index ccdac7e..3cc9cc8 100644 --- a/api/current.txt +++ b/api/current.txt @@ -1301,6 +1301,7 @@ package android { field public static final int thicknessRatio = 16843164; // 0x101019c field public static final int thumb = 16843074; // 0x1010142 field public static final int thumbOffset = 16843075; // 0x1010143 + field public static final int thumbPosition = 16844013; // 0x10104ed field public static final int thumbTextPadding = 16843634; // 0x1010372 field public static final int thumbTint = 16843889; // 0x1010471 field public static final int thumbTintMode = 16843890; // 0x1010472 @@ -9725,15 +9726,13 @@ package android.database { method public void registerDataSetObserver(android.database.DataSetObserver); method public boolean requery(); method public android.os.Bundle respond(android.os.Bundle); + method public void setExtras(android.os.Bundle); method public void setNotificationUri(android.content.ContentResolver, android.net.Uri); method public void unregisterContentObserver(android.database.ContentObserver); method public void unregisterDataSetObserver(android.database.DataSetObserver); - field protected boolean mClosed; - field protected android.content.ContentResolver mContentResolver; - field protected deprecated java.lang.Long mCurrentRowID; - field protected int mPos; - field protected deprecated int mRowIdColumnIndex; - field protected deprecated java.util.HashMap<java.lang.Long, java.util.Map<java.lang.String, java.lang.Object>> mUpdatedRows; + field protected deprecated boolean mClosed; + field protected deprecated android.content.ContentResolver mContentResolver; + field protected deprecated int mPos; } protected static class AbstractCursor.SelfContentObserver extends android.database.ContentObserver { @@ -9833,6 +9832,7 @@ package android.database { method public abstract void registerDataSetObserver(android.database.DataSetObserver); method public abstract deprecated boolean requery(); method public abstract android.os.Bundle respond(android.os.Bundle); + method public abstract void setExtras(android.os.Bundle); method public abstract void setNotificationUri(android.content.ContentResolver, android.net.Uri); method public abstract void unregisterContentObserver(android.database.ContentObserver); method public abstract void unregisterDataSetObserver(android.database.DataSetObserver); @@ -9904,7 +9904,7 @@ package android.database { ctor public CursorWrapper(android.database.Cursor); method public void close(); method public void copyStringToBuffer(int, android.database.CharArrayBuffer); - method public void deactivate(); + method public deprecated void deactivate(); method public byte[] getBlob(int); method public int getColumnCount(); method public int getColumnIndex(java.lang.String); @@ -9938,8 +9938,9 @@ package android.database { method public boolean moveToPrevious(); method public void registerContentObserver(android.database.ContentObserver); method public void registerDataSetObserver(android.database.DataSetObserver); - method public boolean requery(); + method public deprecated boolean requery(); method public android.os.Bundle respond(android.os.Bundle); + method public void setExtras(android.os.Bundle); method public void setNotificationUri(android.content.ContentResolver, android.net.Uri); method public void unregisterContentObserver(android.database.ContentObserver); method public void unregisterDataSetObserver(android.database.DataSetObserver); @@ -14992,6 +14993,7 @@ package android.media { field public static final java.lang.String PARAMETER_KEY_REQUEST_SYNC_FRAME = "request-sync"; field public static final java.lang.String PARAMETER_KEY_SUSPEND = "drop-input-frames"; field public static final java.lang.String PARAMETER_KEY_VIDEO_BITRATE = "video-bitrate"; + field public static final int REASON_RECLAIMED = 1; // 0x1 field public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = 1; // 0x1 field public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = 2; // 0x2 } @@ -15007,6 +15009,7 @@ package android.media { public static abstract class MediaCodec.Callback { ctor public MediaCodec.Callback(); + method public void onCodecReleased(android.media.MediaCodec, int); method public abstract void onError(android.media.MediaCodec, android.media.MediaCodec.CodecException); method public abstract void onInputBufferAvailable(android.media.MediaCodec, int); method public abstract void onOutputBufferAvailable(android.media.MediaCodec, int, android.media.MediaCodec.BufferInfo); @@ -15327,6 +15330,8 @@ package android.media { method public void removeKeys(byte[]); method public void restoreKeys(byte[], byte[]); method public void setOnEventListener(android.media.MediaDrm.OnEventListener); + method public void setOnExpirationUpdateListener(android.media.MediaDrm.OnExpirationUpdateListener, android.os.Handler); + method public void setOnKeysChangeListener(android.media.MediaDrm.OnKeysChangeListener, android.os.Handler); method public void setPropertyByteArray(java.lang.String, byte[]); method public void setPropertyString(java.lang.String, java.lang.String); field public static final int EVENT_KEY_EXPIRED = 3; // 0x3 @@ -15334,6 +15339,11 @@ package android.media { field public static final deprecated int EVENT_PROVISION_REQUIRED = 1; // 0x1 field public static final int EVENT_SESSION_RECLAIMED = 5; // 0x5 field public static final int EVENT_VENDOR_DEFINED = 4; // 0x4 + field public static final int KEY_STATUS_EXPIRED = 1; // 0x1 + field public static final int KEY_STATUS_INTERNAL_ERROR = 4; // 0x4 + field public static final int KEY_STATUS_OUTPUT_NOT_ALLOWED = 2; // 0x2 + field public static final int KEY_STATUS_PENDING = 3; // 0x3 + field public static final int KEY_STATUS_USABLE = 0; // 0x0 field public static final int KEY_TYPE_OFFLINE = 2; // 0x2 field public static final int KEY_TYPE_RELEASE = 3; // 0x3 field public static final int KEY_TYPE_STREAMING = 1; // 0x1 @@ -15360,6 +15370,11 @@ package android.media { method public int getRequestType(); } + public static final class MediaDrm.KeyStatus { + method public byte[] getKeyId(); + method public int getStatusCode(); + } + public static final class MediaDrm.MediaDrmStateException extends java.lang.IllegalStateException { method public java.lang.String getDiagnosticInfo(); } @@ -15368,6 +15383,14 @@ package android.media { method public abstract void onEvent(android.media.MediaDrm, byte[], int, int, byte[]); } + public static abstract interface MediaDrm.OnExpirationUpdateListener { + method public abstract void onExpirationUpdate(android.media.MediaDrm, byte[], long); + } + + public static abstract interface MediaDrm.OnKeysChangeListener { + method public abstract void onKeysChange(android.media.MediaDrm, byte[], java.util.List<android.media.MediaDrm.KeyStatus>, boolean); + } + public static final class MediaDrm.ProvisionRequest { method public byte[] getData(); method public java.lang.String getDefaultUrl(); @@ -15576,6 +15599,7 @@ package android.media { field public static final int METADATA_KEY_ARTIST = 2; // 0x2 field public static final int METADATA_KEY_AUTHOR = 3; // 0x3 field public static final int METADATA_KEY_BITRATE = 20; // 0x14 + field public static final int METADATA_KEY_CAPTURE_FRAMERATE = 25; // 0x19 field public static final int METADATA_KEY_CD_TRACK_NUMBER = 0; // 0x0 field public static final int METADATA_KEY_COMPILATION = 15; // 0xf field public static final int METADATA_KEY_COMPOSER = 4; // 0x4 @@ -16815,11 +16839,17 @@ package android.media.midi { public final class MidiManager { method public android.media.midi.MidiDeviceInfo[] getDeviceList(); + method public void openBluetoothDevice(android.bluetooth.BluetoothDevice, android.media.midi.MidiManager.BluetoothOpenCallback, android.os.Handler); method public void openDevice(android.media.midi.MidiDeviceInfo, android.media.midi.MidiManager.DeviceOpenCallback, android.os.Handler); method public void registerDeviceCallback(android.media.midi.MidiManager.DeviceCallback, android.os.Handler); method public void unregisterDeviceCallback(android.media.midi.MidiManager.DeviceCallback); } + public static abstract class MidiManager.BluetoothOpenCallback { + ctor public MidiManager.BluetoothOpenCallback(); + method public abstract void onDeviceOpened(android.bluetooth.BluetoothDevice, android.media.midi.MidiDevice); + } + public static class MidiManager.DeviceCallback { ctor public MidiManager.DeviceCallback(); method public void onDeviceAdded(android.media.midi.MidiDeviceInfo); @@ -16841,6 +16871,7 @@ package android.media.midi { public abstract class MidiReceiver { ctor public MidiReceiver(); + method public void flush() throws java.io.IOException; method public int getMaxMessageSize(); method public abstract void onReceive(byte[], int, int, long) throws java.io.IOException; method public void send(byte[], int, int) throws java.io.IOException; @@ -22246,6 +22277,9 @@ package android.os { public class BatteryManager { method public int getIntProperty(int); method public long getLongProperty(int); + method public boolean isCharging(); + field public static final java.lang.String ACTION_CHARGING = "android.os.action.CHARGING"; + field public static final java.lang.String ACTION_DISCHARGING = "android.os.action.DISCHARGING"; field public static final int BATTERY_HEALTH_COLD = 7; // 0x7 field public static final int BATTERY_HEALTH_DEAD = 4; // 0x4 field public static final int BATTERY_HEALTH_GOOD = 2; // 0x2 @@ -30524,7 +30558,7 @@ package android.test.mock { ctor public MockCursor(); method public void close(); method public void copyStringToBuffer(int, android.database.CharArrayBuffer); - method public void deactivate(); + method public deprecated void deactivate(); method public byte[] getBlob(int); method public int getColumnCount(); method public int getColumnIndex(java.lang.String); @@ -30557,8 +30591,9 @@ package android.test.mock { method public boolean moveToPrevious(); method public void registerContentObserver(android.database.ContentObserver); method public void registerDataSetObserver(android.database.DataSetObserver); - method public boolean requery(); + method public deprecated boolean requery(); method public android.os.Bundle respond(android.os.Bundle); + method public void setExtras(android.os.Bundle); method public void setNotificationUri(android.content.ContentResolver, android.net.Uri); method public void unregisterContentObserver(android.database.ContentObserver); method public void unregisterDataSetObserver(android.database.DataSetObserver); @@ -32506,6 +32541,7 @@ package android.transition { ctor public TransitionManager(); method public static void beginDelayedTransition(android.view.ViewGroup); method public static void beginDelayedTransition(android.view.ViewGroup, android.transition.Transition); + method public static void endTransitions(android.view.ViewGroup); method public static void go(android.transition.Scene); method public static void go(android.transition.Scene, android.transition.Transition); method public void setTransition(android.transition.Scene, android.transition.Transition); @@ -33524,10 +33560,10 @@ package android.view { method public java.lang.String getName(); method public int getProductId(); method public int getSources(); - method public java.lang.String getUniqueId(); method public int getVendorId(); method public android.os.Vibrator getVibrator(); method public boolean[] hasKeys(int...); + method public boolean hasMic(); method public boolean isVirtual(); method public boolean supportsSource(int); method public void writeToParcel(android.os.Parcel, int); diff --git a/api/removed.txt b/api/removed.txt index c2b9d3e..0c433c3 100644 --- a/api/removed.txt +++ b/api/removed.txt @@ -6,6 +6,16 @@ package android.content.pm { } +package android.database { + + public abstract class AbstractCursor implements android.database.CrossProcessCursor { + field protected java.lang.Long mCurrentRowID; + field protected int mRowIdColumnIndex; + field protected java.util.HashMap<java.lang.Long, java.util.Map<java.lang.String, java.lang.Object>> mUpdatedRows; + } + +} + package android.media { public class AudioFormat { diff --git a/api/system-current.txt b/api/system-current.txt index 7dad2bf..e1e0764 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -1378,6 +1378,7 @@ package android { field public static final int thicknessRatio = 16843164; // 0x101019c field public static final int thumb = 16843074; // 0x1010142 field public static final int thumbOffset = 16843075; // 0x1010143 + field public static final int thumbPosition = 16844013; // 0x10104ed field public static final int thumbTextPadding = 16843634; // 0x1010372 field public static final int thumbTint = 16843889; // 0x1010471 field public static final int thumbTintMode = 16843890; // 0x1010472 @@ -5914,6 +5915,7 @@ package android.app.backup { method public android.app.backup.RestoreSession beginRestoreSession(); method public void dataChanged(); method public static void dataChanged(java.lang.String); + method public long getAvailableRestoreToken(java.lang.String); method public java.lang.String getCurrentTransport(); method public boolean isBackupEnabled(); method public java.lang.String[] listAllTransports(); @@ -10015,15 +10017,13 @@ package android.database { method public void registerDataSetObserver(android.database.DataSetObserver); method public boolean requery(); method public android.os.Bundle respond(android.os.Bundle); + method public void setExtras(android.os.Bundle); method public void setNotificationUri(android.content.ContentResolver, android.net.Uri); method public void unregisterContentObserver(android.database.ContentObserver); method public void unregisterDataSetObserver(android.database.DataSetObserver); - field protected boolean mClosed; - field protected android.content.ContentResolver mContentResolver; - field protected deprecated java.lang.Long mCurrentRowID; - field protected int mPos; - field protected deprecated int mRowIdColumnIndex; - field protected deprecated java.util.HashMap<java.lang.Long, java.util.Map<java.lang.String, java.lang.Object>> mUpdatedRows; + field protected deprecated boolean mClosed; + field protected deprecated android.content.ContentResolver mContentResolver; + field protected deprecated int mPos; } protected static class AbstractCursor.SelfContentObserver extends android.database.ContentObserver { @@ -10123,6 +10123,7 @@ package android.database { method public abstract void registerDataSetObserver(android.database.DataSetObserver); method public abstract deprecated boolean requery(); method public abstract android.os.Bundle respond(android.os.Bundle); + method public abstract void setExtras(android.os.Bundle); method public abstract void setNotificationUri(android.content.ContentResolver, android.net.Uri); method public abstract void unregisterContentObserver(android.database.ContentObserver); method public abstract void unregisterDataSetObserver(android.database.DataSetObserver); @@ -10194,7 +10195,7 @@ package android.database { ctor public CursorWrapper(android.database.Cursor); method public void close(); method public void copyStringToBuffer(int, android.database.CharArrayBuffer); - method public void deactivate(); + method public deprecated void deactivate(); method public byte[] getBlob(int); method public int getColumnCount(); method public int getColumnIndex(java.lang.String); @@ -10228,8 +10229,9 @@ package android.database { method public boolean moveToPrevious(); method public void registerContentObserver(android.database.ContentObserver); method public void registerDataSetObserver(android.database.DataSetObserver); - method public boolean requery(); + method public deprecated boolean requery(); method public android.os.Bundle respond(android.os.Bundle); + method public void setExtras(android.os.Bundle); method public void setNotificationUri(android.content.ContentResolver, android.net.Uri); method public void unregisterContentObserver(android.database.ContentObserver); method public void unregisterDataSetObserver(android.database.DataSetObserver); @@ -16200,6 +16202,7 @@ package android.media { field public static final java.lang.String PARAMETER_KEY_REQUEST_SYNC_FRAME = "request-sync"; field public static final java.lang.String PARAMETER_KEY_SUSPEND = "drop-input-frames"; field public static final java.lang.String PARAMETER_KEY_VIDEO_BITRATE = "video-bitrate"; + field public static final int REASON_RECLAIMED = 1; // 0x1 field public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = 1; // 0x1 field public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = 2; // 0x2 } @@ -16215,6 +16218,7 @@ package android.media { public static abstract class MediaCodec.Callback { ctor public MediaCodec.Callback(); + method public void onCodecReleased(android.media.MediaCodec, int); method public abstract void onError(android.media.MediaCodec, android.media.MediaCodec.CodecException); method public abstract void onInputBufferAvailable(android.media.MediaCodec, int); method public abstract void onOutputBufferAvailable(android.media.MediaCodec, int, android.media.MediaCodec.BufferInfo); @@ -16535,6 +16539,8 @@ package android.media { method public void removeKeys(byte[]); method public void restoreKeys(byte[], byte[]); method public void setOnEventListener(android.media.MediaDrm.OnEventListener); + method public void setOnExpirationUpdateListener(android.media.MediaDrm.OnExpirationUpdateListener, android.os.Handler); + method public void setOnKeysChangeListener(android.media.MediaDrm.OnKeysChangeListener, android.os.Handler); method public void setPropertyByteArray(java.lang.String, byte[]); method public void setPropertyString(java.lang.String, java.lang.String); method public void unprovisionDevice(); @@ -16543,6 +16549,11 @@ package android.media { field public static final deprecated int EVENT_PROVISION_REQUIRED = 1; // 0x1 field public static final int EVENT_SESSION_RECLAIMED = 5; // 0x5 field public static final int EVENT_VENDOR_DEFINED = 4; // 0x4 + field public static final int KEY_STATUS_EXPIRED = 1; // 0x1 + field public static final int KEY_STATUS_INTERNAL_ERROR = 4; // 0x4 + field public static final int KEY_STATUS_OUTPUT_NOT_ALLOWED = 2; // 0x2 + field public static final int KEY_STATUS_PENDING = 3; // 0x3 + field public static final int KEY_STATUS_USABLE = 0; // 0x0 field public static final int KEY_TYPE_OFFLINE = 2; // 0x2 field public static final int KEY_TYPE_RELEASE = 3; // 0x3 field public static final int KEY_TYPE_STREAMING = 1; // 0x1 @@ -16569,6 +16580,11 @@ package android.media { method public int getRequestType(); } + public static final class MediaDrm.KeyStatus { + method public byte[] getKeyId(); + method public int getStatusCode(); + } + public static final class MediaDrm.MediaDrmStateException extends java.lang.IllegalStateException { method public java.lang.String getDiagnosticInfo(); } @@ -16577,6 +16593,14 @@ package android.media { method public abstract void onEvent(android.media.MediaDrm, byte[], int, int, byte[]); } + public static abstract interface MediaDrm.OnExpirationUpdateListener { + method public abstract void onExpirationUpdate(android.media.MediaDrm, byte[], long); + } + + public static abstract interface MediaDrm.OnKeysChangeListener { + method public abstract void onKeysChange(android.media.MediaDrm, byte[], java.util.List<android.media.MediaDrm.KeyStatus>, boolean); + } + public static final class MediaDrm.ProvisionRequest { method public byte[] getData(); method public java.lang.String getDefaultUrl(); @@ -16785,6 +16809,7 @@ package android.media { field public static final int METADATA_KEY_ARTIST = 2; // 0x2 field public static final int METADATA_KEY_AUTHOR = 3; // 0x3 field public static final int METADATA_KEY_BITRATE = 20; // 0x14 + field public static final int METADATA_KEY_CAPTURE_FRAMERATE = 25; // 0x19 field public static final int METADATA_KEY_CD_TRACK_NUMBER = 0; // 0x0 field public static final int METADATA_KEY_COMPILATION = 15; // 0xf field public static final int METADATA_KEY_COMPOSER = 4; // 0x4 @@ -18090,11 +18115,17 @@ package android.media.midi { public final class MidiManager { method public android.media.midi.MidiDeviceInfo[] getDeviceList(); + method public void openBluetoothDevice(android.bluetooth.BluetoothDevice, android.media.midi.MidiManager.BluetoothOpenCallback, android.os.Handler); method public void openDevice(android.media.midi.MidiDeviceInfo, android.media.midi.MidiManager.DeviceOpenCallback, android.os.Handler); method public void registerDeviceCallback(android.media.midi.MidiManager.DeviceCallback, android.os.Handler); method public void unregisterDeviceCallback(android.media.midi.MidiManager.DeviceCallback); } + public static abstract class MidiManager.BluetoothOpenCallback { + ctor public MidiManager.BluetoothOpenCallback(); + method public abstract void onDeviceOpened(android.bluetooth.BluetoothDevice, android.media.midi.MidiDevice); + } + public static class MidiManager.DeviceCallback { ctor public MidiManager.DeviceCallback(); method public void onDeviceAdded(android.media.midi.MidiDeviceInfo); @@ -18116,6 +18147,7 @@ package android.media.midi { public abstract class MidiReceiver { ctor public MidiReceiver(); + method public void flush() throws java.io.IOException; method public int getMaxMessageSize(); method public abstract void onReceive(byte[], int, int, long) throws java.io.IOException; method public void send(byte[], int, int) throws java.io.IOException; @@ -24117,6 +24149,9 @@ package android.os { public class BatteryManager { method public int getIntProperty(int); method public long getLongProperty(int); + method public boolean isCharging(); + field public static final java.lang.String ACTION_CHARGING = "android.os.action.CHARGING"; + field public static final java.lang.String ACTION_DISCHARGING = "android.os.action.DISCHARGING"; field public static final int BATTERY_HEALTH_COLD = 7; // 0x7 field public static final int BATTERY_HEALTH_DEAD = 4; // 0x4 field public static final int BATTERY_HEALTH_GOOD = 2; // 0x2 @@ -33126,7 +33161,7 @@ package android.test.mock { ctor public MockCursor(); method public void close(); method public void copyStringToBuffer(int, android.database.CharArrayBuffer); - method public void deactivate(); + method public deprecated void deactivate(); method public byte[] getBlob(int); method public int getColumnCount(); method public int getColumnIndex(java.lang.String); @@ -33159,8 +33194,9 @@ package android.test.mock { method public boolean moveToPrevious(); method public void registerContentObserver(android.database.ContentObserver); method public void registerDataSetObserver(android.database.DataSetObserver); - method public boolean requery(); + method public deprecated boolean requery(); method public android.os.Bundle respond(android.os.Bundle); + method public void setExtras(android.os.Bundle); method public void setNotificationUri(android.content.ContentResolver, android.net.Uri); method public void unregisterContentObserver(android.database.ContentObserver); method public void unregisterDataSetObserver(android.database.DataSetObserver); @@ -35110,6 +35146,7 @@ package android.transition { ctor public TransitionManager(); method public static void beginDelayedTransition(android.view.ViewGroup); method public static void beginDelayedTransition(android.view.ViewGroup, android.transition.Transition); + method public static void endTransitions(android.view.ViewGroup); method public static void go(android.transition.Scene); method public static void go(android.transition.Scene, android.transition.Transition); method public void setTransition(android.transition.Scene, android.transition.Transition); @@ -36128,10 +36165,10 @@ package android.view { method public java.lang.String getName(); method public int getProductId(); method public int getSources(); - method public java.lang.String getUniqueId(); method public int getVendorId(); method public android.os.Vibrator getVibrator(); method public boolean[] hasKeys(int...); + method public boolean hasMic(); method public boolean isVirtual(); method public boolean supportsSource(int); method public void writeToParcel(android.os.Parcel, int); diff --git a/api/system-removed.txt b/api/system-removed.txt index c2b9d3e..0c433c3 100644 --- a/api/system-removed.txt +++ b/api/system-removed.txt @@ -6,6 +6,16 @@ package android.content.pm { } +package android.database { + + public abstract class AbstractCursor implements android.database.CrossProcessCursor { + field protected java.lang.Long mCurrentRowID; + field protected int mRowIdColumnIndex; + field protected java.util.HashMap<java.lang.Long, java.util.Map<java.lang.String, java.lang.Object>> mUpdatedRows; + } + +} + package android.media { public class AudioFormat { diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 8f125d7..2b35cd4 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -269,45 +269,51 @@ public class ActivityManager { * all activities that are visible to the user. */ public static final int PROCESS_STATE_TOP = 2; + /** @hide Process is hosting a foreground service. */ + public static final int PROCESS_STATE_FOREGROUND_SERVICE = 3; + + /** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */ + public static final int PROCESS_STATE_TOP_SLEEPING = 4; + /** @hide Process is important to the user, and something they are aware of. */ - public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 3; + public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 5; /** @hide Process is important to the user, but not something they are aware of. */ - public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 4; + public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 6; /** @hide Process is in the background running a backup/restore operation. */ - public static final int PROCESS_STATE_BACKUP = 5; + public static final int PROCESS_STATE_BACKUP = 7; /** @hide Process is in the background, but it can't restore its state so we want * to try to avoid killing it. */ - public static final int PROCESS_STATE_HEAVY_WEIGHT = 6; + public static final int PROCESS_STATE_HEAVY_WEIGHT = 8; /** @hide Process is in the background running a service. Unlike oom_adj, this level * is used for both the normal running in background state and the executing * operations state. */ - public static final int PROCESS_STATE_SERVICE = 7; + public static final int PROCESS_STATE_SERVICE = 9; /** @hide Process is in the background running a receiver. Note that from the * perspective of oom_adj receivers run at a higher foreground level, but for our * prioritization here that is not necessary and putting them below services means * many fewer changes in some process states as they receive broadcasts. */ - public static final int PROCESS_STATE_RECEIVER = 8; + public static final int PROCESS_STATE_RECEIVER = 10; /** @hide Process is in the background but hosts the home activity. */ - public static final int PROCESS_STATE_HOME = 9; + public static final int PROCESS_STATE_HOME = 11; /** @hide Process is in the background but hosts the last shown activity. */ - public static final int PROCESS_STATE_LAST_ACTIVITY = 10; + public static final int PROCESS_STATE_LAST_ACTIVITY = 12; /** @hide Process is being cached for later use and contains activities. */ - public static final int PROCESS_STATE_CACHED_ACTIVITY = 11; + public static final int PROCESS_STATE_CACHED_ACTIVITY = 13; /** @hide Process is being cached for later use and is a client of another cached * process that contains activities. */ - public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 12; + public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 14; /** @hide Process is being cached for later use and is empty. */ - public static final int PROCESS_STATE_CACHED_EMPTY = 13; + public static final int PROCESS_STATE_CACHED_EMPTY = 15; /** @hide requestType for assist context: only basic information. */ public static final int ASSIST_CONTEXT_BASIC = 0; @@ -2064,7 +2070,7 @@ public class ActivityManager { return ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE; } else if (procState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND) { return ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE; - } else if (procState >= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) { + } else if (procState >= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { return ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; } else { return ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java index 5dd02ae..179957d 100644 --- a/core/java/android/app/AlarmManager.java +++ b/core/java/android/app/AlarmManager.java @@ -115,6 +115,40 @@ public class AlarmManager /** @hide */ public static final long WINDOW_HEURISTIC = -1; + /** + * Flag for alarms: this is to be a stand-alone alarm, that should not be batched with + * other alarms. + * @hide + */ + public static final int FLAG_STANDALONE = 1<<0; + + /** + * Flag for alarms: this alarm would like to wake the device even if it is idle. This + * is, for example, an alarm for an alarm clock. + * @hide + */ + public static final int FLAG_WAKE_FROM_IDLE = 1<<1; + + /** + * Flag for alarms: this alarm would like to still execute even if the device is + * idle. This won't bring the device out of idle, just allow this specific alarm to + * run. Note that this means the actual time this alarm goes off can be inconsistent + * with the time of non-allow-while-idle alarms (it could go earlier than the time + * requested by another alarm). + * + * @hide + */ + public static final int FLAG_ALLOW_WHILE_IDLE = 1<<2; + + /** + * Flag for alarms: this alarm marks the point where we would like to come out of idle + * mode. It may be moved by the alarm manager to match the first wake-from-idle alarm. + * Scheduling an alarm with this flag puts the alarm manager in to idle mode, where it + * avoids scheduling any further alarms until the marker alarm is executed. + * @hide + */ + public static final int FLAG_IDLE_UNTIL = 1<<3; + private final IAlarmManager mService; private final boolean mAlwaysExact; @@ -204,7 +238,7 @@ public class AlarmManager * @see #RTC_WAKEUP */ public void set(int type, long triggerAtMillis, PendingIntent operation) { - setImpl(type, triggerAtMillis, legacyExactLength(), 0, operation, null, null); + setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, operation, null, null); } /** @@ -265,7 +299,8 @@ public class AlarmManager */ public void setRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation) { - setImpl(type, triggerAtMillis, legacyExactLength(), intervalMillis, operation, null, null); + setImpl(type, triggerAtMillis, legacyExactLength(), intervalMillis, 0, operation, null, + null); } /** @@ -315,7 +350,7 @@ public class AlarmManager */ public void setWindow(int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation) { - setImpl(type, windowStartMillis, windowLengthMillis, 0, operation, null, null); + setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, operation, null, null); } /** @@ -353,7 +388,16 @@ public class AlarmManager * @see #RTC_WAKEUP */ public void setExact(int type, long triggerAtMillis, PendingIntent operation) { - setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, operation, null, null); + setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null); + } + + /** + * Schedule an idle-until alarm, which will keep the alarm manager idle until + * the given time. + * @hide + */ + public void setIdleUntil(int type, long triggerAtMillis, PendingIntent operation) { + setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_IDLE_UNTIL, operation, null, null); } /** @@ -381,18 +425,19 @@ public class AlarmManager * @see android.content.Intent#filterEquals */ public void setAlarmClock(AlarmClockInfo info, PendingIntent operation) { - setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, operation, null, info); + setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, 0, operation, null, info); } /** @hide */ @SystemApi public void set(int type, long triggerAtMillis, long windowMillis, long intervalMillis, PendingIntent operation, WorkSource workSource) { - setImpl(type, triggerAtMillis, windowMillis, intervalMillis, operation, workSource, null); + setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, operation, workSource, + null); } private void setImpl(int type, long triggerAtMillis, long windowMillis, long intervalMillis, - PendingIntent operation, WorkSource workSource, AlarmClockInfo alarmClock) { + int flags, PendingIntent operation, WorkSource workSource, AlarmClockInfo alarmClock) { if (triggerAtMillis < 0) { /* NOTYET if (mAlwaysExact) { @@ -405,7 +450,7 @@ public class AlarmManager } try { - mService.set(type, triggerAtMillis, windowMillis, intervalMillis, operation, + mService.set(type, triggerAtMillis, windowMillis, intervalMillis, flags, operation, workSource, alarmClock); } catch (RemoteException ex) { } @@ -506,7 +551,7 @@ public class AlarmManager */ public void setInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation) { - setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, intervalMillis, operation, null, null); + setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, intervalMillis, 0, operation, null, null); } /** diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java index 3e545f9..2dbbc38 100644 --- a/core/java/android/app/AlertDialog.java +++ b/core/java/android/app/AlertDialog.java @@ -134,6 +134,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * {@code context}'s theme. * * @param context the parent context + * @see android.R.styleable#Theme_alertDialogTheme */ protected AlertDialog(Context context) { this(context, 0); @@ -155,6 +156,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * {@code context}'s theme. * * @param context the parent context + * @see android.R.styleable#Theme_alertDialogTheme */ protected AlertDialog(Context context, boolean cancelable, OnCancelListener cancelListener) { this(context, 0); @@ -187,16 +189,24 @@ public class AlertDialog extends Dialog implements DialogInterface { * @param themeResId the resource ID of the theme against which to inflate * this dialog, or {@code 0} to use the parent * {@code context}'s default alert dialog theme + * @see android.R.styleable#Theme_alertDialogTheme */ protected AlertDialog(Context context, @AttrRes int themeResId) { - super(context, resolveDialogTheme(context, themeResId)); + this(context, themeResId, true); + } + + AlertDialog(Context context, @AttrRes int themeResId, boolean createContextThemeWrapper) { + super(context, createContextThemeWrapper ? resolveDialogTheme(context, themeResId) : 0, + createContextThemeWrapper); mWindow.alwaysReadCloseOnTouchAttr(); mAlert = new AlertController(getContext(), this, getWindow()); } static int resolveDialogTheme(Context context, int themeResId) { - if (themeResId == THEME_TRADITIONAL) { + if (themeResId == 0) { + return 0; + } else if (themeResId == THEME_TRADITIONAL) { return R.style.Theme_Dialog_Alert; } else if (themeResId == THEME_HOLO_DARK) { return R.style.Theme_Holo_Dialog_Alert; @@ -428,7 +438,6 @@ public class AlertDialog extends Dialog implements DialogInterface { public static class Builder { private final AlertController.AlertParams P; - private int mThemeResId; /** * Creates a builder for an alert dialog that uses the default alert @@ -473,7 +482,6 @@ public class AlertDialog extends Dialog implements DialogInterface { public Builder(Context context, int themeResId) { P = new AlertController.AlertParams(new ContextThemeWrapper( context, resolveDialogTheme(context, themeResId))); - mThemeResId = themeResId; } /** @@ -1075,7 +1083,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * create and display the dialog. */ public AlertDialog create() { - final AlertDialog dialog = new AlertDialog(P.mContext, mThemeResId); + final AlertDialog dialog = new AlertDialog(P.mContext); P.apply(dialog.mAlert); dialog.setCancelable(P.mCancelable); if (P.mCancelable) { diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 9defcbe..6a2d207 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -21,7 +21,6 @@ import android.annotation.DrawableRes; import android.annotation.IdRes; import android.annotation.LayoutRes; import android.annotation.StringRes; -import com.android.internal.app.WindowDecorActionBar; import android.annotation.Nullable; import android.content.ComponentName; @@ -56,6 +55,9 @@ import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; +import com.android.internal.R; +import com.android.internal.app.WindowDecorActionBar; + import java.lang.ref.WeakReference; /** @@ -130,27 +132,32 @@ public class Dialog implements DialogInterface, Window.Callback, }; /** - * Create a Dialog window that uses the default dialog frame style. - * - * @param context The Context the Dialog is to run it. In particular, it - * uses the window manager and theme in this context to - * present its UI. + * Creates a dialog window that uses the default dialog theme. + * <p> + * The supplied {@code context} is used to obtain the window manager and + * base theme used to present the dialog. + * + * @param context the context in which the dialog should run + * @see android.R.styleable#Theme_dialogTheme */ public Dialog(Context context) { this(context, 0, true); } /** - * Create a Dialog window that uses a custom dialog style. + * Creates a dialog window that uses a custom dialog style. + * <p> + * The supplied {@code context} is used to obtain the window manager and + * base theme used to present the dialog. + * <p> + * The supplied {@code theme} is applied on top of the context's theme. See + * <a href="{@docRoot}guide/topics/resources/available-resources.html#stylesandthemes"> + * Style and Theme Resources</a> for more information about defining and + * using styles. * - * @param context The Context in which the Dialog should run. In particular, it - * uses the window manager and theme from this context to - * present its UI. - * @param theme A style resource describing the theme to use for the - * window. See <a href="{@docRoot}guide/topics/resources/available-resources.html#stylesandthemes">Style - * and Theme Resources</a> for more information about defining and using - * styles. This theme is applied on top of the current theme in - * <var>context</var>. If 0, the default dialog theme will be used. + * @param context the context in which the dialog should run + * @param theme a style resource describing the theme to use for the + * window, or {@code 0} to use the default dialog theme */ public Dialog(Context context, int theme) { this(context, theme, true); @@ -159,9 +166,8 @@ public class Dialog implements DialogInterface, Window.Callback, Dialog(Context context, int theme, boolean createContextThemeWrapper) { if (createContextThemeWrapper) { if (theme == 0) { - TypedValue outValue = new TypedValue(); - context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme, - outValue, true); + final TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true); theme = outValue.resourceId; } mContext = new ContextThemeWrapper(context, theme); @@ -170,12 +176,14 @@ public class Dialog implements DialogInterface, Window.Callback, } mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); - Window w = new PhoneWindow(mContext); + + final Window w = new PhoneWindow(mContext); mWindow = w; w.setCallback(this); w.setOnWindowDismissedCallback(this); w.setWindowManager(mWindowManager, null, null); w.setGravity(Gravity.CENTER); + mListenersHandler = new ListenersHandler(this); } diff --git a/core/java/android/app/IAlarmManager.aidl b/core/java/android/app/IAlarmManager.aidl index 194082e..d5719f5 100644 --- a/core/java/android/app/IAlarmManager.aidl +++ b/core/java/android/app/IAlarmManager.aidl @@ -28,7 +28,7 @@ import android.os.WorkSource; interface IAlarmManager { /** windowLength == 0 means exact; windowLength < 0 means the let the OS decide */ void set(int type, long triggerAtTime, long windowLength, - long interval, in PendingIntent operation, in WorkSource workSource, + long interval, int flags, in PendingIntent operation, in WorkSource workSource, in AlarmManager.AlarmClockInfo alarmClock); boolean setTime(long millis); void setTimeZone(String zone); diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 33262b3..e2230da 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -76,12 +76,10 @@ interface INotificationManager boolean matchesCallFilter(in Bundle extras); boolean isSystemConditionProviderEnabled(String path); + int getZenMode(); ZenModeConfig getZenModeConfig(); - boolean setZenModeConfig(in ZenModeConfig config); - oneway void setZenMode(int mode); + boolean setZenModeConfig(in ZenModeConfig config, String reason); + oneway void setZenMode(int mode, in Uri conditionId, String reason); oneway void notifyConditions(String pkg, in IConditionProvider provider, in Condition[] conditions); oneway void requestZenModeConditions(in IConditionListener callback, int relevance); - oneway void setZenModeCondition(in Condition condition); - oneway void setAutomaticZenModeConditions(in Uri[] conditionIds); - Condition[] getAutomaticZenModeConditions(); } diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 479327d..fa61e18 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -20,6 +20,7 @@ import android.annotation.SdkConstant; import android.app.Notification.Builder; import android.content.ComponentName; import android.content.Context; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -27,7 +28,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; import android.os.UserHandle; -import android.service.notification.Condition; +import android.provider.Settings.Global; import android.service.notification.IConditionListener; import android.service.notification.ZenModeConfig; import android.util.Log; @@ -282,10 +283,10 @@ public class NotificationManager /** * @hide */ - public void setZenMode(int mode) { + public void setZenMode(int mode, Uri conditionId, String reason) { INotificationManager service = getService(); try { - service.setZenMode(mode); + service.setZenMode(mode, conditionId, reason); } catch (RemoteException e) { } } @@ -293,6 +294,18 @@ public class NotificationManager /** * @hide */ + public boolean setZenModeConfig(ZenModeConfig config, String reason) { + INotificationManager service = getService(); + try { + return service.setZenModeConfig(config, reason); + } catch (RemoteException e) { + return false; + } + } + + /** + * @hide + */ public void requestZenModeConditions(IConditionListener listener, int relevance) { INotificationManager service = getService(); try { @@ -304,24 +317,22 @@ public class NotificationManager /** * @hide */ - public void setZenModeCondition(Condition exitCondition) { + public int getZenMode() { INotificationManager service = getService(); try { - service.setZenModeCondition(exitCondition); + return service.getZenMode(); } catch (RemoteException e) { } + return Global.ZEN_MODE_OFF; } /** * @hide */ - public Condition getZenModeCondition() { + public ZenModeConfig getZenModeConfig() { INotificationManager service = getService(); try { - final ZenModeConfig config = service.getZenModeConfig(); - if (config != null) { - return config.exitCondition; - } + return service.getZenModeConfig(); } catch (RemoteException e) { } return null; diff --git a/core/java/android/app/VoiceInteractor.java b/core/java/android/app/VoiceInteractor.java index 7acf5f0..022a62c 100644 --- a/core/java/android/app/VoiceInteractor.java +++ b/core/java/android/app/VoiceInteractor.java @@ -318,7 +318,7 @@ public class VoiceInteractor { * @param label The label that will both be matched against what the user speaks * and displayed visually. * @param index The location of this option within the overall set of options. - * Can be used to help identify which the option when it is returned from the + * Can be used to help identify the option when it is returned from the * voice interactor. */ public Option(CharSequence label, int index) { diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java index 9151a16..8b79305 100644 --- a/core/java/android/app/backup/BackupManager.java +++ b/core/java/android/app/backup/BackupManager.java @@ -339,4 +339,30 @@ public class BackupManager { } } } + + /** + * Ask the framework which dataset, if any, the given package's data would be + * restored from if we were to install it right now. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + * + * @param packageName The name of the package whose most-suitable dataset we + * wish to look up + * @return The dataset token from which a restore should be attempted, or zero if + * no suitable data is available. + * + * @hide + */ + @SystemApi + public long getAvailableRestoreToken(String packageName) { + checkServiceBinder(); + if (sService != null) { + try { + return sService.getAvailableRestoreToken(packageName); + } catch (RemoteException e) { + Log.e(TAG, "getAvailableRestoreToken() couldn't connect"); + } + } + return 0; + } } diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index 8f36dc4..87e4ef1 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -313,4 +313,17 @@ interface IBackupManager { * is being queried. */ boolean isBackupServiceActive(int whichUser); + + /** + * Ask the framework which dataset, if any, the given package's data would be + * restored from if we were to install it right now. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + * + * @param packageName The name of the package whose most-suitable dataset we + * wish to look up + * @return The dataset token from which a restore should be attempted, or zero if + * no suitable data is available. + */ + long getAvailableRestoreToken(String packageName); } diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index 590d791..044e3e3 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -517,6 +517,38 @@ public class IntentFilter implements Parcelable { } /** + * Return if this filter handle all HTTP or HTTPS data URI or not. + * + * @return True if the filter handle all HTTP or HTTPS data URI. False otherwise. + * + * This will check if if the Intent action is {@link android.content.Intent#ACTION_VIEW} and + * the Intent category is {@link android.content.Intent#CATEGORY_BROWSABLE} and the Intent + * data scheme is "http" or "https" and that there is no specific host defined. + * + * @hide + */ + public final boolean handleAllWebDataURI() { + return hasWebDataURI() && (countDataAuthorities() == 0); + } + + /** + * Return if this filter has any HTTP or HTTPS data URI or not. + * + * @return True if the filter has any HTTP or HTTPS data URI. False otherwise. + * + * This will check if if the Intent action is {@link android.content.Intent#ACTION_VIEW} and + * the Intent category is {@link android.content.Intent#CATEGORY_BROWSABLE} and the Intent + * data scheme is "http" or "https". + * + * @hide + */ + public final boolean hasWebDataURI() { + return hasAction(Intent.ACTION_VIEW) && + hasCategory(Intent.CATEGORY_BROWSABLE) && + (hasDataScheme(SCHEME_HTTP) || hasDataScheme(SCHEME_HTTPS)); + } + + /** * Return if this filter needs to be automatically verified again its data URIs or not. * * @return True if the filter needs to be automatically verified. False otherwise. @@ -530,10 +562,7 @@ public class IntentFilter implements Parcelable { * @hide */ public final boolean needsVerification() { - return hasAction(Intent.ACTION_VIEW) && - hasCategory(Intent.CATEGORY_BROWSABLE) && - (hasDataScheme(SCHEME_HTTP) || hasDataScheme(SCHEME_HTTPS)) && - getAutoVerify(); + return hasWebDataURI() && getAutoVerify(); } /** diff --git a/core/java/android/content/pm/IntentFilterVerificationInfo.java b/core/java/android/content/pm/IntentFilterVerificationInfo.java index 28cbaa8..e50b0ff 100644 --- a/core/java/android/content/pm/IntentFilterVerificationInfo.java +++ b/core/java/android/content/pm/IntentFilterVerificationInfo.java @@ -36,7 +36,7 @@ import java.util.ArrayList; /** * The {@link com.android.server.pm.PackageManagerService} maintains some - * {@link IntentFilterVerificationInfo}s for each domain / package / class name per user. + * {@link IntentFilterVerificationInfo}s for each domain / package name. * * @hide */ diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index bdbed75..7523675 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -2778,7 +2778,7 @@ public class PackageParser { } /** - * Check if one of the IntentFilter as an action VIEW and a HTTP/HTTPS data URI + * Check if one of the IntentFilter as both actions DEFAULT / VIEW and a HTTP/HTTPS data URI */ private static boolean hasDomainURLs(Package pkg) { if (pkg == null || pkg.activities == null) return false; @@ -2792,8 +2792,10 @@ public class PackageParser { for (int m=0; m<countFilters; m++) { ActivityIntentInfo aii = filters.get(m); if (!aii.hasAction(Intent.ACTION_VIEW)) continue; + if (!aii.hasAction(Intent.ACTION_DEFAULT)) continue; if (aii.hasDataScheme(IntentFilter.SCHEME_HTTP) || aii.hasDataScheme(IntentFilter.SCHEME_HTTPS)) { + Slog.d(TAG, "hasDomainURLs:true for package:" + pkg.packageName); return true; } } diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java index 7b141f0..05f5e90 100644 --- a/core/java/android/content/pm/ResolveInfo.java +++ b/core/java/android/content/pm/ResolveInfo.java @@ -144,9 +144,9 @@ public class ResolveInfo implements Parcelable { public boolean system; /** - * @hide Does the associated IntentFilter needs verification ? + * @hide Does the associated IntentFilter comes from a Browser ? */ - public boolean filterNeedsVerification; + public boolean handleAllWebDataURI; private ComponentInfo getComponentInfo() { if (activityInfo != null) return activityInfo; @@ -288,7 +288,7 @@ public class ResolveInfo implements Parcelable { resolvePackageName = orig.resolvePackageName; system = orig.system; targetUserId = orig.targetUserId; - filterNeedsVerification = orig.filterNeedsVerification; + handleAllWebDataURI = orig.handleAllWebDataURI; } public String toString() { @@ -350,7 +350,7 @@ public class ResolveInfo implements Parcelable { dest.writeInt(targetUserId); dest.writeInt(system ? 1 : 0); dest.writeInt(noResourceId ? 1 : 0); - dest.writeInt(filterNeedsVerification ? 1 : 0); + dest.writeInt(handleAllWebDataURI ? 1 : 0); } public static final Creator<ResolveInfo> CREATOR @@ -396,7 +396,7 @@ public class ResolveInfo implements Parcelable { targetUserId = source.readInt(); system = source.readInt() != 0; noResourceId = source.readInt() != 0; - filterNeedsVerification = source.readInt() != 0; + handleAllWebDataURI = source.readInt() != 0; } public static class DisplayNameComparator diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java index b5b89dd..581fe7f 100644 --- a/core/java/android/database/AbstractCursor.java +++ b/core/java/android/database/AbstractCursor.java @@ -35,34 +35,38 @@ public abstract class AbstractCursor implements CrossProcessCursor { private static final String TAG = "Cursor"; /** - * @deprecated This is never updated by this class and should not be used + * @removed This field should not be used. */ - @Deprecated protected HashMap<Long, Map<String, Object>> mUpdatedRows; - protected int mPos; - /** - * This must be set to the index of the row ID column by any - * subclass that wishes to support updates. - * - * @deprecated This field should not be used. + * @removed This field should not be used. */ - @Deprecated protected int mRowIdColumnIndex; /** - * If {@link #mRowIdColumnIndex} is not -1 this contains contains the value of - * the column at {@link #mRowIdColumnIndex} for the current row this cursor is - * pointing at. - * - * @deprecated This field should not be used. + * @removed This field should not be used. */ - @Deprecated protected Long mCurrentRowID; + /** + * @deprecated Use {@link #getPosition()} instead. + */ + @Deprecated + protected int mPos; + + /** + * @deprecated Use {@link #isClosed()} instead. + */ + @Deprecated protected boolean mClosed; + + /** + * @deprecated Do not use. + */ + @Deprecated protected ContentResolver mContentResolver; + private Uri mNotifyUri; private final Object mSelfObserverLock = new Object(); @@ -76,18 +80,28 @@ public abstract class AbstractCursor implements CrossProcessCursor { /* -------------------------------------------------------- */ /* These need to be implemented by subclasses */ + @Override abstract public int getCount(); + @Override abstract public String[] getColumnNames(); + @Override abstract public String getString(int column); + @Override abstract public short getShort(int column); + @Override abstract public int getInt(int column); + @Override abstract public long getLong(int column); + @Override abstract public float getFloat(int column); + @Override abstract public double getDouble(int column); + @Override abstract public boolean isNull(int column); + @Override public int getType(int column) { // Reflects the assumption that all commonly used field types (meaning everything // but blobs) are convertible to strings so it should be safe to call @@ -96,6 +110,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { } // TODO implement getBlob in all cursor types + @Override public byte[] getBlob(int column) { throw new UnsupportedOperationException("getBlob is not supported"); } @@ -108,14 +123,17 @@ public abstract class AbstractCursor implements CrossProcessCursor { * * @return The pre-filled window that backs this cursor, or null if none. */ + @Override public CursorWindow getWindow() { return null; } + @Override public int getColumnCount() { return getColumnNames().length; } + @Override public void deactivate() { onDeactivateOrClose(); } @@ -129,6 +147,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { mDataSetObservable.notifyInvalidated(); } + @Override public boolean requery() { if (mSelfObserver != null && mSelfObserverRegistered == false) { mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver); @@ -138,10 +157,12 @@ public abstract class AbstractCursor implements CrossProcessCursor { return true; } + @Override public boolean isClosed() { return mClosed; } + @Override public void close() { mClosed = true; mContentObservable.unregisterAll(); @@ -158,11 +179,13 @@ public abstract class AbstractCursor implements CrossProcessCursor { * @param newPosition the position that we're moving to * @return true if the move is successful, false otherwise */ + @Override public boolean onMove(int oldPosition, int newPosition) { return true; } + @Override public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { // Default implementation, uses getString String result = getString(columnIndex); @@ -183,15 +206,14 @@ public abstract class AbstractCursor implements CrossProcessCursor { /* Implementation */ public AbstractCursor() { mPos = -1; - mRowIdColumnIndex = -1; - mCurrentRowID = null; - mUpdatedRows = new HashMap<Long, Map<String, Object>>(); } + @Override public final int getPosition() { return mPos; } + @Override public final boolean moveToPosition(int position) { // Make sure position isn't past the end of the cursor final int count = getCount(); @@ -216,9 +238,6 @@ public abstract class AbstractCursor implements CrossProcessCursor { mPos = -1; } else { mPos = position; - if (mRowIdColumnIndex != -1) { - mCurrentRowID = Long.valueOf(getLong(mRowIdColumnIndex)); - } } return result; @@ -229,35 +248,43 @@ public abstract class AbstractCursor implements CrossProcessCursor { DatabaseUtils.cursorFillWindow(this, position, window); } + @Override public final boolean move(int offset) { return moveToPosition(mPos + offset); } + @Override public final boolean moveToFirst() { return moveToPosition(0); } + @Override public final boolean moveToLast() { return moveToPosition(getCount() - 1); } + @Override public final boolean moveToNext() { return moveToPosition(mPos + 1); } + @Override public final boolean moveToPrevious() { return moveToPosition(mPos - 1); } + @Override public final boolean isFirst() { return mPos == 0 && getCount() != 0; } + @Override public final boolean isLast() { int cnt = getCount(); return mPos == (cnt - 1) && cnt != 0; } + @Override public final boolean isBeforeFirst() { if (getCount() == 0) { return true; @@ -265,6 +292,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { return mPos == -1; } + @Override public final boolean isAfterLast() { if (getCount() == 0) { return true; @@ -272,6 +300,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { return mPos == getCount(); } + @Override public int getColumnIndex(String columnName) { // Hack according to bug 903852 final int periodIndex = columnName.lastIndexOf('.'); @@ -297,6 +326,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { return -1; } + @Override public int getColumnIndexOrThrow(String columnName) { final int index = getColumnIndex(columnName); if (index < 0) { @@ -305,14 +335,17 @@ public abstract class AbstractCursor implements CrossProcessCursor { return index; } + @Override public String getColumnName(int columnIndex) { return getColumnNames()[columnIndex]; } + @Override public void registerContentObserver(ContentObserver observer) { mContentObservable.registerObserver(observer); } + @Override public void unregisterContentObserver(ContentObserver observer) { // cursor will unregister all observers when it close if (!mClosed) { @@ -320,10 +353,12 @@ public abstract class AbstractCursor implements CrossProcessCursor { } } + @Override public void registerDataSetObserver(DataSetObserver observer) { mDataSetObservable.registerObserver(observer); } + @Override public void unregisterDataSetObserver(DataSetObserver observer) { mDataSetObservable.unregisterObserver(observer); } @@ -350,6 +385,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { * @param notifyUri The URI to watch for changes. This can be a * specific row URI, or a base URI for a whole class of content. */ + @Override public void setNotificationUri(ContentResolver cr, Uri notifyUri) { setNotificationUri(cr, notifyUri, UserHandle.myUserId()); } @@ -368,31 +404,29 @@ public abstract class AbstractCursor implements CrossProcessCursor { } } + @Override public Uri getNotificationUri() { synchronized (mSelfObserverLock) { return mNotifyUri; } } + @Override public boolean getWantsAllOnMoveCalls() { return false; } - /** - * Sets a {@link Bundle} that will be returned by {@link #getExtras()}. <code>null</code> will - * be converted into {@link Bundle#EMPTY}. - * - * @param extras {@link Bundle} to set. - * @hide - */ + @Override public void setExtras(Bundle extras) { mExtras = (extras == null) ? Bundle.EMPTY : extras; } + @Override public Bundle getExtras() { return mExtras; } + @Override public Bundle respond(Bundle extras) { return Bundle.EMPTY; } diff --git a/core/java/android/database/BulkCursorToCursorAdaptor.java b/core/java/android/database/BulkCursorToCursorAdaptor.java index 98c7043..8576715 100644 --- a/core/java/android/database/BulkCursorToCursorAdaptor.java +++ b/core/java/android/database/BulkCursorToCursorAdaptor.java @@ -41,7 +41,6 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor { public void initialize(BulkCursorDescriptor d) { mBulkCursor = d.cursor; mColumns = d.columnNames; - mRowIdColumnIndex = DatabaseUtils.findRowIdColumnIndex(mColumns); mWantsAllOnMoveCalls = d.wantsAllOnMoveCalls; mCount = d.count; if (d.window != null) { diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java index fc2a885..d10c9b8 100644 --- a/core/java/android/database/Cursor.java +++ b/core/java/android/database/Cursor.java @@ -444,6 +444,13 @@ public interface Cursor extends Closeable { boolean getWantsAllOnMoveCalls(); /** + * Sets a {@link Bundle} that will be returned by {@link #getExtras()}. + * + * @param extras {@link Bundle} to set, or null to set an empty bundle. + */ + void setExtras(Bundle extras); + + /** * Returns a bundle of extra values. This is an optional way for cursors to provide out-of-band * metadata to their users. One use of this is for reporting on the progress of network requests * that are required to fetch data for the cursor. diff --git a/core/java/android/database/CursorWrapper.java b/core/java/android/database/CursorWrapper.java index d8fcb17..63a2792 100644 --- a/core/java/android/database/CursorWrapper.java +++ b/core/java/android/database/CursorWrapper.java @@ -45,163 +45,210 @@ public class CursorWrapper implements Cursor { return mCursor; } + @Override public void close() { mCursor.close(); } + @Override public boolean isClosed() { return mCursor.isClosed(); } + @Override public int getCount() { return mCursor.getCount(); } + @Override + @Deprecated public void deactivate() { mCursor.deactivate(); } + @Override public boolean moveToFirst() { return mCursor.moveToFirst(); } + @Override public int getColumnCount() { return mCursor.getColumnCount(); } + @Override public int getColumnIndex(String columnName) { return mCursor.getColumnIndex(columnName); } + @Override public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { return mCursor.getColumnIndexOrThrow(columnName); } + @Override public String getColumnName(int columnIndex) { return mCursor.getColumnName(columnIndex); } + @Override public String[] getColumnNames() { return mCursor.getColumnNames(); } + @Override public double getDouble(int columnIndex) { return mCursor.getDouble(columnIndex); } + @Override + public void setExtras(Bundle extras) { + mCursor.setExtras(extras); + } + + @Override public Bundle getExtras() { return mCursor.getExtras(); } + @Override public float getFloat(int columnIndex) { return mCursor.getFloat(columnIndex); } + @Override public int getInt(int columnIndex) { return mCursor.getInt(columnIndex); } + @Override public long getLong(int columnIndex) { return mCursor.getLong(columnIndex); } + @Override public short getShort(int columnIndex) { return mCursor.getShort(columnIndex); } + @Override public String getString(int columnIndex) { return mCursor.getString(columnIndex); } + @Override public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { mCursor.copyStringToBuffer(columnIndex, buffer); } + @Override public byte[] getBlob(int columnIndex) { return mCursor.getBlob(columnIndex); } + @Override public boolean getWantsAllOnMoveCalls() { return mCursor.getWantsAllOnMoveCalls(); } + @Override public boolean isAfterLast() { return mCursor.isAfterLast(); } + @Override public boolean isBeforeFirst() { return mCursor.isBeforeFirst(); } + @Override public boolean isFirst() { return mCursor.isFirst(); } + @Override public boolean isLast() { return mCursor.isLast(); } + @Override public int getType(int columnIndex) { return mCursor.getType(columnIndex); } + @Override public boolean isNull(int columnIndex) { return mCursor.isNull(columnIndex); } + @Override public boolean moveToLast() { return mCursor.moveToLast(); } + @Override public boolean move(int offset) { return mCursor.move(offset); } + @Override public boolean moveToPosition(int position) { return mCursor.moveToPosition(position); } + @Override public boolean moveToNext() { return mCursor.moveToNext(); } + @Override public int getPosition() { return mCursor.getPosition(); } + @Override public boolean moveToPrevious() { return mCursor.moveToPrevious(); } + @Override public void registerContentObserver(ContentObserver observer) { - mCursor.registerContentObserver(observer); + mCursor.registerContentObserver(observer); } + @Override public void registerDataSetObserver(DataSetObserver observer) { - mCursor.registerDataSetObserver(observer); + mCursor.registerDataSetObserver(observer); } + @Override + @Deprecated public boolean requery() { return mCursor.requery(); } + @Override public Bundle respond(Bundle extras) { return mCursor.respond(extras); } + @Override public void setNotificationUri(ContentResolver cr, Uri uri) { - mCursor.setNotificationUri(cr, uri); + mCursor.setNotificationUri(cr, uri); } + @Override public Uri getNotificationUri() { return mCursor.getNotificationUri(); } + @Override public void unregisterContentObserver(ContentObserver observer) { - mCursor.unregisterContentObserver(observer); + mCursor.unregisterContentObserver(observer); } + @Override public void unregisterDataSetObserver(DataSetObserver observer) { mCursor.unregisterDataSetObserver(observer); } diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index 5a1a8e2..2dc5ca4 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -105,7 +105,6 @@ public class SQLiteCursor extends AbstractWindowedCursor { mQuery = query; mColumns = query.getColumnNames(); - mRowIdColumnIndex = DatabaseUtils.findRowIdColumnIndex(mColumns); } /** diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java index bd5a392..cccc4be 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -16,10 +16,12 @@ package android.os; +import android.content.Context; import android.os.BatteryProperty; import android.os.IBatteryPropertiesRegistrar; import android.os.RemoteException; import android.os.ServiceManager; +import com.android.internal.app.IBatteryStats; /** * The BatteryManager class contains strings and constants used for values @@ -128,6 +130,26 @@ public class BatteryManager { public static final int BATTERY_PLUGGED_ANY = BATTERY_PLUGGED_AC | BATTERY_PLUGGED_USB | BATTERY_PLUGGED_WIRELESS; + /** + * Sent when the device's battery has started charging (or has reached full charge + * and the device is on power). This is a good time to do work that you would like to + * avoid doing while on battery (that is to avoid draining the user's battery due to + * things they don't care enough about). + * + * This is paired with {@link #ACTION_DISCHARGING}. The current state can always + * be retrieved with {@link #isCharging()}. + */ + public static final String ACTION_CHARGING = "android.os.action.CHARGING"; + + /** + * Sent when the device's battery may be discharging, so apps should avoid doing + * extraneous work that would cause it to discharge faster. + * + * This is paired with {@link #ACTION_CHARGING}. The current state can always + * be retrieved with {@link #isCharging()}. + */ + public static final String ACTION_DISCHARGING = "android.os.action.DISCHARGING"; + /* * Battery property identifiers. These must match the values in * frameworks/native/include/batteryservice/BatteryService.h @@ -162,17 +184,34 @@ public class BatteryManager { */ public static final int BATTERY_PROPERTY_ENERGY_COUNTER = 5; + private final IBatteryStats mBatteryStats; private final IBatteryPropertiesRegistrar mBatteryPropertiesRegistrar; /** * @removed Was previously made visible by accident. */ public BatteryManager() { + mBatteryStats = IBatteryStats.Stub.asInterface( + ServiceManager.getService(BatteryStats.SERVICE_NAME)); mBatteryPropertiesRegistrar = IBatteryPropertiesRegistrar.Stub.asInterface( ServiceManager.getService("batteryproperties")); } /** + * Return true if the battery is currently considered to be charging. This means that + * the device is plugged in and is supplying sufficient power that the battery level is + * going up (or the battery is fully charged). Changes in this state are matched by + * broadcasts of {@link #ACTION_CHARGING} and {@link #ACTION_DISCHARGING}. + */ + public boolean isCharging() { + try { + return mBatteryStats.isCharging(); + } catch (RemoteException e) { + return true; + } + } + + /** * Query a battery property from the batteryproperties service. * * Returns the requested value, or Long.MIN_VALUE if property not diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 3051926..1566985 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -1065,6 +1065,7 @@ public abstract class BatteryStats implements Parcelable { public static final int STATE_SCREEN_ON_FLAG = 1<<20; public static final int STATE_BATTERY_PLUGGED_FLAG = 1<<19; public static final int STATE_PHONE_IN_CALL_FLAG = 1<<18; + public static final int STATE_CHARGING_FLAG = 1<<17; public static final int STATE_BLUETOOTH_ON_FLAG = 1<<16; public static final int MOST_INTERESTING_STATES = @@ -1751,6 +1752,7 @@ public abstract class BatteryStats implements Parcelable { new BitDescription(HistoryItem.STATE_SCREEN_ON_FLAG, "screen", "S"), new BitDescription(HistoryItem.STATE_BATTERY_PLUGGED_FLAG, "plugged", "BP"), new BitDescription(HistoryItem.STATE_PHONE_IN_CALL_FLAG, "phone_in_call", "Pcl"), + new BitDescription(HistoryItem.STATE_CHARGING_FLAG, "charging", "ch"), new BitDescription(HistoryItem.STATE_BLUETOOTH_ON_FLAG, "bluetooth", "b"), new BitDescription(HistoryItem.STATE_DATA_CONNECTION_MASK, HistoryItem.STATE_DATA_CONNECTION_SHIFT, "data_conn", "Pcn", diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 5ee8fb3..3087e1d 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -880,6 +880,15 @@ public final class Settings { "android.settings.VOICE_CONTROL_DO_NOT_DISTURB_MODE"; /** + * Activity Action: Show Zen Mode schedule rule configuration settings. + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_ZEN_MODE_SCHEDULE_RULE_SETTINGS + = "android.settings.ZEN_MODE_SCHEDULE_RULE_SETTINGS"; + + /** * Activity Action: Show the regulatory information screen for the device. * <p> * In some cases, a matching Activity may not exist, so ensure you safeguard @@ -7218,6 +7227,18 @@ public final class Settings { return "ZEN_MODE_OFF"; } + /** @hide */ public static boolean isValidZenMode(int value) { + switch (value) { + case Global.ZEN_MODE_OFF: + case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + case Global.ZEN_MODE_ALARMS: + case Global.ZEN_MODE_NO_INTERRUPTIONS: + return true; + default: + return false; + } + } + /** * Opaque value, changes when persisted zen mode configuration changes. * diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 2702457..5aaf2e7 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -22,22 +22,25 @@ import android.content.res.Resources; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.provider.Settings.Global; import android.text.TextUtils; import android.text.format.DateFormat; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Slog; +import com.android.internal.R; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; import java.util.Locale; import java.util.Objects; - -import com.android.internal.R; +import java.util.UUID; /** * Persisted configuration for zen mode. @@ -47,10 +50,6 @@ import com.android.internal.R; public class ZenModeConfig implements Parcelable { private static String TAG = "ZenModeConfig"; - public static final String SLEEP_MODE_NIGHTS = "nights"; - public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights"; - public static final String SLEEP_MODE_DAYS_PREFIX = "days:"; - public static final int SOURCE_ANYONE = 0; public static final int SOURCE_CONTACT = 1; public static final int SOURCE_STAR = 2; @@ -60,6 +59,7 @@ public class ZenModeConfig implements Parcelable { Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY }; public static final int[] WEEKNIGHT_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY }; + public static final int[] WEEKEND_DAYS = { Calendar.FRIDAY, Calendar.SATURDAY }; public static final int[] MINUTE_BUCKETS = new int[] { 15, 30, 45, 60, 120, 180, 240, 480 }; private static final int SECONDS_MS = 1000; @@ -69,7 +69,7 @@ public class ZenModeConfig implements Parcelable { private static final boolean DEFAULT_ALLOW_REMINDERS = true; private static final boolean DEFAULT_ALLOW_EVENTS = true; - private static final int XML_VERSION = 1; + private static final int XML_VERSION = 2; private static final String ZEN_TAG = "zen"; private static final String ZEN_ATT_VERSION = "version"; private static final String ALLOW_TAG = "allow"; @@ -78,14 +78,6 @@ public class ZenModeConfig implements Parcelable { private static final String ALLOW_ATT_FROM = "from"; private static final String ALLOW_ATT_REMINDERS = "reminders"; private static final String ALLOW_ATT_EVENTS = "events"; - private static final String SLEEP_TAG = "sleep"; - private static final String SLEEP_ATT_MODE = "mode"; - private static final String SLEEP_ATT_NONE = "none"; - - private static final String SLEEP_ATT_START_HR = "startHour"; - private static final String SLEEP_ATT_START_MIN = "startMin"; - private static final String SLEEP_ATT_END_HR = "endHour"; - private static final String SLEEP_ATT_END_MIN = "endMin"; private static final String CONDITION_TAG = "condition"; private static final String CONDITION_ATT_COMPONENT = "component"; @@ -97,8 +89,16 @@ public class ZenModeConfig implements Parcelable { private static final String CONDITION_ATT_STATE = "state"; private static final String CONDITION_ATT_FLAGS = "flags"; - private static final String EXIT_CONDITION_TAG = "exitCondition"; - private static final String EXIT_CONDITION_ATT_COMPONENT = "component"; + private static final String MANUAL_TAG = "manual"; + private static final String AUTOMATIC_TAG = "automatic"; + + private static final String RULE_ATT_ID = "id"; + private static final String RULE_ATT_ENABLED = "enabled"; + private static final String RULE_ATT_SNOOZING = "snoozing"; + private static final String RULE_ATT_NAME = "name"; + private static final String RULE_ATT_COMPONENT = "component"; + private static final String RULE_ATT_ZEN = "zen"; + private static final String RULE_ATT_CONDITION_ID = "conditionId"; public boolean allowCalls; public boolean allowMessages; @@ -106,16 +106,8 @@ public class ZenModeConfig implements Parcelable { public boolean allowEvents = DEFAULT_ALLOW_EVENTS; public int allowFrom = SOURCE_ANYONE; - public String sleepMode; - public int sleepStartHour; // 0-23 - public int sleepStartMinute; // 0-59 - public int sleepEndHour; - public int sleepEndMinute; - public boolean sleepNone; // false = priority, true = none - public ComponentName[] conditionComponents; - public Uri[] conditionIds; - public Condition exitCondition; - public ComponentName exitConditionComponent; + public ZenRule manualRule; + public ArrayMap<String, ZenRule> automaticRules = new ArrayMap<>(); public ZenModeConfig() { } @@ -124,27 +116,18 @@ public class ZenModeConfig implements Parcelable { allowMessages = source.readInt() == 1; allowReminders = source.readInt() == 1; allowEvents = source.readInt() == 1; - if (source.readInt() == 1) { - sleepMode = source.readString(); - } - sleepStartHour = source.readInt(); - sleepStartMinute = source.readInt(); - sleepEndHour = source.readInt(); - sleepEndMinute = source.readInt(); - sleepNone = source.readInt() == 1; - int len = source.readInt(); - if (len > 0) { - conditionComponents = new ComponentName[len]; - source.readTypedArray(conditionComponents, ComponentName.CREATOR); - } - len = source.readInt(); + allowFrom = source.readInt(); + manualRule = source.readParcelable(null); + final int len = source.readInt(); if (len > 0) { - conditionIds = new Uri[len]; - source.readTypedArray(conditionIds, Uri.CREATOR); + final String[] ids = new String[len]; + final ZenRule[] rules = new ZenRule[len]; + source.readStringArray(ids); + source.readTypedArray(rules, ZenRule.CREATOR); + for (int i = 0; i < len; i++) { + automaticRules.put(ids[i], rules[i]); + } } - allowFrom = source.readInt(); - exitCondition = source.readParcelable(null); - exitConditionComponent = source.readParcelable(null); } @Override @@ -153,32 +136,22 @@ public class ZenModeConfig implements Parcelable { dest.writeInt(allowMessages ? 1 : 0); dest.writeInt(allowReminders ? 1 : 0); dest.writeInt(allowEvents ? 1 : 0); - if (sleepMode != null) { - dest.writeInt(1); - dest.writeString(sleepMode); - } else { - dest.writeInt(0); - } - dest.writeInt(sleepStartHour); - dest.writeInt(sleepStartMinute); - dest.writeInt(sleepEndHour); - dest.writeInt(sleepEndMinute); - dest.writeInt(sleepNone ? 1 : 0); - if (conditionComponents != null && conditionComponents.length > 0) { - dest.writeInt(conditionComponents.length); - dest.writeTypedArray(conditionComponents, 0); - } else { - dest.writeInt(0); - } - if (conditionIds != null && conditionIds.length > 0) { - dest.writeInt(conditionIds.length); - dest.writeTypedArray(conditionIds, 0); + dest.writeInt(allowFrom); + dest.writeParcelable(manualRule, 0); + if (!automaticRules.isEmpty()) { + final int len = automaticRules.size(); + final String[] ids = new String[len]; + final ZenRule[] rules = new ZenRule[len]; + for (int i = 0; i < len; i++) { + ids[i] = automaticRules.keyAt(i); + rules[i] = automaticRules.valueAt(i); + } + dest.writeInt(len); + dest.writeStringArray(ids); + dest.writeTypedArray(rules, 0); } else { dest.writeInt(0); } - dest.writeInt(allowFrom); - dest.writeParcelable(exitCondition, 0); - dest.writeParcelable(exitConditionComponent, 0); } @Override @@ -189,19 +162,38 @@ public class ZenModeConfig implements Parcelable { .append(",allowFrom=").append(sourceToString(allowFrom)) .append(",allowReminders=").append(allowReminders) .append(",allowEvents=").append(allowEvents) - .append(",sleepMode=").append(sleepMode) - .append(",sleepStart=").append(sleepStartHour).append('.').append(sleepStartMinute) - .append(",sleepEnd=").append(sleepEndHour).append('.').append(sleepEndMinute) - .append(",sleepNone=").append(sleepNone) - .append(",conditionComponents=") - .append(conditionComponents == null ? null : TextUtils.join(",", conditionComponents)) - .append(",conditionIds=") - .append(conditionIds == null ? null : TextUtils.join(",", conditionIds)) - .append(",exitCondition=").append(exitCondition) - .append(",exitConditionComponent=").append(exitConditionComponent) + .append(",automaticRules=").append(automaticRules) + .append(",manualRule=").append(manualRule) .append(']').toString(); } + public boolean isValid() { + if (!isValidManualRule(manualRule)) return false; + final int N = automaticRules.size(); + for (int i = 0; i < N; i++) { + if (!isValidAutomaticRule(automaticRules.valueAt(i))) return false; + } + return true; + } + + private static boolean isValidManualRule(ZenRule rule) { + return rule == null || Global.isValidZenMode(rule.zenMode) && sameCondition(rule); + } + + private static boolean isValidAutomaticRule(ZenRule rule) { + return rule != null && !TextUtils.isEmpty(rule.name) && Global.isValidZenMode(rule.zenMode) + && rule.conditionId != null && sameCondition(rule); + } + + private static boolean sameCondition(ZenRule rule) { + if (rule == null) return false; + if (rule.conditionId == null) { + return rule.condition == null; + } else { + return rule.condition == null || rule.conditionId.equals(rule.condition.id); + } + } + public static String sourceToString(int source) { switch (source) { case SOURCE_ANYONE: @@ -225,45 +217,29 @@ public class ZenModeConfig implements Parcelable { && other.allowFrom == allowFrom && other.allowReminders == allowReminders && other.allowEvents == allowEvents - && Objects.equals(other.sleepMode, sleepMode) - && other.sleepNone == sleepNone - && other.sleepStartHour == sleepStartHour - && other.sleepStartMinute == sleepStartMinute - && other.sleepEndHour == sleepEndHour - && other.sleepEndMinute == sleepEndMinute - && Objects.deepEquals(other.conditionComponents, conditionComponents) - && Objects.deepEquals(other.conditionIds, conditionIds) - && Objects.equals(other.exitCondition, exitCondition) - && Objects.equals(other.exitConditionComponent, exitConditionComponent); + && Objects.equals(other.automaticRules, automaticRules) + && Objects.equals(other.manualRule, manualRule); } @Override public int hashCode() { return Objects.hash(allowCalls, allowMessages, allowFrom, allowReminders, allowEvents, - sleepMode, sleepNone, sleepStartHour, sleepStartMinute, sleepEndHour, - sleepEndMinute, Arrays.hashCode(conditionComponents), Arrays.hashCode(conditionIds), - exitCondition, exitConditionComponent); + automaticRules, manualRule); } - public boolean isValid() { - return isValidHour(sleepStartHour) && isValidMinute(sleepStartMinute) - && isValidHour(sleepEndHour) && isValidMinute(sleepEndMinute) - && isValidSleepMode(sleepMode); - } - - public static boolean isValidSleepMode(String sleepMode) { - return sleepMode == null || sleepMode.equals(SLEEP_MODE_NIGHTS) - || sleepMode.equals(SLEEP_MODE_WEEKNIGHTS) || tryParseDays(sleepMode) != null; + private static String toDayList(int[] days) { + if (days == null || days.length == 0) return ""; + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < days.length; i++) { + if (i > 0) sb.append('.'); + sb.append(days[i]); + } + return sb.toString(); } - public static int[] tryParseDays(String sleepMode) { - if (sleepMode == null) return null; - sleepMode = sleepMode.trim(); - if (SLEEP_MODE_NIGHTS.equals(sleepMode)) return ALL_DAYS; - if (SLEEP_MODE_WEEKNIGHTS.equals(sleepMode)) return WEEKNIGHT_DAYS; - if (!sleepMode.startsWith(SLEEP_MODE_DAYS_PREFIX)) return null; - if (sleepMode.equals(SLEEP_MODE_DAYS_PREFIX)) return null; - final String[] tokens = sleepMode.substring(SLEEP_MODE_DAYS_PREFIX.length()).split(","); + private static int[] tryParseDayList(String dayList, String sep) { + if (dayList == null) return null; + final String[] tokens = dayList.split(sep); if (tokens.length == 0) return null; final int[] rt = new int[tokens.length]; for (int i = 0; i < tokens.length; i++) { @@ -283,7 +259,7 @@ public class ZenModeConfig implements Parcelable { } } - public static ZenModeConfig readXml(XmlPullParser parser) + public static ZenModeConfig readXml(XmlPullParser parser, Migration migration) throws XmlPullParserException, IOException { int type = parser.getEventType(); if (type != XmlPullParser.START_TAG) return null; @@ -291,16 +267,13 @@ public class ZenModeConfig implements Parcelable { if (!ZEN_TAG.equals(tag)) return null; final ZenModeConfig rt = new ZenModeConfig(); final int version = safeInt(parser, ZEN_ATT_VERSION, XML_VERSION); - final ArrayList<ComponentName> conditionComponents = new ArrayList<ComponentName>(); - final ArrayList<Uri> conditionIds = new ArrayList<Uri>(); + if (version == 1) { + final XmlV1 v1 = XmlV1.readXml(parser); + return migration.migrate(v1); + } while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { tag = parser.getName(); if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) { - if (!conditionComponents.isEmpty()) { - rt.conditionComponents = conditionComponents - .toArray(new ComponentName[conditionComponents.size()]); - rt.conditionIds = conditionIds.toArray(new Uri[conditionIds.size()]); - } return rt; } if (type == XmlPullParser.START_TAG) { @@ -314,31 +287,13 @@ public class ZenModeConfig implements Parcelable { if (rt.allowFrom < SOURCE_ANYONE || rt.allowFrom > MAX_SOURCE) { throw new IndexOutOfBoundsException("bad source in config:" + rt.allowFrom); } - } else if (SLEEP_TAG.equals(tag)) { - final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE); - rt.sleepMode = isValidSleepMode(mode)? mode : null; - rt.sleepNone = safeBoolean(parser, SLEEP_ATT_NONE, false); - final int startHour = safeInt(parser, SLEEP_ATT_START_HR, 0); - final int startMinute = safeInt(parser, SLEEP_ATT_START_MIN, 0); - final int endHour = safeInt(parser, SLEEP_ATT_END_HR, 0); - final int endMinute = safeInt(parser, SLEEP_ATT_END_MIN, 0); - rt.sleepStartHour = isValidHour(startHour) ? startHour : 0; - rt.sleepStartMinute = isValidMinute(startMinute) ? startMinute : 0; - rt.sleepEndHour = isValidHour(endHour) ? endHour : 0; - rt.sleepEndMinute = isValidMinute(endMinute) ? endMinute : 0; - } else if (CONDITION_TAG.equals(tag)) { - final ComponentName component = - safeComponentName(parser, CONDITION_ATT_COMPONENT); - final Uri conditionId = safeUri(parser, CONDITION_ATT_ID); - if (component != null && conditionId != null) { - conditionComponents.add(component); - conditionIds.add(conditionId); - } - } else if (EXIT_CONDITION_TAG.equals(tag)) { - rt.exitCondition = readConditionXml(parser); - if (rt.exitCondition != null) { - rt.exitConditionComponent = - safeComponentName(parser, EXIT_CONDITION_ATT_COMPONENT); + } else if (MANUAL_TAG.equals(tag)) { + rt.manualRule = readRuleXml(parser); + } else if (AUTOMATIC_TAG.equals(tag)) { + final String id = parser.getAttributeValue(null, RULE_ATT_ID); + final ZenRule automaticRule = readRuleXml(parser); + if (id != null && automaticRule != null) { + rt.automaticRules.put(id, automaticRule); } } } @@ -358,39 +313,61 @@ public class ZenModeConfig implements Parcelable { out.attribute(null, ALLOW_ATT_FROM, Integer.toString(allowFrom)); out.endTag(null, ALLOW_TAG); - out.startTag(null, SLEEP_TAG); - if (sleepMode != null) { - out.attribute(null, SLEEP_ATT_MODE, sleepMode); - } - out.attribute(null, SLEEP_ATT_NONE, Boolean.toString(sleepNone)); - out.attribute(null, SLEEP_ATT_START_HR, Integer.toString(sleepStartHour)); - out.attribute(null, SLEEP_ATT_START_MIN, Integer.toString(sleepStartMinute)); - out.attribute(null, SLEEP_ATT_END_HR, Integer.toString(sleepEndHour)); - out.attribute(null, SLEEP_ATT_END_MIN, Integer.toString(sleepEndMinute)); - out.endTag(null, SLEEP_TAG); - - if (conditionComponents != null && conditionIds != null - && conditionComponents.length == conditionIds.length) { - for (int i = 0; i < conditionComponents.length; i++) { - out.startTag(null, CONDITION_TAG); - out.attribute(null, CONDITION_ATT_COMPONENT, - conditionComponents[i].flattenToString()); - out.attribute(null, CONDITION_ATT_ID, conditionIds[i].toString()); - out.endTag(null, CONDITION_TAG); - } + if (manualRule != null) { + out.startTag(null, MANUAL_TAG); + writeRuleXml(manualRule, out); + out.endTag(null, MANUAL_TAG); } - if (exitCondition != null && exitConditionComponent != null) { - out.startTag(null, EXIT_CONDITION_TAG); - out.attribute(null, EXIT_CONDITION_ATT_COMPONENT, - exitConditionComponent.flattenToString()); - writeConditionXml(exitCondition, out); - out.endTag(null, EXIT_CONDITION_TAG); + final int N = automaticRules.size(); + for (int i = 0; i < N; i++) { + final String id = automaticRules.keyAt(i); + final ZenRule automaticRule = automaticRules.valueAt(i); + out.startTag(null, AUTOMATIC_TAG); + out.attribute(null, RULE_ATT_ID, id); + writeRuleXml(automaticRule, out); + out.endTag(null, AUTOMATIC_TAG); } out.endTag(null, ZEN_TAG); } + public static ZenRule readRuleXml(XmlPullParser parser) { + final ZenRule rt = new ZenRule(); + rt.enabled = safeBoolean(parser, RULE_ATT_ENABLED, true); + rt.snoozing = safeBoolean(parser, RULE_ATT_SNOOZING, false); + rt.name = parser.getAttributeValue(null, RULE_ATT_NAME); + final String zen = parser.getAttributeValue(null, RULE_ATT_ZEN); + rt.zenMode = tryParseZenMode(zen, -1); + if (rt.zenMode == -1) { + Slog.w(TAG, "Bad zen mode in rule xml:" + zen); + return null; + } + rt.conditionId = safeUri(parser, RULE_ATT_CONDITION_ID); + rt.component = safeComponentName(parser, RULE_ATT_COMPONENT); + rt.condition = readConditionXml(parser); + return rt.condition != null ? rt : null; + } + + public static void writeRuleXml(ZenRule rule, XmlSerializer out) throws IOException { + out.attribute(null, RULE_ATT_ENABLED, Boolean.toString(rule.enabled)); + out.attribute(null, RULE_ATT_SNOOZING, Boolean.toString(rule.snoozing)); + if (rule.name != null) { + out.attribute(null, RULE_ATT_NAME, rule.name); + } + out.attribute(null, RULE_ATT_ZEN, Integer.toString(rule.zenMode)); + if (rule.component != null) { + out.attribute(null, RULE_ATT_COMPONENT, rule.component.flattenToString()); + } + if (rule.conditionId != null) { + out.attribute(null, RULE_ATT_CONDITION_ID, rule.conditionId.toString()); + } + if (rule.condition != null) { + writeConditionXml(rule.condition, out); + } + } + public static Condition readConditionXml(XmlPullParser parser) { final Uri id = safeUri(parser, CONDITION_ATT_ID); + if (id == null) return null; final String summary = parser.getAttributeValue(null, CONDITION_ATT_SUMMARY); final String line1 = parser.getAttributeValue(null, CONDITION_ATT_LINE1); final String line2 = parser.getAttributeValue(null, CONDITION_ATT_LINE2); @@ -446,6 +423,14 @@ public class ZenModeConfig implements Parcelable { return Uri.parse(val); } + public ArraySet<String> getAutomaticRuleNames() { + final ArraySet<String> rt = new ArraySet<String>(); + for (int i = 0; i < automaticRules.size(); i++) { + rt.add(automaticRules.valueAt(i).name); + } + return rt; + } + @Override public int describeContents() { return 0; @@ -475,17 +460,6 @@ public class ZenModeConfig implements Parcelable { } }; - public DowntimeInfo toDowntimeInfo() { - final DowntimeInfo downtime = new DowntimeInfo(); - downtime.startHour = sleepStartHour; - downtime.startMinute = sleepStartMinute; - downtime.endHour = sleepEndHour; - downtime.endMinute = sleepEndMinute; - downtime.mode = sleepMode; - downtime.none = sleepNone; - return downtime; - } - public static Condition toTimeCondition(Context context, int minutesFromNow, int userHandle) { final long now = System.currentTimeMillis(); final long millis = minutesFromNow == 0 ? ZERO_VALUE_MS : minutesFromNow * MINUTES_MS; @@ -548,38 +522,77 @@ public class ZenModeConfig implements Parcelable { return tryParseCountdownConditionId(conditionId) != 0; } - // Built-in downtime conditions - // e.g. condition://android/downtime?start=10.00&end=7.00&mode=days%3A5%2C6&none=false - public static final String DOWNTIME_PATH = "downtime"; + // built-in schedule conditions + public static final String SCHEDULE_PATH = "schedule"; + + public static class ScheduleInfo { + public int[] days; + public int startHour; + public int startMinute; + public int endHour; + public int endMinute; + + @Override + public int hashCode() { + return 0; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ScheduleInfo)) return false; + final ScheduleInfo other = (ScheduleInfo) o; + return toDayList(days).equals(toDayList(other.days)) + && startHour == other.startHour + && startMinute == other.startMinute + && endHour == other.endHour + && endMinute == other.endMinute; + } + + public ScheduleInfo copy() { + final ScheduleInfo rt = new ScheduleInfo(); + if (days != null) { + rt.days = new int[days.length]; + System.arraycopy(days, 0, rt.days, 0, days.length); + } + rt.startHour = startHour; + rt.startMinute = startMinute; + rt.endHour = endHour; + rt.endMinute = endMinute; + return rt; + } + } - public static Uri toDowntimeConditionId(DowntimeInfo downtime) { + public static Uri toScheduleConditionId(ScheduleInfo schedule) { return new Uri.Builder().scheme(Condition.SCHEME) .authority(SYSTEM_AUTHORITY) - .appendPath(DOWNTIME_PATH) - .appendQueryParameter("start", downtime.startHour + "." + downtime.startMinute) - .appendQueryParameter("end", downtime.endHour + "." + downtime.endMinute) - .appendQueryParameter("mode", downtime.mode) - .appendQueryParameter("none", Boolean.toString(downtime.none)) + .appendPath(SCHEDULE_PATH) + .appendQueryParameter("days", toDayList(schedule.days)) + .appendQueryParameter("start", schedule.startHour + "." + schedule.startMinute) + .appendQueryParameter("end", schedule.endHour + "." + schedule.endMinute) .build(); } - public static DowntimeInfo tryParseDowntimeConditionId(Uri conditionId) { - if (!Condition.isValidId(conditionId, SYSTEM_AUTHORITY) - || conditionId.getPathSegments().size() != 1 - || !DOWNTIME_PATH.equals(conditionId.getPathSegments().get(0))) { - return null; - } + public static boolean isValidScheduleConditionId(Uri conditionId) { + return tryParseScheduleConditionId(conditionId) != null; + } + + public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) { + final boolean isSchedule = conditionId != null + && conditionId.getScheme().equals(Condition.SCHEME) + && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY) + && conditionId.getPathSegments().size() == 1 + && conditionId.getPathSegments().get(0).equals(ZenModeConfig.SCHEDULE_PATH); + if (!isSchedule) return null; final int[] start = tryParseHourAndMinute(conditionId.getQueryParameter("start")); final int[] end = tryParseHourAndMinute(conditionId.getQueryParameter("end")); if (start == null || end == null) return null; - final DowntimeInfo downtime = new DowntimeInfo(); - downtime.startHour = start[0]; - downtime.startMinute = start[1]; - downtime.endHour = end[0]; - downtime.endMinute = end[1]; - downtime.mode = conditionId.getQueryParameter("mode"); - downtime.none = Boolean.toString(true).equals(conditionId.getQueryParameter("none")); - return downtime; + final ScheduleInfo rt = new ScheduleInfo(); + rt.days = tryParseDayList(conditionId.getQueryParameter("days"), "\\."); + rt.startHour = start[0]; + rt.startMinute = start[1]; + rt.endHour = end[0]; + rt.endMinute = end[1]; + return rt; } private static int[] tryParseHourAndMinute(String value) { @@ -591,36 +604,268 @@ public class ZenModeConfig implements Parcelable { return isValidHour(hour) && isValidMinute(minute) ? new int[] { hour, minute } : null; } - public static boolean isValidDowntimeConditionId(Uri conditionId) { - return tryParseDowntimeConditionId(conditionId) != null; + private static int tryParseZenMode(String value, int defValue) { + final int rt = tryParseInt(value, defValue); + return Global.isValidZenMode(rt) ? rt : defValue; } - public static class DowntimeInfo { - public int startHour; // 0-23 - public int startMinute; // 0-59 - public int endHour; - public int endMinute; - public String mode; - public boolean none; + public String newRuleId() { + return UUID.randomUUID().toString().replace("-", ""); + } + + public static String getConditionLine1(Context context, ZenModeConfig config, + int userHandle) { + return getConditionLine(context, config, userHandle, true /*useLine1*/); + } + + public static String getConditionSummary(Context context, ZenModeConfig config, + int userHandle) { + return getConditionLine(context, config, userHandle, false /*useLine1*/); + } + + private static String getConditionLine(Context context, ZenModeConfig config, + int userHandle, boolean useLine1) { + if (config == null) return ""; + if (config.manualRule != null) { + final Uri id = config.manualRule.conditionId; + if (id == null) { + return context.getString(com.android.internal.R.string.zen_mode_forever); + } + final long time = tryParseCountdownConditionId(id); + Condition c = config.manualRule.condition; + if (time > 0) { + final long now = System.currentTimeMillis(); + final long span = time - now; + c = toTimeCondition(context, + time, Math.round(span / (float) MINUTES_MS), now, userHandle); + } + final String rt = c == null ? "" : useLine1 ? c.line1 : c.summary; + return TextUtils.isEmpty(rt) ? "" : rt; + } + String summary = ""; + for (ZenRule automaticRule : config.automaticRules.values()) { + if (automaticRule.enabled && !automaticRule.snoozing + && automaticRule.isTrueOrUnknown()) { + if (summary.isEmpty()) { + summary = automaticRule.name; + } else { + summary = context.getResources() + .getString(R.string.zen_mode_rule_name_combination, summary, + automaticRule.name); + } + } + } + return summary; + } + + public static class ZenRule implements Parcelable { + public boolean enabled; + public boolean snoozing; // user manually disabled this instance + public String name; // required for automatic (unique) + public int zenMode; + public Uri conditionId; // required for automatic + public Condition condition; // optional + public ComponentName component; // optional + + public ZenRule() { } + + public ZenRule(Parcel source) { + enabled = source.readInt() == 1; + snoozing = source.readInt() == 1; + if (source.readInt() == 1) { + name = source.readString(); + } + zenMode = source.readInt(); + conditionId = source.readParcelable(null); + condition = source.readParcelable(null); + component = source.readParcelable(null); + } @Override - public int hashCode() { + public int describeContents() { return 0; } @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(enabled ? 1 : 0); + dest.writeInt(snoozing ? 1 : 0); + if (name != null) { + dest.writeInt(1); + dest.writeString(name); + } else { + dest.writeInt(0); + } + dest.writeInt(zenMode); + dest.writeParcelable(conditionId, 0); + dest.writeParcelable(condition, 0); + dest.writeParcelable(component, 0); + } + + @Override + public String toString() { + return new StringBuilder(ZenRule.class.getSimpleName()).append('[') + .append("enabled=").append(enabled) + .append(",snoozing=").append(snoozing) + .append(",name=").append(name) + .append(",zenMode=").append(Global.zenModeToString(zenMode)) + .append(",conditionId=").append(conditionId) + .append(",condition=").append(condition) + .append(",component=").append(component) + .append(']').toString(); + } + + @Override public boolean equals(Object o) { - if (!(o instanceof DowntimeInfo)) return false; - final DowntimeInfo other = (DowntimeInfo) o; - return startHour == other.startHour - && startMinute == other.startMinute - && endHour == other.endHour - && endMinute == other.endMinute - && Objects.equals(mode, other.mode) - && none == other.none; + if (!(o instanceof ZenRule)) return false; + if (o == this) return true; + final ZenRule other = (ZenRule) o; + return other.enabled == enabled + && other.snoozing == snoozing + && Objects.equals(other.name, name) + && other.zenMode == zenMode + && Objects.equals(other.conditionId, conditionId) + && Objects.equals(other.condition, condition) + && Objects.equals(other.component, component); + } + + @Override + public int hashCode() { + return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, + component); + } + + public boolean isTrueOrUnknown() { + return condition == null || condition.state == Condition.STATE_TRUE + || condition.state == Condition.STATE_UNKNOWN; + } + + public static final Parcelable.Creator<ZenRule> CREATOR + = new Parcelable.Creator<ZenRule>() { + @Override + public ZenRule createFromParcel(Parcel source) { + return new ZenRule(source); + } + @Override + public ZenRule[] newArray(int size) { + return new ZenRule[size]; + } + }; + } + + // Legacy config + public static final class XmlV1 { + public static final String SLEEP_MODE_NIGHTS = "nights"; + public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights"; + public static final String SLEEP_MODE_DAYS_PREFIX = "days:"; + + private static final String EXIT_CONDITION_TAG = "exitCondition"; + private static final String EXIT_CONDITION_ATT_COMPONENT = "component"; + private static final String SLEEP_TAG = "sleep"; + private static final String SLEEP_ATT_MODE = "mode"; + private static final String SLEEP_ATT_NONE = "none"; + + private static final String SLEEP_ATT_START_HR = "startHour"; + private static final String SLEEP_ATT_START_MIN = "startMin"; + private static final String SLEEP_ATT_END_HR = "endHour"; + private static final String SLEEP_ATT_END_MIN = "endMin"; + + public boolean allowCalls; + public boolean allowMessages; + public boolean allowReminders = DEFAULT_ALLOW_REMINDERS; + public boolean allowEvents = DEFAULT_ALLOW_EVENTS; + public int allowFrom = SOURCE_ANYONE; + + public String sleepMode; // nights, weeknights, days:1,2,3 Calendar.days + public int sleepStartHour; // 0-23 + public int sleepStartMinute; // 0-59 + public int sleepEndHour; + public int sleepEndMinute; + public boolean sleepNone; // false = priority, true = none + public ComponentName[] conditionComponents; + public Uri[] conditionIds; + public Condition exitCondition; // manual exit condition + public ComponentName exitConditionComponent; // manual exit condition component + + private static boolean isValidSleepMode(String sleepMode) { + return sleepMode == null || sleepMode.equals(SLEEP_MODE_NIGHTS) + || sleepMode.equals(SLEEP_MODE_WEEKNIGHTS) || tryParseDays(sleepMode) != null; + } + + public static int[] tryParseDays(String sleepMode) { + if (sleepMode == null) return null; + sleepMode = sleepMode.trim(); + if (SLEEP_MODE_NIGHTS.equals(sleepMode)) return ALL_DAYS; + if (SLEEP_MODE_WEEKNIGHTS.equals(sleepMode)) return WEEKNIGHT_DAYS; + if (!sleepMode.startsWith(SLEEP_MODE_DAYS_PREFIX)) return null; + if (sleepMode.equals(SLEEP_MODE_DAYS_PREFIX)) return null; + return tryParseDayList(sleepMode.substring(SLEEP_MODE_DAYS_PREFIX.length()), ","); + } + + public static XmlV1 readXml(XmlPullParser parser) + throws XmlPullParserException, IOException { + int type; + String tag; + XmlV1 rt = new XmlV1(); + final ArrayList<ComponentName> conditionComponents = new ArrayList<ComponentName>(); + final ArrayList<Uri> conditionIds = new ArrayList<Uri>(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + tag = parser.getName(); + if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) { + if (!conditionComponents.isEmpty()) { + rt.conditionComponents = conditionComponents + .toArray(new ComponentName[conditionComponents.size()]); + rt.conditionIds = conditionIds.toArray(new Uri[conditionIds.size()]); + } + return rt; + } + if (type == XmlPullParser.START_TAG) { + if (ALLOW_TAG.equals(tag)) { + rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false); + rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false); + rt.allowReminders = safeBoolean(parser, ALLOW_ATT_REMINDERS, + DEFAULT_ALLOW_REMINDERS); + rt.allowEvents = safeBoolean(parser, ALLOW_ATT_EVENTS, + DEFAULT_ALLOW_EVENTS); + rt.allowFrom = safeInt(parser, ALLOW_ATT_FROM, SOURCE_ANYONE); + if (rt.allowFrom < SOURCE_ANYONE || rt.allowFrom > MAX_SOURCE) { + throw new IndexOutOfBoundsException("bad source in config:" + + rt.allowFrom); + } + } else if (SLEEP_TAG.equals(tag)) { + final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE); + rt.sleepMode = isValidSleepMode(mode)? mode : null; + rt.sleepNone = safeBoolean(parser, SLEEP_ATT_NONE, false); + final int startHour = safeInt(parser, SLEEP_ATT_START_HR, 0); + final int startMinute = safeInt(parser, SLEEP_ATT_START_MIN, 0); + final int endHour = safeInt(parser, SLEEP_ATT_END_HR, 0); + final int endMinute = safeInt(parser, SLEEP_ATT_END_MIN, 0); + rt.sleepStartHour = isValidHour(startHour) ? startHour : 0; + rt.sleepStartMinute = isValidMinute(startMinute) ? startMinute : 0; + rt.sleepEndHour = isValidHour(endHour) ? endHour : 0; + rt.sleepEndMinute = isValidMinute(endMinute) ? endMinute : 0; + } else if (CONDITION_TAG.equals(tag)) { + final ComponentName component = + safeComponentName(parser, CONDITION_ATT_COMPONENT); + final Uri conditionId = safeUri(parser, CONDITION_ATT_ID); + if (component != null && conditionId != null) { + conditionComponents.add(component); + conditionIds.add(conditionId); + } + } else if (EXIT_CONDITION_TAG.equals(tag)) { + rt.exitCondition = readConditionXml(parser); + if (rt.exitCondition != null) { + rt.exitConditionComponent = + safeComponentName(parser, EXIT_CONDITION_ATT_COMPONENT); + } + } + } + } + throw new IllegalStateException("Failed to reach END_DOCUMENT"); } } - // built-in next alarm conditions - public static final String NEXT_ALARM_PATH = "next_alarm"; + public interface Migration { + ZenModeConfig migrate(XmlV1 v1); + } } diff --git a/core/java/android/transition/TransitionManager.java b/core/java/android/transition/TransitionManager.java index 0b70fdb..5209f90 100644 --- a/core/java/android/transition/TransitionManager.java +++ b/core/java/android/transition/TransitionManager.java @@ -430,7 +430,6 @@ public class TransitionManager { * Ends all pending and ongoing transitions on the specified scene root. * * @param sceneRoot The root of the View hierarchy to end transitions on. - * @hide */ public static void endTransitions(final ViewGroup sceneRoot) { sPendingTransitions.remove(sceneRoot); diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 2eac549..1ee4780 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -49,7 +49,6 @@ public final class InputDevice implements Parcelable { private final String mName; private final int mVendorId; private final int mProductId; - private final String mUniqueId; private final String mDescriptor; private final InputDeviceIdentifier mIdentifier; private final boolean mIsExternal; @@ -57,6 +56,7 @@ public final class InputDevice implements Parcelable { private final int mKeyboardType; private final KeyCharacterMap mKeyCharacterMap; private final boolean mHasVibrator; + private final boolean mHasMic; private final boolean mHasButtonUnderPad; private final ArrayList<MotionRange> mMotionRanges = new ArrayList<MotionRange>(); @@ -357,8 +357,8 @@ public final class InputDevice implements Parcelable { // Called by native code. private InputDevice(int id, int generation, int controllerNumber, String name, int vendorId, - int productId, String uniqueId, String descriptor, boolean isExternal, int sources, - int keyboardType, KeyCharacterMap keyCharacterMap, boolean hasVibrator, + int productId, String descriptor, boolean isExternal, int sources, int keyboardType, + KeyCharacterMap keyCharacterMap, boolean hasVibrator, boolean hasMic, boolean hasButtonUnderPad) { mId = id; mGeneration = generation; @@ -366,13 +366,13 @@ public final class InputDevice implements Parcelable { mName = name; mVendorId = vendorId; mProductId = productId; - mUniqueId = uniqueId; mDescriptor = descriptor; mIsExternal = isExternal; mSources = sources; mKeyboardType = keyboardType; mKeyCharacterMap = keyCharacterMap; mHasVibrator = hasVibrator; + mHasMic = hasMic; mHasButtonUnderPad = hasButtonUnderPad; mIdentifier = new InputDeviceIdentifier(descriptor, vendorId, productId); } @@ -384,13 +384,13 @@ public final class InputDevice implements Parcelable { mName = in.readString(); mVendorId = in.readInt(); mProductId = in.readInt(); - mUniqueId = in.readString(); mDescriptor = in.readString(); mIsExternal = in.readInt() != 0; mSources = in.readInt(); mKeyboardType = in.readInt(); mKeyCharacterMap = KeyCharacterMap.CREATOR.createFromParcel(in); mHasVibrator = in.readInt() != 0; + mHasMic = in.readInt() != 0; mHasButtonUnderPad = in.readInt() != 0; mIdentifier = new InputDeviceIdentifier(mDescriptor, mVendorId, mProductId); @@ -509,23 +509,6 @@ public final class InputDevice implements Parcelable { } /** - * Gets the vendor's unique id for the given device, if available. - * <p> - * A vendor may assign a unique id to a device (e.g., MAC address for - * Bluetooth devices). A null value will be assigned where a unique id is - * not available. - * </p><p> - * This method is dependent on the vendor, whereas {@link #getDescriptor} - * attempts to create a unique id even when the vendor has not provided one. - * </p> - * - * @return The unique id of a given device - */ - public String getUniqueId() { - return mUniqueId; - } - - /** * Gets the input device descriptor, which is a stable identifier for an input device. * <p> * An input device descriptor uniquely identifies an input device. Its value @@ -737,6 +720,14 @@ public final class InputDevice implements Parcelable { } /** + * Reports whether the device has a built-in microphone. + * @return Whether the device has a built-in microphone. + */ + public boolean hasMic() { + return mHasMic; + } + + /** * Reports whether the device has a button under its touchpad * @return Whether the device has a button under its touchpad * @hide @@ -864,13 +855,13 @@ public final class InputDevice implements Parcelable { out.writeString(mName); out.writeInt(mVendorId); out.writeInt(mProductId); - out.writeString(mUniqueId); out.writeString(mDescriptor); out.writeInt(mIsExternal ? 1 : 0); out.writeInt(mSources); out.writeInt(mKeyboardType); mKeyCharacterMap.writeToParcel(out, flags); out.writeInt(mHasVibrator ? 1 : 0); + out.writeInt(mHasMic ? 1 : 0); out.writeInt(mHasButtonUnderPad ? 1 : 0); final int numRanges = mMotionRanges.size(); @@ -916,6 +907,8 @@ public final class InputDevice implements Parcelable { description.append(" Has Vibrator: ").append(mHasVibrator).append("\n"); + description.append(" Has mic: ").append(mHasMic).append("\n"); + description.append(" Sources: 0x").append(Integer.toHexString(mSources)).append(" ("); appendSourceDescriptionIfApplicable(description, SOURCE_KEYBOARD, "keyboard"); appendSourceDescriptionIfApplicable(description, SOURCE_DPAD, "dpad"); diff --git a/core/java/android/widget/ActionMenuPresenter.java b/core/java/android/widget/ActionMenuPresenter.java index 36bce0b..f951dc2 100644 --- a/core/java/android/widget/ActionMenuPresenter.java +++ b/core/java/android/widget/ActionMenuPresenter.java @@ -397,7 +397,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter public void updateMenuView(boolean cleared) { final ViewGroup menuViewParent = (ViewGroup) ((View) mMenuView).getParent(); if (menuViewParent != null) { - setupItemAnimations(); +// setupItemAnimations(); ActionBarTransition.beginDelayedTransition(menuViewParent); } super.updateMenuView(cleared); diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 32b99a8..8f4e8e1 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -1671,7 +1671,8 @@ public class Editor { return false; } ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); - mSelectionActionMode = mTextView.startActionMode(actionModeCallback); + mSelectionActionMode = mTextView.startActionMode( + actionModeCallback, ActionMode.TYPE_FLOATING); return mSelectionActionMode != null; } @@ -1704,7 +1705,8 @@ public class Editor { // immediately hide the newly created action bar and would be visually distracting. if (!willExtract) { ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); - mSelectionActionMode = mTextView.startActionMode(actionModeCallback); + mSelectionActionMode = mTextView.startActionMode( + actionModeCallback, ActionMode.TYPE_FLOATING); } final boolean selectionStarted = mSelectionActionMode != null || willExtract; diff --git a/core/java/android/widget/FastScroller.java b/core/java/android/widget/FastScroller.java index 21213ac..4b5407a 100644 --- a/core/java/android/widget/FastScroller.java +++ b/core/java/android/widget/FastScroller.java @@ -46,6 +46,8 @@ import android.view.ViewGroupOverlay; import android.widget.AbsListView.OnScrollListener; import android.widget.ImageView.ScaleType; +import com.android.internal.R; + /** * Helper class for AbsListView to draw and control the Fast Scroll thumb */ @@ -82,6 +84,10 @@ class FastScroller { private static final int OVERLAY_AT_THUMB = 1; private static final int OVERLAY_ABOVE_THUMB = 2; + // Positions for thumb in relation to track. + private static final int THUMB_POSITION_MIDPOINT = 0; + private static final int THUMB_POSITION_INSIDE = 1; + // Indices for mPreviewResId. private static final int PREVIEW_LEFT = 0; private static final int PREVIEW_RIGHT = 1; @@ -100,7 +106,6 @@ class FastScroller { private final ImageView mThumbImage; private final ImageView mTrackImage; private final View mPreviewImage; - /** * Preview image resource IDs for left- and right-aligned layouts. See * {@link #PREVIEW_LEFT} and {@link #PREVIEW_RIGHT}. @@ -130,6 +135,11 @@ class FastScroller { private Drawable mThumbDrawable; private Drawable mTrackDrawable; private int mTextAppearance; + private int mThumbPosition; + + // Used to convert between y-coordinate and thumb position within track. + private float mThumbOffset; + private float mThumbRange; /** Total width of decorations. */ private int mWidth; @@ -278,7 +288,6 @@ class FastScroller { } private void updateAppearance() { - final Context context = mList.getContext(); int width = 0; // Add track to overlay if it has an image. @@ -298,12 +307,9 @@ class FastScroller { // Account for minimum thumb width. mWidth = Math.max(width, mThumbMinWidth); - mPreviewImage.setMinimumWidth(mPreviewMinWidth); - mPreviewImage.setMinimumHeight(mPreviewMinHeight); - if (mTextAppearance != 0) { - mPrimaryText.setTextAppearance(context, mTextAppearance); - mSecondaryText.setTextAppearance(context, mTextAppearance); + mPrimaryText.setTextAppearance(mTextAppearance); + mSecondaryText.setTextAppearance(mTextAppearance); } if (mTextColor != null) { @@ -316,13 +322,11 @@ class FastScroller { mSecondaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); } - final int textMinSize = Math.max(0, mPreviewMinHeight); - mPrimaryText.setMinimumWidth(textMinSize); - mPrimaryText.setMinimumHeight(textMinSize); + final int padding = mPreviewPadding; mPrimaryText.setIncludeFontPadding(false); - mSecondaryText.setMinimumWidth(textMinSize); - mSecondaryText.setMinimumHeight(textMinSize); + mPrimaryText.setPadding(padding, padding, padding, padding); mSecondaryText.setIncludeFontPadding(false); + mSecondaryText.setPadding(padding, padding, padding, padding); refreshDrawablePressedState(); } @@ -330,50 +334,53 @@ class FastScroller { public void setStyle(@StyleRes int resId) { final Context context = mList.getContext(); final TypedArray ta = context.obtainStyledAttributes(null, - com.android.internal.R.styleable.FastScroll, android.R.attr.fastScrollStyle, resId); + R.styleable.FastScroll, R.attr.fastScrollStyle, resId); final int N = ta.getIndexCount(); for (int i = 0; i < N; i++) { final int index = ta.getIndex(i); switch (index) { - case com.android.internal.R.styleable.FastScroll_position: + case R.styleable.FastScroll_position: mOverlayPosition = ta.getInt(index, OVERLAY_FLOATING); break; - case com.android.internal.R.styleable.FastScroll_backgroundLeft: + case R.styleable.FastScroll_backgroundLeft: mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(index, 0); break; - case com.android.internal.R.styleable.FastScroll_backgroundRight: + case R.styleable.FastScroll_backgroundRight: mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(index, 0); break; - case com.android.internal.R.styleable.FastScroll_thumbDrawable: + case R.styleable.FastScroll_thumbDrawable: mThumbDrawable = ta.getDrawable(index); break; - case com.android.internal.R.styleable.FastScroll_trackDrawable: + case R.styleable.FastScroll_trackDrawable: mTrackDrawable = ta.getDrawable(index); break; - case com.android.internal.R.styleable.FastScroll_textAppearance: + case R.styleable.FastScroll_textAppearance: mTextAppearance = ta.getResourceId(index, 0); break; - case com.android.internal.R.styleable.FastScroll_textColor: + case R.styleable.FastScroll_textColor: mTextColor = ta.getColorStateList(index); break; - case com.android.internal.R.styleable.FastScroll_textSize: + case R.styleable.FastScroll_textSize: mTextSize = ta.getDimensionPixelSize(index, 0); break; - case com.android.internal.R.styleable.FastScroll_minWidth: + case R.styleable.FastScroll_minWidth: mPreviewMinWidth = ta.getDimensionPixelSize(index, 0); break; - case com.android.internal.R.styleable.FastScroll_minHeight: + case R.styleable.FastScroll_minHeight: mPreviewMinHeight = ta.getDimensionPixelSize(index, 0); break; - case com.android.internal.R.styleable.FastScroll_thumbMinWidth: + case R.styleable.FastScroll_thumbMinWidth: mThumbMinWidth = ta.getDimensionPixelSize(index, 0); break; - case com.android.internal.R.styleable.FastScroll_thumbMinHeight: + case R.styleable.FastScroll_thumbMinHeight: mThumbMinHeight = ta.getDimensionPixelSize(index, 0); break; - case com.android.internal.R.styleable.FastScroll_padding: + case R.styleable.FastScroll_padding: mPreviewPadding = ta.getDimensionPixelSize(index, 0); break; + case R.styleable.FastScroll_thumbPosition: + mThumbPosition = ta.getInt(index, THUMB_POSITION_MIDPOINT); + break; } } @@ -478,14 +485,16 @@ class FastScroller { final int previewResId = mPreviewResId[mLayoutFromRight ? PREVIEW_RIGHT : PREVIEW_LEFT]; mPreviewImage.setBackgroundResource(previewResId); - // Add extra padding for text. - final Drawable background = mPreviewImage.getBackground(); - if (background != null) { - final Rect padding = mTempBounds; - background.getPadding(padding); - padding.offset(mPreviewPadding, mPreviewPadding); - mPreviewImage.setPadding(padding.left, padding.top, padding.right, padding.bottom); - } + // Propagate padding to text min width/height. + final int textMinWidth = Math.max(0, mPreviewMinWidth - mPreviewImage.getPaddingLeft() + - mPreviewImage.getPaddingRight()); + mPrimaryText.setMinimumWidth(textMinWidth); + mSecondaryText.setMinimumWidth(textMinWidth); + + final int textMinHeight = Math.max(0, mPreviewMinHeight - mPreviewImage.getPaddingTop() + - mPreviewImage.getPaddingBottom()); + mPrimaryText.setMinimumHeight(textMinHeight); + mSecondaryText.setMinimumHeight(textMinHeight); // Requires re-layout. updateLayout(); @@ -560,6 +569,8 @@ class FastScroller { layoutThumb(); layoutTrack(); + updateOffsetAndRange(); + final Rect bounds = mTempBounds; measurePreview(mPrimaryText, bounds); applyLayout(mPrimaryText, bounds); @@ -758,15 +769,45 @@ class FastScroller { final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); track.measure(widthMeasureSpec, heightMeasureSpec); + final int top; + final int bottom; + if (mThumbPosition == THUMB_POSITION_INSIDE) { + top = container.top; + bottom = container.bottom; + } else { + final int thumbHalfHeight = thumb.getHeight() / 2; + top = container.top + thumbHalfHeight; + bottom = container.bottom - thumbHalfHeight; + } + final int trackWidth = track.getMeasuredWidth(); - final int thumbHalfHeight = thumb.getHeight() / 2; final int left = thumb.getLeft() + (thumb.getWidth() - trackWidth) / 2; final int right = left + trackWidth; - final int top = container.top + thumbHalfHeight; - final int bottom = container.bottom - thumbHalfHeight; track.layout(left, top, right, bottom); } + /** + * Updates the offset and range used to convert from absolute y-position to + * thumb position within the track. + */ + private void updateOffsetAndRange() { + final View trackImage = mTrackImage; + final View thumbImage = mThumbImage; + final float min; + final float max; + if (mThumbPosition == THUMB_POSITION_INSIDE) { + final float halfThumbHeight = thumbImage.getHeight() / 2f; + min = trackImage.getTop() + halfThumbHeight; + max = trackImage.getBottom() - halfThumbHeight; + } else{ + min = trackImage.getTop(); + max = trackImage.getBottom(); + } + + mThumbOffset = min; + mThumbRange = max - min; + } + private void setState(int state) { mList.removeCallbacks(mDeferHide); @@ -1145,18 +1186,8 @@ class FastScroller { * to place the thumb. */ private void setThumbPos(float position) { - final Rect container = mContainerRect; - final int top = container.top; - final int bottom = container.bottom; - - final View trackImage = mTrackImage; - final View thumbImage = mThumbImage; - final float min = trackImage.getTop(); - final float max = trackImage.getBottom(); - final float offset = min; - final float range = max - min; - final float thumbMiddle = position * range + offset; - thumbImage.setTranslationY(thumbMiddle - thumbImage.getHeight() / 2); + final float thumbMiddle = position * mThumbRange + mThumbOffset; + mThumbImage.setTranslationY(thumbMiddle - mThumbImage.getHeight() / 2f); final View previewImage = mPreviewImage; final float previewHalfHeight = previewImage.getHeight() / 2f; @@ -1175,6 +1206,9 @@ class FastScroller { } // Center the preview on the thumb, constrained to the list bounds. + final Rect container = mContainerRect; + final int top = container.top; + final int bottom = container.bottom; final float minP = top + previewHalfHeight; final float maxP = bottom - previewHalfHeight; final float previewMiddle = MathUtils.constrain(previewPos, minP, maxP); @@ -1186,19 +1220,13 @@ class FastScroller { } private float getPosFromMotionEvent(float y) { - final View trackImage = mTrackImage; - final float min = trackImage.getTop(); - final float max = trackImage.getBottom(); - final float offset = min; - final float range = max - min; - // If the list is the same height as the thumbnail or shorter, // effectively disable scrolling. - if (range <= 0) { + if (mThumbRange <= 0) { return 0f; } - return MathUtils.constrain((y - offset) / range, 0f, 1f); + return MathUtils.constrain((y - mThumbOffset) / mThumbRange, 0f, 1f); } /** diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index 1746bed..4f0e29e 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -40,6 +40,9 @@ interface IBatteryStats { ParcelFileDescriptor getStatisticsStream(); + // Return true if we see the battery as currently charging. + boolean isCharging(); + // Return the computed amount of time remaining on battery, in milliseconds. // Returns -1 if nothing could be computed. long computeBatteryTimeRemaining(); diff --git a/core/java/com/android/internal/app/ProcessStats.java b/core/java/com/android/internal/app/ProcessStats.java index 75beee9..fe79eff 100644 --- a/core/java/com/android/internal/app/ProcessStats.java +++ b/core/java/com/android/internal/app/ProcessStats.java @@ -140,6 +140,8 @@ public final class ProcessStats implements Parcelable { STATE_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT STATE_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT_UI STATE_TOP, // ActivityManager.PROCESS_STATE_TOP + STATE_IMPORTANT_FOREGROUND, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE + STATE_TOP, // ActivityManager.PROCESS_STATE_TOP_SLEEPING STATE_IMPORTANT_FOREGROUND, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND STATE_IMPORTANT_BACKGROUND, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND STATE_BACKUP, // ActivityManager.PROCESS_STATE_BACKUP diff --git a/core/java/com/android/internal/logging/MetricsConstants.java b/core/java/com/android/internal/logging/MetricsConstants.java index e5cba84..ee225a1 100644 --- a/core/java/com/android/internal/logging/MetricsConstants.java +++ b/core/java/com/android/internal/logging/MetricsConstants.java @@ -32,9 +32,16 @@ public interface MetricsConstants { public static final int ACCOUNTS_ACCOUNT_SYNC = 9; public static final int ACCOUNTS_CHOOSE_ACCOUNT_ACTIVITY = 10; public static final int ACCOUNTS_MANAGE_ACCOUNTS = 11; + public static final int ACTION_WIFI_ADD_NETWORK = 134; + public static final int ACTION_WIFI_CONNECT = 135; + public static final int ACTION_WIFI_FORCE_SCAN = 136; + public static final int ACTION_WIFI_FORGET = 137; + public static final int ACTION_WIFI_OFF = 138; + public static final int ACTION_WIFI_ON = 139; public static final int APN = 12; public static final int APN_EDITOR = 13; public static final int APPLICATION = 16; + public static final int APPLICATIONS_ADVANCED = 130; public static final int APPLICATIONS_APP_LAUNCH = 17; public static final int APPLICATIONS_APP_PERMISSION = 18; public static final int APPLICATIONS_APP_STORAGE = 19; @@ -85,8 +92,13 @@ public interface MetricsConstants { public static final int INPUTMETHOD_USER_DICTIONARY_ADD_WORD = 62; public static final int LOCATION = 63; public static final int LOCATION_MODE = 64; + public static final int LOCATION_SCANNING = 131; public static final int MAIN_SETTINGS = 1; public static final int MANAGE_APPLICATIONS = 65; + public static final int MANAGE_APPLICATIONS_ALL = 132; + public static final int MANAGE_APPLICATIONS_NOTIFICATIONS = 133; + public static final int MANAGE_DOMAIN_URLS = 143; + public static final int MANAGE_PERMISSIONS = 140; public static final int MASTER_CLEAR = 66; public static final int MASTER_CLEAR_CONFIRM = 67; public static final int NET_DATA_USAGE_METERED = 68; @@ -94,10 +106,15 @@ public interface MetricsConstants { public static final int NFC_PAYMENT = 70; public static final int NOTIFICATION = 71; public static final int NOTIFICATION_APP_NOTIFICATION = 72; + public static final int NOTIFICATION_ITEM = 128; + public static final int NOTIFICATION_ITEM_ACTION = 129; public static final int NOTIFICATION_OTHER_SOUND = 73; + public static final int NOTIFICATION_PANEL = 127; public static final int NOTIFICATION_REDACTION = 74; public static final int NOTIFICATION_STATION = 75; public static final int NOTIFICATION_ZEN_MODE = 76; + public static final int NOTIFICATION_ZEN_MODE_AUTOMATION = 142; + public static final int NOTIFICATION_ZEN_MODE_PRIORITY = 141; public static final int OWNER_INFO = 77; public static final int PRINT_JOB_SETTINGS = 78; public static final int PRINT_SERVICE_SETTINGS = 79; @@ -132,7 +149,6 @@ public interface MetricsConstants { public static final int TRUST_AGENT = 91; public static final int TTS_ENGINE_SETTINGS = 93; public static final int TTS_TEXT_TO_SPEECH = 94; - public static final int TYPE_UNKNOWN = 0; public static final int USAGE_ACCESS = 95; public static final int USER = 96; public static final int USERS_APP_RESTRICTIONS = 97; diff --git a/core/java/com/android/internal/logging/MetricsLogger.java b/core/java/com/android/internal/logging/MetricsLogger.java index 1038543..f38229a 100644 --- a/core/java/com/android/internal/logging/MetricsLogger.java +++ b/core/java/com/android/internal/logging/MetricsLogger.java @@ -26,23 +26,9 @@ import android.os.Build; */ public class MetricsLogger implements MetricsConstants { // These constants are temporary, they should migrate to MetricsConstants. - public static final int APPLICATIONS_ADVANCED = 132; - public static final int LOCATION_SCANNING = 133; - public static final int MANAGE_APPLICATIONS_ALL = 134; - public static final int MANAGE_APPLICATIONS_NOTIFICATIONS = 135; + // next value is 145; - public static final int ACTION_WIFI_ADD_NETWORK = 136; - public static final int ACTION_WIFI_CONNECT = 137; - public static final int ACTION_WIFI_FORCE_SCAN = 138; - public static final int ACTION_WIFI_FORGET = 139; - public static final int ACTION_WIFI_OFF = 140; - public static final int ACTION_WIFI_ON = 141; - - public static final int MANAGE_PERMISSIONS = 142; - public static final int NOTIFICATION_ZEN_MODE_PRIORITY = 143; - public static final int NOTIFICATION_ZEN_MODE_AUTOMATION = 144; - - public static final int MANAGE_DOMAIN_URLS = 143; + public static final int NOTIFICATION_ZEN_MODE_SCHEDULE_RULE = 144; public static void visible(Context context, int category) throws IllegalArgumentException { if (Build.IS_DEBUGGABLE && category == VIEW_UNKNOWN) { diff --git a/core/java/com/android/internal/midi/EventScheduler.java b/core/java/com/android/internal/midi/EventScheduler.java index 7b9a48c..506902f 100644 --- a/core/java/com/android/internal/midi/EventScheduler.java +++ b/core/java/com/android/internal/midi/EventScheduler.java @@ -16,6 +16,7 @@ package com.android.internal.midi; +import java.util.Iterator; import java.util.SortedMap; import java.util.TreeMap; @@ -28,7 +29,7 @@ public class EventScheduler { private static final long NANOS_PER_MILLI = 1000000; private final Object mLock = new Object(); - private SortedMap<Long, FastEventQueue> mEventBuffer; + volatile private SortedMap<Long, FastEventQueue> mEventBuffer; private FastEventQueue mEventPool = null; private int mMaxPoolSize = 200; private boolean mClosed; @@ -68,6 +69,7 @@ public class EventScheduler { mEventsRemoved++; SchedulableEvent event = mFirst; mFirst = event.mNext; + event.mNext = null; return event; } @@ -87,7 +89,7 @@ public class EventScheduler { */ public static class SchedulableEvent { private long mTimestamp; - private SchedulableEvent mNext = null; + volatile private SchedulableEvent mNext = null; /** * @param timestamp @@ -235,6 +237,11 @@ public class EventScheduler { return event; } + protected void flush() { + // Replace our event buffer with a fresh empty one + mEventBuffer = new TreeMap<Long, FastEventQueue>(); + } + public void close() { synchronized (mLock) { mClosed = true; diff --git a/core/java/com/android/internal/midi/MidiConstants.java b/core/java/com/android/internal/midi/MidiConstants.java index 87552e4..f78f75a 100644 --- a/core/java/com/android/internal/midi/MidiConstants.java +++ b/core/java/com/android/internal/midi/MidiConstants.java @@ -19,7 +19,7 @@ package com.android.internal.midi; /** * MIDI related constants and static methods. */ -public class MidiConstants { +public final class MidiConstants { public static final byte STATUS_COMMAND_MASK = (byte) 0xF0; public static final byte STATUS_CHANNEL_MASK = (byte) 0x0F; @@ -85,4 +85,16 @@ public class MidiConstants { } return (goodBytes == 0); } + + // Returns true if this command can be used for running status + public static boolean allowRunningStatus(int command) { + // only Channel Voice and Channel Mode commands can use running status + return (command >= STATUS_NOTE_OFF && command < STATUS_SYSTEM_EXCLUSIVE); + } + + // Returns true if this command cancels running status + public static boolean cancelsRunningStatus(int command) { + // System Common messages cancel running status + return (command >= STATUS_SYSTEM_EXCLUSIVE && command <= STATUS_END_SYSEX); + } } diff --git a/core/java/com/android/internal/midi/MidiDispatcher.java b/core/java/com/android/internal/midi/MidiDispatcher.java index 377bc68..70e699a 100644 --- a/core/java/com/android/internal/midi/MidiDispatcher.java +++ b/core/java/com/android/internal/midi/MidiDispatcher.java @@ -83,4 +83,11 @@ public final class MidiDispatcher extends MidiReceiver { } } } + + @Override + public void flush() throws IOException { + for (MidiReceiver receiver : mReceivers) { + receiver.flush(); + } + } } diff --git a/core/java/com/android/internal/midi/MidiEventScheduler.java b/core/java/com/android/internal/midi/MidiEventScheduler.java index 42d70f6..4dc5838 100644 --- a/core/java/com/android/internal/midi/MidiEventScheduler.java +++ b/core/java/com/android/internal/midi/MidiEventScheduler.java @@ -28,16 +28,9 @@ public class MidiEventScheduler extends EventScheduler { // Maintain a pool of scheduled events to reduce memory allocation. // This pool increases performance by about 14%. private final static int POOL_EVENT_SIZE = 16; - - private final MidiReceiver[] mReceivers; + private MidiReceiver mReceiver = new SchedulingReceiver(); private class SchedulingReceiver extends MidiReceiver { - private final int mPortNumber; - - public SchedulingReceiver(int portNumber) { - mPortNumber = portNumber; - } - /** * Store these bytes in the EventScheduler to be delivered at the specified * time. @@ -47,14 +40,17 @@ public class MidiEventScheduler extends EventScheduler { throws IOException { MidiEvent event = createScheduledEvent(msg, offset, count, timestamp); if (event != null) { - event.portNumber = mPortNumber; add(event); } } + + @Override + public void flush() { + MidiEventScheduler.this.flush(); + } } public static class MidiEvent extends SchedulableEvent { - public int portNumber; public int count = 0; public byte[] data; @@ -80,17 +76,6 @@ public class MidiEventScheduler extends EventScheduler { } } - public MidiEventScheduler() { - this(0); - } - - public MidiEventScheduler(int portCount) { - mReceivers = new MidiReceiver[portCount]; - for (int i = 0; i < portCount; i++) { - mReceivers[i] = new SchedulingReceiver(i); - } - } - /** * Create an event that contains the message. */ @@ -132,15 +117,7 @@ public class MidiEventScheduler extends EventScheduler { * @return the MidiReceiver */ public MidiReceiver getReceiver() { - return mReceivers[0]; - } - - /** - * This MidiReceiver will write date to the scheduling buffer. - * @return the MidiReceiver - */ - public MidiReceiver getReceiver(int portNumber) { - return mReceivers[portNumber]; + return mReceiver; } } diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 793d0d3..c5c0ba6 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -22,6 +22,7 @@ import android.bluetooth.BluetoothActivityEnergyInfo; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.content.Context; +import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkStats; import android.net.wifi.WifiActivityEnergyInfo; @@ -127,6 +128,7 @@ public final class BatteryStatsImpl extends BatteryStats { static final int MSG_UPDATE_WAKELOCKS = 1; static final int MSG_REPORT_POWER_CHANGE = 2; + static final int MSG_REPORT_CHARGING = 3; static final long DELAY_UPDATE_WAKELOCKS = 5*1000; private final KernelWakelockReader mKernelWakelockReader = new KernelWakelockReader(); @@ -135,6 +137,7 @@ public final class BatteryStatsImpl extends BatteryStats { public interface BatteryCallback { public void batteryNeedsCpuUpdate(); public void batteryPowerChanged(boolean onBattery); + public void batterySendBroadcast(Intent intent); } final class MyHandler extends Handler { @@ -156,6 +159,18 @@ public final class BatteryStatsImpl extends BatteryStats { cb.batteryPowerChanged(msg.arg1 != 0); } break; + case MSG_REPORT_CHARGING: + if (cb != null) { + final String action; + synchronized (BatteryStatsImpl.this) { + action = mCharging ? BatteryManager.ACTION_CHARGING + : BatteryManager.ACTION_DISCHARGING; + } + Intent intent = new Intent(action); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + cb.batterySendBroadcast(intent); + } + break; } } } @@ -393,6 +408,12 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mOnBattery; boolean mOnBatteryInternal; + /** + * External reporting of whether the device is actually charging. + */ + boolean mCharging = true; + int mLastChargingStateLevel; + /* * These keep track of battery levels (1-100) at the last plug event and the last unplug event. */ @@ -7243,6 +7264,10 @@ public final class BatteryStatsImpl extends BatteryStats { return mOnBattery; } + public boolean isCharging() { + return mCharging; + } + public boolean isScreenOn() { return mScreenState == Display.STATE_ON; } @@ -7802,6 +7827,20 @@ public final class BatteryStatsImpl extends BatteryStats { } } + boolean setChargingLocked(boolean charging) { + if (mCharging != charging) { + mCharging = charging; + if (charging) { + mHistoryCur.states |= HistoryItem.STATE_CHARGING_FLAG; + } else { + mHistoryCur.states &= ~HistoryItem.STATE_CHARGING_FLAG; + } + mHandler.sendEmptyMessage(MSG_REPORT_CHARGING); + return true; + } + return false; + } + void setOnBatteryLocked(final long mSecRealtime, final long mSecUptime, final boolean onBattery, final int oldStatus, final int level) { boolean doWrite = false; @@ -7861,6 +7900,10 @@ public final class BatteryStatsImpl extends BatteryStats { reset = true; mDischargeStepTracker.init(); } + if (mCharging) { + setChargingLocked(false); + } + mLastChargingStateLevel = level; mOnBattery = mOnBatteryInternal = true; mLastDischargeStepLevel = level; mMinDischargeStepLevel = level; @@ -7890,6 +7933,7 @@ public final class BatteryStatsImpl extends BatteryStats { mDischargeAmountScreenOff = 0; updateTimeBasesLocked(true, !screenOn, uptime, realtime); } else { + mLastChargingStateLevel = level; mOnBattery = mOnBatteryInternal = false; pullPendingStateUpdatesLocked(); mHistoryCur.batteryLevel = (byte)level; @@ -7982,10 +8026,13 @@ public final class BatteryStatsImpl extends BatteryStats { mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG; } } + // Always start out assuming charging, that will be updated later. + mHistoryCur.states |= HistoryItem.STATE_CHARGING_FLAG; mHistoryCur.batteryStatus = (byte)status; mHistoryCur.batteryLevel = (byte)level; mMaxChargeStepLevel = mMinDischargeStepLevel = mLastChargeStepLevel = mLastDischargeStepLevel = level; + mLastChargingStateLevel = level; } else if (mCurrentBatteryLevel != level || mOnBattery != onBattery) { recordDailyStatsIfNeededLocked(level >= 100 && onBattery); } @@ -8046,13 +8093,11 @@ public final class BatteryStatsImpl extends BatteryStats { mHistoryCur.batteryVoltage = (char)volt; changed = true; } - if (changed) { - addHistoryRecordLocked(elapsedRealtime, uptime); - } long modeBits = (((long)mInitStepMode) << STEP_LEVEL_INITIAL_MODE_SHIFT) | (((long)mModStepMode) << STEP_LEVEL_MODIFIED_MODE_SHIFT) | (((long)(level&0xff)) << STEP_LEVEL_LEVEL_SHIFT); if (onBattery) { + changed |= setChargingLocked(false); if (mLastDischargeStepLevel != level && mMinDischargeStepLevel > level) { mDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level, modeBits, elapsedRealtime); @@ -8064,6 +8109,28 @@ public final class BatteryStatsImpl extends BatteryStats { mModStepMode = 0; } } else { + if (level >= 90) { + // If the battery level is at least 90%, always consider the device to be + // charging even if it happens to go down a level. + changed |= setChargingLocked(true); + mLastChargeStepLevel = level; + } if (!mCharging) { + if (mLastChargeStepLevel < level) { + // We have not reporting that we are charging, but the level has now + // gone up, so consider the state to be charging. + changed |= setChargingLocked(true); + mLastChargeStepLevel = level; + } + } else { + if (mLastChargeStepLevel > level) { + // We had reported that the device was charging, but here we are with + // power connected and the level going down. Looks like the current + // power supplied isn't enough, so consider the device to now be + // discharging. + changed |= setChargingLocked(false); + mLastChargeStepLevel = level; + } + } if (mLastChargeStepLevel != level && mMaxChargeStepLevel < level) { mChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel, modeBits, elapsedRealtime); @@ -8075,6 +8142,9 @@ public final class BatteryStatsImpl extends BatteryStats { mModStepMode = 0; } } + if (changed) { + addHistoryRecordLocked(elapsedRealtime, uptime); + } } if (!onBattery && status == BatteryManager.BATTERY_STATUS_FULL) { // We don't record history while we are plugged in and fully charged. diff --git a/core/java/com/android/internal/os/InstallerConnection.java b/core/java/com/android/internal/os/InstallerConnection.java index a4cdf19..671bf24 100644 --- a/core/java/com/android/internal/os/InstallerConnection.java +++ b/core/java/com/android/internal/os/InstallerConnection.java @@ -90,12 +90,15 @@ public class InstallerConnection { } } - public int dexopt(String apkPath, int uid, boolean isPublic, String instructionSet) { - return dexopt(apkPath, uid, isPublic, "*", instructionSet, false, false, null); + public int dexopt(String apkPath, int uid, boolean isPublic, + String instructionSet, int dexoptNeeded) { + return dexopt(apkPath, uid, isPublic, "*", instructionSet, dexoptNeeded, + false, false, null); } public int dexopt(String apkPath, int uid, boolean isPublic, String pkgName, - String instructionSet, boolean vmSafeMode, boolean debuggable, String outputPath) { + String instructionSet, int dexoptNeeded, boolean vmSafeMode, + boolean debuggable, String outputPath) { StringBuilder builder = new StringBuilder("dexopt"); builder.append(' '); builder.append(apkPath); @@ -106,6 +109,8 @@ public class InstallerConnection { builder.append(pkgName); builder.append(' '); builder.append(instructionSet); + builder.append(' '); + builder.append(dexoptNeeded); builder.append(vmSafeMode ? " 1" : " 0"); builder.append(debuggable ? " 1" : " 0"); builder.append(' '); @@ -113,25 +118,6 @@ public class InstallerConnection { return execute(builder.toString()); } - public int patchoat(String apkPath, int uid, boolean isPublic, String instructionSet) { - return patchoat(apkPath, uid, isPublic, "*", instructionSet); - } - - public int patchoat(String apkPath, int uid, boolean isPublic, String pkgName, - String instructionSet) { - StringBuilder builder = new StringBuilder("patchoat"); - builder.append(' '); - builder.append(apkPath); - builder.append(' '); - builder.append(uid); - builder.append(isPublic ? " 1" : " 0"); - builder.append(' '); - builder.append(pkgName); - builder.append(' '); - builder.append(instructionSet); - return execute(builder.toString()); - } - private boolean connect() { if (mSocket != null) { return true; diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 70f7b72..3ad4f1c 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -467,12 +467,11 @@ public class ZygoteInit { try { for (String classPathElement : classPathElements) { - final byte dexopt = DexFile.isDexOptNeededInternal(classPathElement, "*", instructionSet, - false /* defer */); - if (dexopt == DexFile.DEXOPT_NEEDED) { - installer.dexopt(classPathElement, Process.SYSTEM_UID, false, instructionSet); - } else if (dexopt == DexFile.PATCHOAT_NEEDED) { - installer.patchoat(classPathElement, Process.SYSTEM_UID, false, instructionSet); + final int dexoptNeeded = DexFile.getDexOptNeeded( + classPathElement, "*", instructionSet, false /* defer */); + if (dexoptNeeded != DexFile.NO_DEXOPT_NEEDED) { + installer.dexopt(classPathElement, Process.SYSTEM_UID, false, + instructionSet, dexoptNeeded); } } } catch (IOException ioe) { diff --git a/core/java/com/android/internal/widget/FloatingToolbar.java b/core/java/com/android/internal/widget/FloatingToolbar.java index be9945d..2219ad1 100644 --- a/core/java/com/android/internal/widget/FloatingToolbar.java +++ b/core/java/com/android/internal/widget/FloatingToolbar.java @@ -24,7 +24,9 @@ import android.content.Context; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.Region; import android.graphics.drawable.ColorDrawable; +import android.util.Size; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; @@ -32,19 +34,28 @@ import android.view.MenuItem; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.view.Window; +import android.view.WindowManager; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.Transformation; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.ListView; import android.widget.PopupWindow; - -import com.android.internal.R; -import com.android.internal.util.Preconditions; +import android.widget.TextView; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import com.android.internal.R; +import com.android.internal.util.Preconditions; + /** * A floating toolbar for showing contextual menu items. * This view shows as many menu item buttons as can fit in the horizontal toolbar and the @@ -53,6 +64,9 @@ import java.util.List; */ public final class FloatingToolbar { + // This class is responsible for the public API of the floating toolbar. + // It delegates rendering operations to the FloatingToolbarPopup. + private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER = new MenuItem.OnMenuItemClickListener() { @Override @@ -63,17 +77,6 @@ public final class FloatingToolbar { private final Context mContext; private final FloatingToolbarPopup mPopup; - private final ViewGroup mMenuItemButtonsContainer; - private final View.OnClickListener mMenuItemButtonOnClickListener = - new View.OnClickListener() { - @Override - public void onClick(View v) { - if (v.getTag() instanceof MenuItem) { - mMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag()); - mPopup.dismiss(); - } - } - }; private final Rect mContentRect = new Rect(); private final Point mCoordinates = new Point(); @@ -81,17 +84,17 @@ public final class FloatingToolbar { private Menu mMenu; private List<CharSequence> mShowingTitles = new ArrayList<CharSequence>(); private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; - private View mOpenOverflowButton; private int mSuggestedWidth; + private boolean mWidthChanged = true; + private int mOverflowDirection; /** * Initializes a floating toolbar. */ public FloatingToolbar(Context context, Window window) { mContext = Preconditions.checkNotNull(context); - mPopup = new FloatingToolbarPopup(Preconditions.checkNotNull(window.getDecorView())); - mMenuItemButtonsContainer = createMenuButtonsContainer(context); + mPopup = new FloatingToolbarPopup(window.getDecorView()); } /** @@ -137,6 +140,10 @@ public final class FloatingToolbar { * toolbar. */ public FloatingToolbar setSuggestedWidth(int suggestedWidth) { + // Check if there's been a substantial width spec change. + int difference = Math.abs(suggestedWidth - mSuggestedWidth); + mWidthChanged = difference > (mSuggestedWidth * 0.2); + mSuggestedWidth = suggestedWidth; return this; } @@ -146,16 +153,18 @@ public final class FloatingToolbar { */ public FloatingToolbar show() { List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); - if (hasContentChanged(menuItems) || hasWidthChanged()) { + if (!isCurrentlyShowing(menuItems) || mWidthChanged) { mPopup.dismiss(); - layoutMenuItemButtons(menuItems); + mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth); mShowingTitles = getMenuItemTitles(menuItems); } refreshCoordinates(); + mPopup.setOverflowDirection(mOverflowDirection); mPopup.updateCoordinates(mCoordinates.x, mCoordinates.y); if (!mPopup.isShowing()) { mPopup.show(mCoordinates.x, mCoordinates.y); } + mWidthChanged = false; return this; } @@ -189,45 +198,26 @@ public final class FloatingToolbar { * Refreshes {@link #mCoordinates} with values based on {@link #mContentRect}. */ private void refreshCoordinates() { - int popupWidth = mPopup.getWidth(); - int popupHeight = mPopup.getHeight(); - if (!mPopup.isShowing()) { - // Popup isn't yet shown, get estimated size from the menu item buttons container. - mMenuItemButtonsContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); - popupWidth = mMenuItemButtonsContainer.getMeasuredWidth(); - popupHeight = mMenuItemButtonsContainer.getMeasuredHeight(); - } - int x = mContentRect.centerX() - popupWidth / 2; + int x = mContentRect.centerX() - mPopup.getWidth() / 2; int y; - if (shouldDisplayAtTopOfContent()) { - y = mContentRect.top - popupHeight; + if (mContentRect.top > mPopup.getHeight()) { + y = mContentRect.top - mPopup.getHeight(); + mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_UP; + } else if (mContentRect.top > getEstimatedToolbarHeight(mContext)) { + y = mContentRect.top - getEstimatedToolbarHeight(mContext); + mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN; } else { y = mContentRect.bottom; + mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN; } mCoordinates.set(x, y); } /** - * Returns true if this floating toolbar's menu items have been reordered or changed. - */ - private boolean hasContentChanged(List<MenuItem> menuItems) { - return !mShowingTitles.equals(getMenuItemTitles(menuItems)); - } - - /** - * Returns true if there is a significant change in width of the toolbar. - */ - private boolean hasWidthChanged() { - int actualWidth = mPopup.getWidth(); - int difference = Math.abs(actualWidth - mSuggestedWidth); - return difference > (actualWidth * 0.2); - } - - /** - * Returns true if the preferred positioning of the toolbar is above the content rect. + * Returns true if this floating toolbar is currently showing the specified menu items. */ - private boolean shouldDisplayAtTopOfContent() { - return mContentRect.top - getMinimumOverflowHeight(mContext) > 0; + private boolean isCurrentlyShowing(List<MenuItem> menuItems) { + return mShowingTitles.equals(getMenuItemTitles(menuItems)); } /** @@ -258,178 +248,162 @@ public final class FloatingToolbar { return titles; } - private void layoutMenuItemButtons(List<MenuItem> menuItems) { - final int toolbarWidth = getAdjustedToolbarWidth(mContext, mSuggestedWidth) - // Reserve space for the "open overflow" button. - - getEstimatedOpenOverflowButtonWidth(mContext); - - int availableWidth = toolbarWidth; - LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems); - - mMenuItemButtonsContainer.removeAllViews(); - - boolean isFirstItem = true; - while (!remainingMenuItems.isEmpty()) { - final MenuItem menuItem = remainingMenuItems.peek(); - Button menuItemButton = createMenuItemButton(mContext, menuItem); - - // Adding additional left padding for the first button to even out button spacing. - if (isFirstItem) { - menuItemButton.setPadding( - 2 * menuItemButton.getPaddingLeft(), - menuItemButton.getPaddingTop(), - menuItemButton.getPaddingRight(), - menuItemButton.getPaddingBottom()); - isFirstItem = false; - } - - // Adding additional right padding for the last button to even out button spacing. - if (remainingMenuItems.size() == 1) { - menuItemButton.setPadding( - menuItemButton.getPaddingLeft(), - menuItemButton.getPaddingTop(), - 2 * menuItemButton.getPaddingRight(), - menuItemButton.getPaddingBottom()); - } - - menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); - int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth); - if (menuItemButtonWidth <= availableWidth) { - menuItemButton.setTag(menuItem); - menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener); - mMenuItemButtonsContainer.addView(menuItemButton); - menuItemButton.getLayoutParams().width = menuItemButtonWidth; - availableWidth -= menuItemButtonWidth; - remainingMenuItems.pop(); - } else { - // The "open overflow" button launches the vertical overflow from the - // floating toolbar. - createOpenOverflowButtonIfNotExists(); - mMenuItemButtonsContainer.addView(mOpenOverflowButton); - break; - } - } - mPopup.setContentView(mMenuItemButtonsContainer); - } - - /** - * Creates and returns the button that opens the vertical overflow. - */ - private void createOpenOverflowButtonIfNotExists() { - mOpenOverflowButton = (ImageButton) LayoutInflater.from(mContext) - .inflate(R.layout.floating_popup_open_overflow_button, null); - mOpenOverflowButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - // Open the overflow. - } - }); - } - - /** - * Creates and returns a floating toolbar menu buttons container. - */ - private static ViewGroup createMenuButtonsContainer(Context context) { - return (ViewGroup) LayoutInflater.from(context) - .inflate(R.layout.floating_popup_container, null); - } /** - * Creates and returns a menu button for the specified menu item. + * A popup window used by the floating toolbar. + * + * This class is responsible for the rendering/animation of the floating toolbar. + * It can hold one of 2 panels (i.e. main panel and overflow panel) at a time. + * It delegates specific panel functionality to the appropriate panel. */ - private static Button createMenuItemButton(Context context, MenuItem menuItem) { - Button menuItemButton = (Button) LayoutInflater.from(context) - .inflate(R.layout.floating_popup_menu_button, null); - menuItemButton.setText(menuItem.getTitle()); - menuItemButton.setContentDescription(menuItem.getTitle()); - return menuItemButton; - } - - private static int getMinimumOverflowHeight(Context context) { - return context.getResources(). - getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height); - } + private static final class FloatingToolbarPopup { - private static int getEstimatedOpenOverflowButtonWidth(Context context) { - return context.getResources() - .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width); - } + public static final int OVERFLOW_DIRECTION_UP = 0; + public static final int OVERFLOW_DIRECTION_DOWN = 1; - private static int getAdjustedToolbarWidth(Context context, int width) { - if (width <= 0 || width > getScreenWidth(context)) { - width = context.getResources() - .getDimensionPixelSize(R.dimen.floating_toolbar_default_width); - } - return width; - } + private final View mParent; + private final PopupWindow mPopupWindow; + private final ViewGroup mContentContainer; + private final int mPadding; - /** - * Returns the device's screen width. - */ - public static int getScreenWidth(Context context) { - return context.getResources().getDisplayMetrics().widthPixels; - } + private final Animation.AnimationListener mOnOverflowOpened = + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} - /** - * Returns the device's screen height. - */ - public static int getScreenHeight(Context context) { - return context.getResources().getDisplayMetrics().heightPixels; - } + @Override + public void onAnimationEnd(Animation animation) { + // This animation should never be run if the overflow panel has not been + // initialized. + Preconditions.checkNotNull(mOverflowPanel); + mContentContainer.removeAllViews(); + mContentContainer.addView(mOverflowPanel.getView()); + mOverflowPanel.fadeIn(true); + setContentAreaAsTouchableSurface(); + } + @Override + public void onAnimationRepeat(Animation animation) {} + }; + private final Animation.AnimationListener mOnOverflowClosed = + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} - /** - * A popup window used by the floating toolbar. - */ - private static final class FloatingToolbarPopup { + @Override + public void onAnimationEnd(Animation animation) { + // This animation should never be run if the main panel has not been + // initialized. + Preconditions.checkNotNull(mMainPanel); + mContentContainer.removeAllViews(); + mContentContainer.addView(mMainPanel.getView()); + mMainPanel.fadeIn(true); + setContentAreaAsTouchableSurface(); + } - private final View mParent; - private final PopupWindow mPopupWindow; - private final ViewGroup mContentContainer; - private final Animator.AnimatorListener mOnDismissEnd = - new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { - mPopupWindow.dismiss(); - mDismissAnimating = false; + public void onAnimationRepeat(Animation animation) { } }; private final AnimatorSet mGrowFadeInFromBottomAnimation; private final AnimatorSet mShrinkFadeOutFromBottomAnimation; + private final Runnable mOpenOverflow = new Runnable() { + @Override + public void run() { + openOverflow(); + } + }; + private final Runnable mCloseOverflow = new Runnable() { + @Override + public void run() { + closeOverflow(); + } + }; + + private final Region mTouchableRegion = new Region(); + private boolean mDismissAnimating; + private FloatingToolbarOverflowPanel mOverflowPanel; + private FloatingToolbarMainPanel mMainPanel; + private int mOverflowDirection; + /** - * Initializes a new floating bar popup. + * Initializes a new floating toolbar popup. * - * @param parent A parent view to get the {@link View#getWindowToken()} token from. + * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token + * from. */ public FloatingToolbarPopup(View parent) { mParent = Preconditions.checkNotNull(parent); mContentContainer = createContentContainer(parent.getContext()); mPopupWindow = createPopupWindow(mContentContainer); mGrowFadeInFromBottomAnimation = createGrowFadeInFromBottom(mContentContainer); - mShrinkFadeOutFromBottomAnimation = - createShrinkFadeOutFromBottomAnimation(mContentContainer, mOnDismissEnd); + mShrinkFadeOutFromBottomAnimation = createShrinkFadeOutFromBottomAnimation( + mContentContainer, + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mPopupWindow.dismiss(); + mDismissAnimating = false; + setMainPanelAsContent(); + } + }); + // Make the touchable area of this popup be the area specified by mTouchableRegion. + mPopupWindow.getContentView() + .getRootView() + .getViewTreeObserver() + .addOnComputeInternalInsetsListener( + new ViewTreeObserver.OnComputeInternalInsetsListener() { + public void onComputeInternalInsets( + ViewTreeObserver.InternalInsetsInfo info) { + info.contentInsets.setEmpty(); + info.visibleInsets.setEmpty(); + info.touchableRegion.set(mTouchableRegion); + info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo + .TOUCHABLE_INSETS_REGION); + } + }); + mPadding = parent.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_margin); + } + + /** + * Lays out buttons for the specified menu items. + */ + public void layoutMenuItems(List<MenuItem> menuItems, + MenuItem.OnMenuItemClickListener menuItemClickListener, int suggestedWidth) { + mContentContainer.removeAllViews(); + if (mMainPanel == null) { + mMainPanel = new FloatingToolbarMainPanel(mParent.getContext(), mOpenOverflow); + } + List<MenuItem> overflowMenuItems = + mMainPanel.layoutMenuItems(menuItems, suggestedWidth); + mMainPanel.setOnMenuItemClickListener(menuItemClickListener); + if (!overflowMenuItems.isEmpty()) { + if (mOverflowPanel == null) { + mOverflowPanel = + new FloatingToolbarOverflowPanel(mParent.getContext(), mCloseOverflow); + } + mOverflowPanel.setMenuItems(overflowMenuItems); + mOverflowPanel.setOnMenuItemClickListener(menuItemClickListener); + } + updatePopupSize(); } /** * Shows this popup at the specified coordinates. * The specified coordinates may be adjusted to make sure the popup is entirely on-screen. - * If this popup is already showing, this will be a no-op. */ public void show(int x, int y) { if (isShowing()) { - updateCoordinates(x, y); return; } - mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, 0, 0); - positionOnScreen(x, y); + stopDismissAnimation(); + preparePopupContent(); + mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, x, y); growFadeInFromBottom(); - - mDismissAnimating = false; } /** @@ -440,12 +414,9 @@ public final class FloatingToolbar { return; } - if (mDismissAnimating) { - // This window is already dismissing. Don't restart the animation. - return; - } mDismissAnimating = true; shrinkFadeOutFromBottom(); + setZeroTouchableSurface(); } /** @@ -460,32 +431,40 @@ public final class FloatingToolbar { * The specified coordinates may be adjusted to make sure the popup is entirely on-screen. */ public void updateCoordinates(int x, int y) { - if (isShowing()) { - positionOnScreen(x, y); + if (mDismissAnimating) { + // Already being dismissed. Ignore. + return; } + + preparePopupContent(); + mPopupWindow.update(x, y, getWidth(), getHeight()); } /** - * Sets the content of this popup. + * Sets the direction in which the overflow will open. i.e. up or down. + * + * @param overflowDirection Either {@link #OVERFLOW_DIRECTION_UP} + * or {@link #OVERFLOW_DIRECTION_DOWN}. */ - public void setContentView(View view) { - Preconditions.checkNotNull(view); - mContentContainer.removeAllViews(); - mContentContainer.addView(view); + public void setOverflowDirection(int overflowDirection) { + mOverflowDirection = overflowDirection; + if (mOverflowPanel != null) { + mOverflowPanel.setOverflowDirection(mOverflowDirection); + } } /** * Returns the width of this popup. */ public int getWidth() { - return mContentContainer.getWidth(); + return mPopupWindow.getWidth(); } /** * Returns the height of this popup. */ public int getHeight() { - return mContentContainer.getHeight(); + return mPopupWindow.getHeight(); } /** @@ -495,24 +474,10 @@ public final class FloatingToolbar { return mContentContainer.getContext(); } - private void positionOnScreen(int x, int y) { - if (getWidth() == 0) { - // content size is yet to be measured. - mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); - } - x = clamp(x, 0, getScreenWidth(getContext()) - getWidth()); - y = clamp(y, 0, getScreenHeight(getContext()) - getHeight()); - - // Position the view w.r.t. the window. - mContentContainer.setX(x); - mContentContainer.setY(y); - } - /** * Performs the "grow and fade in from the bottom" animation on the floating popup. */ private void growFadeInFromBottom() { - setPivot(); mGrowFadeInFromBottomAnimation.start(); } @@ -520,77 +485,643 @@ public final class FloatingToolbar { * Performs the "shrink and fade out from bottom" animation on the floating popup. */ private void shrinkFadeOutFromBottom() { - setPivot(); mShrinkFadeOutFromBottomAnimation.start(); } + private void stopDismissAnimation() { + mDismissAnimating = false; + mShrinkFadeOutFromBottomAnimation.cancel(); + } + + /** + * Opens the floating toolbar overflow. + * This method should not be called if menu items have not been laid out with + * {@link #layoutMenuItems(List, MenuItem.OnMenuItemClickListener, int)}. + * + * @throws IllegalStateException if called when menu items have not been laid out. + */ + private void openOverflow() { + Preconditions.checkNotNull(mMainPanel); + Preconditions.checkNotNull(mOverflowPanel); + + mMainPanel.fadeOut(true); + Size overflowPanelSize = mOverflowPanel.measure(); + final int targetWidth = getOverflowWidth(mParent.getContext()); + final int targetHeight = overflowPanelSize.getHeight(); + final boolean morphUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP); + final int startWidth = mContentContainer.getWidth(); + final int startHeight = mContentContainer.getHeight(); + final float startY = mContentContainer.getY(); + final float right = mContentContainer.getX() + mContentContainer.getWidth(); + Animation widthAnimation = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); + int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth)); + params.width = startWidth + deltaWidth; + mContentContainer.setLayoutParams(params); + mContentContainer.setX(right - mContentContainer.getWidth()); + } + }; + Animation heightAnimation = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); + int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight)); + params.height = startHeight + deltaHeight; + mContentContainer.setLayoutParams(params); + if (morphUpwards) { + float y = startY - (mContentContainer.getHeight() - startHeight); + mContentContainer.setY(y); + } + } + }; + widthAnimation.setDuration(240); + heightAnimation.setDuration(180); + heightAnimation.setStartOffset(60); + AnimationSet animation = new AnimationSet(true); + animation.setAnimationListener(mOnOverflowOpened); + animation.addAnimation(widthAnimation); + animation.addAnimation(heightAnimation); + mContentContainer.startAnimation(animation); + } + + /** + * Opens the floating toolbar overflow. + * This method should not be called if menu items have not been laid out with + * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}. + * + * @throws IllegalStateException + */ + private void closeOverflow() { + Preconditions.checkNotNull(mMainPanel); + Preconditions.checkNotNull(mOverflowPanel); + + mOverflowPanel.fadeOut(true); + Size mainPanelSize = mMainPanel.measure(); + final int targetWidth = mainPanelSize.getWidth(); + final int targetHeight = mainPanelSize.getHeight(); + final int startWidth = mContentContainer.getWidth(); + final int startHeight = mContentContainer.getHeight(); + final float right = mContentContainer.getX() + mContentContainer.getWidth(); + final float bottom = mContentContainer.getY() + mContentContainer.getHeight(); + final boolean morphedUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP); + Animation widthAnimation = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); + int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth)); + params.width = startWidth + deltaWidth; + mContentContainer.setLayoutParams(params); + mContentContainer.setX(right - mContentContainer.getWidth()); + } + }; + Animation heightAnimation = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); + int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight)); + params.height = startHeight + deltaHeight; + mContentContainer.setLayoutParams(params); + if (morphedUpwards) { + mContentContainer.setY(bottom - mContentContainer.getHeight()); + } + } + }; + widthAnimation.setDuration(150); + widthAnimation.setStartOffset(150); + heightAnimation.setDuration(210); + AnimationSet animation = new AnimationSet(true); + animation.setAnimationListener(mOnOverflowClosed); + animation.addAnimation(widthAnimation); + animation.addAnimation(heightAnimation); + mContentContainer.startAnimation(animation); + } + + /** + * Prepares the content container for show and update calls. + */ + private void preparePopupContent() { + // Do not call this method if main view panel has not been initialized. + Preconditions.checkNotNull(mMainPanel); + + // If we're yet to show the popup, set the container visibility to zero. + // The "show" animation will make this visible. + if (!mPopupWindow.isShowing()) { + mContentContainer.setAlpha(0); + } + + // Make sure panels are visible. + mMainPanel.fadeIn(false); + if (mOverflowPanel != null) { + mOverflowPanel.fadeIn(false); + } + + // Make sure a panel is set as the content. + if (mContentContainer.getChildCount() == 0) { + mContentContainer.addView(mMainPanel.getView()); + } + + // Make sure the main panel is at the correct position. + if (mContentContainer.getChildAt(0) == mMainPanel.getView()) { + mContentContainer.setX(mPadding); + float y = mPadding; + if (mOverflowDirection == OVERFLOW_DIRECTION_UP) { + y = getHeight() - getEstimatedToolbarHeight(mParent.getContext()) - mPadding; + } + mContentContainer.setY(y); + } + + setContentAreaAsTouchableSurface(); + } + + /** + * Sets the current content to be the main view panel. + */ + private void setMainPanelAsContent() { + mContentContainer.removeAllViews(); + Size mainPanelSize = mMainPanel.measure(); + ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); + params.width = mainPanelSize.getWidth(); + params.height = mainPanelSize.getHeight(); + mContentContainer.setLayoutParams(params); + mContentContainer.addView(mMainPanel.getView()); + } + + private void updatePopupSize() { + int width = 0; + int height = 0; + if (mMainPanel != null) { + Size mainPanelSize = mMainPanel.measure(); + width = mainPanelSize.getWidth(); + height = mainPanelSize.getHeight(); + } + if (mOverflowPanel != null) { + Size overflowPanelSize = mOverflowPanel.measure(); + width = Math.max(width, overflowPanelSize.getWidth()); + height = Math.max(height, overflowPanelSize.getHeight()); + } + mPopupWindow.setWidth(width + mPadding * 2); + mPopupWindow.setHeight(height + mPadding * 2); + } + + /** + * Sets the touchable region of this popup to be zero. This means that all touch events on + * this popup will go through to the surface behind it. + */ + private void setZeroTouchableSurface() { + mTouchableRegion.setEmpty(); + } + + /** + * Sets the touchable region of this popup to be the area occupied by its content. + */ + private void setContentAreaAsTouchableSurface() { + if (!mPopupWindow.isShowing()) { + mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + } + int width = mContentContainer.getMeasuredWidth(); + int height = mContentContainer.getMeasuredHeight(); + mTouchableRegion.set( + (int) mContentContainer.getX(), + (int) mContentContainer.getY(), + (int) mContentContainer.getX() + width, + (int) mContentContainer.getY() + height); + } + } + + /** + * A widget that holds the primary menu items in the floating toolbar. + */ + private static final class FloatingToolbarMainPanel { + + private final Context mContext; + private final ViewGroup mContentView; + private final View.OnClickListener mMenuItemButtonOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v.getTag() instanceof MenuItem) { + if (mOnMenuItemClickListener != null) { + mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag()); + } + } + } + }; + private final ViewFader viewFader; + private final Runnable mOpenOverflow; + + private View mOpenOverflowButton; + private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener; + + /** + * Initializes a floating toolbar popup main view panel. + * + * @param context + * @param openOverflow The code that opens the toolbar popup overflow. + */ + public FloatingToolbarMainPanel(Context context, Runnable openOverflow) { + mContext = Preconditions.checkNotNull(context); + mContentView = new LinearLayout(context); + viewFader = new ViewFader(mContentView); + mOpenOverflow = Preconditions.checkNotNull(openOverflow); + } + /** - * Sets the popup content container's pivot. + * Fits as many menu items in the main panel and returns a list of the menu items that + * were not fit in. + * + * @return The menu items that are not included in this main panel. */ - private void setPivot() { - mContentContainer.setPivotX(mContentContainer.getMeasuredWidth() / 2); - mContentContainer.setPivotY(mContentContainer.getMeasuredHeight()); + public List<MenuItem> layoutMenuItems(List<MenuItem> menuItems, int suggestedWidth) { + final int toolbarWidth = getAdjustedToolbarWidth(mContext, suggestedWidth) + // Reserve space for the "open overflow" button. + - getEstimatedOpenOverflowButtonWidth(mContext); + + int availableWidth = toolbarWidth; + final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems); + + mContentView.removeAllViews(); + + boolean isFirstItem = true; + while (!remainingMenuItems.isEmpty()) { + final MenuItem menuItem = remainingMenuItems.peek(); + Button menuItemButton = createMenuItemButton(mContext, menuItem); + + // Adding additional left padding for the first button to even out button spacing. + if (isFirstItem) { + menuItemButton.setPadding( + 2 * menuItemButton.getPaddingLeft(), + menuItemButton.getPaddingTop(), + menuItemButton.getPaddingRight(), + menuItemButton.getPaddingBottom()); + isFirstItem = false; + } + + // Adding additional right padding for the last button to even out button spacing. + if (remainingMenuItems.size() == 1) { + menuItemButton.setPadding( + menuItemButton.getPaddingLeft(), + menuItemButton.getPaddingTop(), + 2 * menuItemButton.getPaddingRight(), + menuItemButton.getPaddingBottom()); + } + + menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth); + if (menuItemButtonWidth <= availableWidth) { + menuItemButton.setTag(menuItem); + menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener); + mContentView.addView(menuItemButton); + ViewGroup.LayoutParams params = menuItemButton.getLayoutParams(); + params.width = menuItemButtonWidth; + menuItemButton.setLayoutParams(params); + availableWidth -= menuItemButtonWidth; + remainingMenuItems.pop(); + } else { + if (mOpenOverflowButton == null) { + mOpenOverflowButton = (ImageButton) LayoutInflater.from(mContext) + .inflate(R.layout.floating_popup_open_overflow_button, null); + mOpenOverflowButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mOpenOverflowButton != null) { + mOpenOverflow.run(); + } + } + }); + } + mContentView.addView(mOpenOverflowButton); + break; + } + } + return remainingMenuItems; + } + + public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) { + mOnMenuItemClickListener = listener; } - private static ViewGroup createContentContainer(Context context) { - return (ViewGroup) LayoutInflater.from(context) - .inflate(R.layout.floating_popup_container, null); + public View getView() { + return mContentView; } - private static PopupWindow createPopupWindow(View content) { - ViewGroup popupContentHolder = new LinearLayout(content.getContext()); - PopupWindow popupWindow = new PopupWindow(popupContentHolder); - popupWindow.setAnimationStyle(0); - popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - popupWindow.setWidth(getScreenWidth(content.getContext())); - popupWindow.setHeight(getScreenHeight(content.getContext())); - content.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - popupContentHolder.addView(content); - return popupWindow; + public void fadeIn(boolean animate) { + viewFader.fadeIn(animate); + } + + public void fadeOut(boolean animate) { + viewFader.fadeOut(animate); } /** - * Creates a "grow and fade in from the bottom" animation for the specified view. + * Returns how big this panel's view should be. + * This method should only be called when the view has not been attached to a parent + * otherwise it will throw an illegal state. + */ + public Size measure() throws IllegalStateException { + Preconditions.checkState(mContentView.getParent() == null); + mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight()); + } + } + + + /** + * A widget that holds the overflow items in the floating toolbar. + */ + private static final class FloatingToolbarOverflowPanel { + + private final LinearLayout mContentView; + private final ViewGroup mBackButtonContainer; + private final View mBackButton; + private final ListView mListView; + private final ViewFader mViewFader; + private final Runnable mCloseOverflow; + + private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener; + + /** + * Initializes a floating toolbar popup overflow view panel. * - * @param view The view to animate + * @param context + * @param closeOverflow The code that closes the toolbar popup's overflow. + */ + public FloatingToolbarOverflowPanel(Context context, Runnable closeOverflow) { + mCloseOverflow = Preconditions.checkNotNull(closeOverflow); + + mContentView = new LinearLayout(context); + mContentView.setOrientation(LinearLayout.VERTICAL); + mViewFader = new ViewFader(mContentView); + + mBackButton = LayoutInflater.from(context) + .inflate(R.layout.floating_popup_close_overflow_button, null); + mBackButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mCloseOverflow.run(); + } + }); + mBackButtonContainer = new LinearLayout(context); + mBackButtonContainer.addView(mBackButton); + + mListView = createOverflowListView(context); + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(position); + if (mOnMenuItemClickListener != null) { + mOnMenuItemClickListener.onMenuItemClick(menuItem); + } + } + }); + + mContentView.addView(mListView); + mContentView.addView(mBackButtonContainer); + } + + /** + * Sets the menu items to be displayed in the overflow. */ - private static AnimatorSet createGrowFadeInFromBottom(View view) { - AnimatorSet growFadeInFromBottomAnimation = new AnimatorSet(); - growFadeInFromBottomAnimation.playTogether( - ObjectAnimator.ofFloat(view, View.SCALE_X, 0.5f, 1).setDuration(125), - ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.5f, 1).setDuration(125), - ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(75)); - return growFadeInFromBottomAnimation; + public void setMenuItems(List<MenuItem> menuItems) { + ArrayAdapter overflowListViewAdapter = (ArrayAdapter) mListView.getAdapter(); + overflowListViewAdapter.clear(); + overflowListViewAdapter.addAll(menuItems); + setListViewHeight(); + } + + public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) { + mOnMenuItemClickListener = listener; } /** - * Creates a "shrink and fade out from bottom" animation for the specified view. + * Notifies the overflow of the current direction in which the overflow will be opened. * - * @param view The view to animate - * @param listener The animation listener + * @param overflowDirection {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_UP} + * or {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_DOWN}. */ - private static AnimatorSet createShrinkFadeOutFromBottomAnimation( - View view, Animator.AnimatorListener listener) { - AnimatorSet shrinkFadeOutFromBottomAnimation = new AnimatorSet(); - shrinkFadeOutFromBottomAnimation.playTogether( - ObjectAnimator.ofFloat(view, View.SCALE_Y, 1, 0.5f).setDuration(125), - ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(75)); - shrinkFadeOutFromBottomAnimation.setStartDelay(150); - shrinkFadeOutFromBottomAnimation.addListener(listener); - return shrinkFadeOutFromBottomAnimation; + public void setOverflowDirection(int overflowDirection) { + mContentView.removeView(mBackButtonContainer); + int index = (overflowDirection == FloatingToolbarPopup.OVERFLOW_DIRECTION_UP)? 1 : 0; + mContentView.addView(mBackButtonContainer, index); } /** - * Returns value, restricted to the range min->max (inclusive). - * If maximum is less than minimum, the result is undefined. + * Returns the content view of the overflow. + */ + public View getView() { + return mContentView; + } + + public void fadeIn(boolean animate) { + mViewFader.fadeIn(animate); + } + + public void fadeOut(boolean animate) { + mViewFader.fadeOut(animate); + } + + /** + * Returns how big this panel's view should be. + * This method should only be called when the view has not been attached to a parent. * - * @param value The value to clamp. - * @param minimum The minimum value in the range. - * @param maximum The maximum value in the range. Must not be less than minimum. + * @throws IllegalStateException */ - private static int clamp(int value, int minimum, int maximum) { - return Math.max(minimum, Math.min(value, maximum)); + public Size measure() { + Preconditions.checkState(mContentView.getParent() == null); + mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight()); + } + + private void setListViewHeight() { + int itemHeight = getEstimatedToolbarHeight(mContentView.getContext()); + int height = mListView.getAdapter().getCount() * itemHeight; + int maxHeight = mContentView.getContext().getResources(). + getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height); + ViewGroup.LayoutParams params = mListView.getLayoutParams(); + params.height = Math.min(height, maxHeight); + mListView.setLayoutParams(params); } + + private static ListView createOverflowListView(final Context context) { + final ListView overflowListView = new ListView(context); + overflowListView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + overflowListView.setDivider(null); + overflowListView.setDividerHeight(0); + final ArrayAdapter overflowListViewAdapter = + new ArrayAdapter<MenuItem>(context, 0) { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView menuButton; + if (convertView != null) { + menuButton = (TextView) convertView; + } else { + menuButton = createOverflowMenuItemButton(context); + } + MenuItem menuItem = getItem(position); + menuButton.setText(menuItem.getTitle()); + menuButton.setContentDescription(menuItem.getTitle()); + return menuButton; + } + }; + overflowListView.setAdapter(overflowListViewAdapter); + return overflowListView; + } + } + + + /** + * A helper for fading in or out a view. + */ + private static final class ViewFader { + + private static final int FADE_OUT_DURATION = 250; + private static final int FADE_IN_DURATION = 150; + + private final View mView; + private final ObjectAnimator mFadeOutAnimation; + private final ObjectAnimator mFadeInAnimation; + + private ViewFader(View view) { + mView = Preconditions.checkNotNull(view); + mFadeOutAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0) + .setDuration(FADE_OUT_DURATION); + mFadeInAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1) + .setDuration(FADE_IN_DURATION); + } + + public void fadeIn(boolean animate) { + if (animate) { + mFadeInAnimation.start(); + } else { + mView.setAlpha(1); + } + } + + public void fadeOut(boolean animate) { + if (animate) { + mFadeOutAnimation.start(); + } else { + mView.setAlpha(0); + } + } + } + + + /** + * Creates and returns a menu button for the specified menu item. + */ + private static Button createMenuItemButton(Context context, MenuItem menuItem) { + Button menuItemButton = (Button) LayoutInflater.from(context) + .inflate(R.layout.floating_popup_menu_button, null); + menuItemButton.setText(menuItem.getTitle()); + menuItemButton.setContentDescription(menuItem.getTitle()); + return menuItemButton; + } + + /** + * Creates and returns a styled floating toolbar overflow list view item. + */ + private static TextView createOverflowMenuItemButton(Context context) { + return (TextView) LayoutInflater.from(context) + .inflate(R.layout.floating_popup_overflow_list_item, null); + } + + private static ViewGroup createContentContainer(Context context) { + return (ViewGroup) LayoutInflater.from(context) + .inflate(R.layout.floating_popup_container, null); + } + + private static PopupWindow createPopupWindow(View content) { + ViewGroup popupContentHolder = new LinearLayout(content.getContext()); + PopupWindow popupWindow = new PopupWindow(popupContentHolder); + popupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); + popupWindow.setAnimationStyle(0); + popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + content.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + popupContentHolder.addView(content); + return popupWindow; + } + + /** + * Creates a "grow and fade in from the bottom" animation for the specified view. + * + * @param view The view to animate + */ + private static AnimatorSet createGrowFadeInFromBottom(View view) { + AnimatorSet growFadeInFromBottomAnimation = new AnimatorSet(); + growFadeInFromBottomAnimation.playTogether( + ObjectAnimator.ofFloat(view, View.SCALE_X, 0.5f, 1).setDuration(125), + ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.5f, 1).setDuration(125), + ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(75)); + growFadeInFromBottomAnimation.setStartDelay(50); + return growFadeInFromBottomAnimation; + } + + /** + * Creates a "shrink and fade out from bottom" animation for the specified view. + * + * @param view The view to animate + * @param listener The animation listener + */ + private static AnimatorSet createShrinkFadeOutFromBottomAnimation( + View view, Animator.AnimatorListener listener) { + AnimatorSet shrinkFadeOutFromBottomAnimation = new AnimatorSet(); + shrinkFadeOutFromBottomAnimation.playTogether( + ObjectAnimator.ofFloat(view, View.SCALE_Y, 1, 0.5f).setDuration(125), + ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(75)); + shrinkFadeOutFromBottomAnimation.setStartDelay(150); + shrinkFadeOutFromBottomAnimation.addListener(listener); + return shrinkFadeOutFromBottomAnimation; + } + + private static int getOverflowWidth(Context context) { + return context.getResources() + .getDimensionPixelSize(R.dimen.floating_toolbar_overflow_width); + } + + private static int getEstimatedToolbarHeight(Context context) { + return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height); + } + + private static int getEstimatedOpenOverflowButtonWidth(Context context) { + return context.getResources() + .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width); + } + + private static int getAdjustedToolbarWidth(Context context, int width) { + if (width <= 0 || width > getScreenWidth(context)) { + width = context.getResources() + .getDimensionPixelSize(R.dimen.floating_toolbar_default_width); + } + return width; + } + + /** + * Returns the device's screen width. + */ + private static int getScreenWidth(Context context) { + return context.getResources().getDisplayMetrics().widthPixels; + } + + /** + * Returns the device's screen height. + */ + private static int getScreenHeight(Context context) { + return context.getResources().getDisplayMetrics().heightPixels; + } + + /** + * Returns value, restricted to the range min->max (inclusive). + * If maximum is less than minimum, the result is undefined. + * + * @param value The value to clamp. + * @param minimum The minimum value in the range. + * @param maximum The maximum value in the range. Must not be less than minimum. + */ + private static int clamp(int value, int minimum, int maximum) { + return Math.max(minimum, Math.min(value, maximum)); } } diff --git a/core/jni/android_view_InputDevice.cpp b/core/jni/android_view_InputDevice.cpp index fb91c8f..9cf6a9d 100644 --- a/core/jni/android_view_InputDevice.cpp +++ b/core/jni/android_view_InputDevice.cpp @@ -48,11 +48,6 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi return NULL; } - ScopedLocalRef<jstring> uniqueIdObj(env, env->NewStringUTF(deviceInfo.getIdentifier().uniqueId)); - if (!uniqueIdObj.get()) { - return NULL; - } - ScopedLocalRef<jobject> kcmObj(env, android_view_KeyCharacterMap_create(env, deviceInfo.getId(), deviceInfo.getKeyCharacterMap())); @@ -62,13 +57,16 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi const InputDeviceIdentifier& ident = deviceInfo.getIdentifier(); + // Not sure why, but JNI is complaining when I pass this through directly. + jboolean hasMic = deviceInfo.hasMic() ? JNI_TRUE : JNI_FALSE; + ScopedLocalRef<jobject> inputDeviceObj(env, env->NewObject(gInputDeviceClassInfo.clazz, gInputDeviceClassInfo.ctor, deviceInfo.getId(), deviceInfo.getGeneration(), deviceInfo.getControllerNumber(), nameObj.get(), static_cast<int32_t>(ident.vendor), static_cast<int32_t>(ident.product), - uniqueIdObj.get(), descriptorObj.get(), deviceInfo.isExternal(), - deviceInfo.getSources(), deviceInfo.getKeyboardType(), kcmObj.get(), - deviceInfo.hasVibrator(), deviceInfo.hasButtonUnderPad())); + descriptorObj.get(), deviceInfo.isExternal(), deviceInfo.getSources(), + deviceInfo.getKeyboardType(), kcmObj.get(), deviceInfo.hasVibrator(), + hasMic, deviceInfo.hasButtonUnderPad())); const Vector<InputDeviceInfo::MotionRange>& ranges = deviceInfo.getMotionRanges(); for (size_t i = 0; i < ranges.size(); i++) { @@ -90,7 +88,7 @@ int register_android_view_InputDevice(JNIEnv* env) gInputDeviceClassInfo.clazz = MakeGlobalRefOrDie(env, gInputDeviceClassInfo.clazz); gInputDeviceClassInfo.ctor = GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "<init>", - "(IIILjava/lang/String;IILjava/lang/String;Ljava/lang/String;ZIILandroid/view/KeyCharacterMap;ZZ)V"); + "(IIILjava/lang/String;IILjava/lang/String;ZIILandroid/view/KeyCharacterMap;ZZZ)V"); gInputDeviceClassInfo.addMotionRange = GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "addMotionRange", "(IIFFFFF)V"); diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index f427f2b..f2f7be2 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -56,6 +56,8 @@ <protected-broadcast android:name="android.intent.action.ACTION_POWER_CONNECTED" /> <protected-broadcast android:name="android.intent.action.ACTION_POWER_DISCONNECTED" /> <protected-broadcast android:name="android.intent.action.ACTION_SHUTDOWN" /> + <protected-broadcast android:name="android.intent.action.CHARGING" /> + <protected-broadcast android:name="android.intent.action.DISCHARGING" /> <protected-broadcast android:name="android.intent.action.DEVICE_STORAGE_LOW" /> <protected-broadcast android:name="android.intent.action.DEVICE_STORAGE_OK" /> <protected-broadcast android:name="android.intent.action.DEVICE_STORAGE_FULL" /> diff --git a/core/res/res/drawable/fastscroll_label_left_material.xml b/core/res/res/drawable/fastscroll_label_left_material.xml index 430d1b0..c825f73 100644 --- a/core/res/res/drawable/fastscroll_label_left_material.xml +++ b/core/res/res/drawable/fastscroll_label_left_material.xml @@ -14,14 +14,18 @@ limitations under the License. --> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <corners - android:topLeftRadius="44dp" - android:topRightRadius="44dp" - android:bottomRightRadius="44dp" /> - <padding - android:paddingLeft="22dp" - android:paddingRight="22dp" /> - <solid android:color="?attr/colorControlActivated" /> -</shape> +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:insetLeft="16dp"> + <shape + android:shape="rectangle" + android:tint="?attr/colorControlActivated"> + <corners + android:topLeftRadius="44dp" + android:topRightRadius="44dp" + android:bottomRightRadius="44dp" /> + <padding + android:left="22dp" + android:right="22dp" /> + <solid android:color="@color/white" /> + </shape> +</inset> diff --git a/core/res/res/drawable/fastscroll_label_right_material.xml b/core/res/res/drawable/fastscroll_label_right_material.xml index 6e61397..94f5fde 100644 --- a/core/res/res/drawable/fastscroll_label_right_material.xml +++ b/core/res/res/drawable/fastscroll_label_right_material.xml @@ -14,14 +14,18 @@ limitations under the License. --> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <corners - android:topLeftRadius="44dp" - android:topRightRadius="44dp" - android:bottomLeftRadius="44dp" /> - <padding - android:paddingLeft="22dp" - android:paddingRight="22dp" /> - <solid android:color="?attr/colorControlActivated" /> -</shape> +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:insetRight="16dp"> + <shape + android:shape="rectangle" + android:tint="?attr/colorControlActivated"> + <corners + android:topLeftRadius="44dp" + android:topRightRadius="44dp" + android:bottomLeftRadius="44dp" /> + <padding + android:left="22dp" + android:right="22dp" /> + <solid android:color="@color/white" /> + </shape> +</inset> diff --git a/core/res/res/layout/floating_popup_close_overflow_button.xml b/core/res/res/layout/floating_popup_close_overflow_button.xml new file mode 100644 index 0000000..a1d2811 --- /dev/null +++ b/core/res/res/layout/floating_popup_close_overflow_button.xml @@ -0,0 +1,24 @@ +<!-- +/* Copyright 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. +*/ +--> +<ImageButton xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/floating_toolbar_menu_button_minimum_width" + android:layout_height="match_parent" + android:minWidth="@dimen/floating_toolbar_menu_button_minimum_width" + android:minHeight="@dimen/floating_toolbar_height" + android:src="?android:attr/actionModeCloseDrawable" + android:contentDescription="@string/floating_toolbar_close_overflow_description" + android:background="?attr/selectableItemBackgroundBorderless" /> diff --git a/core/res/res/layout/floating_popup_open_overflow_button.xml b/core/res/res/layout/floating_popup_open_overflow_button.xml index 4c1176c..dca5384 100644 --- a/core/res/res/layout/floating_popup_open_overflow_button.xml +++ b/core/res/res/layout/floating_popup_open_overflow_button.xml @@ -21,5 +21,5 @@ android:minWidth="@dimen/floating_toolbar_menu_button_minimum_width" android:minHeight="@dimen/floating_toolbar_height" android:src="@drawable/ic_menu_moreoverflow_material" - android:contentDescription="@string/action_menu_overflow_description" + android:contentDescription="@string/floating_toolbar_open_overflow_description" android:background="?attr/selectableItemBackgroundBorderless" /> diff --git a/core/res/res/layout/floating_popup_overflow_list_item b/core/res/res/layout/floating_popup_overflow_list_item new file mode 100644 index 0000000..9294f3b --- /dev/null +++ b/core/res/res/layout/floating_popup_overflow_list_item @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* Copyright 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. +*/ +--> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:gravity="center_vertical" + android:minWidth="@dimen/floating_toolbar_menu_button_side_padding" + android:minHeight="@dimen/floating_toolbar_height" + android:paddingLeft="@dimen/floating_toolbar_menu_button_side_padding" + android:paddingRight="@dimen/floating_toolbar_menu_button_side_padding" + android:paddingTop="0dp" + android:paddingBottom="0dp" + android:singleLine="true" + android:ellipsize="end" + android:fontFamily="sans-serif" + android:textSize="@dimen/floating_toolbar_text_size" + android:textAllCaps="true" /> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index eb37619..aefe79d 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -3414,9 +3414,9 @@ <attr name="position"> <!-- Floating at the top of the content. --> <enum name="floating" value="0" /> - <!-- Pinned alongside the thumb. --> + <!-- Pinned to the thumb, vertically centered with the middle of the thumb. --> <enum name="atThumb" value="1" /> - <!-- Pinned above the thumb. --> + <!-- Pinned to the thumb, vertically centered with the top edge of the thumb. --> <enum name="aboveThumb" value="2" /> </attr> <attr name="textAppearance" /> @@ -3428,6 +3428,16 @@ <attr name="minHeight" /> <!-- Padding for the section header preview. --> <attr name="padding" /> + <!-- Position of thumb in relation to the track. --> + <attr name="thumbPosition"> + <!-- The thumb's midpoint is anchored to the track. At its + extremes, the thumb will extend half-way outside the + track. --> + <enum name="midpoint" value="0" /> + <!-- The thumb is entirely inside the track. At its extremes, + the thumb will be contained entirely within the track. --> + <enum name="inside" value="1" /> + </attr> </declare-styleable> <declare-styleable name="FrameLayout"> <!-- Determines whether to measure all children or just those in diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index e879244..1b9d133 100755 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2044,16 +2044,9 @@ <!-- Enabled built-in zen mode condition providers --> <string-array translatable="false" name="config_system_condition_providers"> <item>countdown</item> - <item>downtime</item> - <item>next_alarm</item> + <item>schedule</item> </string-array> - <!-- Show the next-alarm as a zen exit condition if it occurs in the next n hours. --> - <integer name="config_next_alarm_condition_lookahead_threshold_hrs">12</integer> - - <!-- Show downtime as a zen exit condition if it starts in the next n hours. --> - <integer name="config_downtime_condition_lookahead_threshold_hrs">4</integer> - <!-- Flags enabling default window features. See Window.java --> <bool name="config_defaultWindowFeatureOptionsPanel">true</bool> <bool name="config_defaultWindowFeatureContextMenu">true</bool> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 7d08e7f..2654a25 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -379,11 +379,11 @@ <dimen name="text_handle_min_size">40dp</dimen> <!-- Lighting and shadow properties --> - <dimen name="light_y">-200dp</dimen> - <dimen name="light_z">800dp</dimen> - <dimen name="light_radius">600dp</dimen> - <item type="dimen" format="float" name="ambient_shadow_alpha">0.075</item> - <item type="dimen" format="float" name="spot_shadow_alpha">0.15</item> + <dimen name="light_y">0dp</dimen> + <dimen name="light_z">600dp</dimen> + <dimen name="light_radius">800dp</dimen> + <item type="dimen" format="float" name="ambient_shadow_alpha">0.039</item> + <item type="dimen" format="float" name="spot_shadow_alpha">0.19</item> <!-- Floating toolbar dimensions --> <dimen name="floating_toolbar_height">48dp</dimen> @@ -392,4 +392,6 @@ <dimen name="floating_toolbar_menu_button_minimum_width">48dp</dimen> <dimen name="floating_toolbar_default_width">250dp</dimen> <dimen name="floating_toolbar_minimum_overflow_height">192dp</dimen> + <dimen name="floating_toolbar_overflow_width">130dp</dimen> + <dimen name="floating_toolbar_margin">2dp</dimen> </resources> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 24d17a4..7349d23 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2659,4 +2659,5 @@ <public type="attr" name="breakStrategy" /> <public type="attr" name="supportsAssistGesture" /> + <public type="attr" name="thumbPosition" /> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 7dc3ff7..4285ea1 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5170,12 +5170,6 @@ <!-- [CHAR_LIMIT=NONE] Battery saver: Feature description --> <string name="battery_saver_description">To help improve battery life, battery saver reduces your device’s performance and limits vibration, location services, and most background data. Email, messaging, and other apps that rely on syncing may not update unless you open them.\n\nBattery saver turns off automatically when your device is charging.</string> - <!-- [CHAR_LIMIT=NONE] Zen mode: Condition summary for built-in downtime condition, if active --> - <string name="downtime_condition_summary">Until your downtime ends at <xliff:g id="formattedTime" example="10:00 PM">%1$s</xliff:g></string> - - <!-- [CHAR_LIMIT=NONE] Zen mode: Condition line one for built-in downtime condition, if active --> - <string name="downtime_condition_line_one">Until your downtime ends</string> - <!-- Zen mode condition - summary: time duration in minutes. [CHAR LIMIT=NONE] --> <plurals name="zen_mode_duration_minutes_summary"> <item quantity="one">For one minute (until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g>)</item> @@ -5206,14 +5200,23 @@ <!-- Zen mode condition: no exit criteria. [CHAR LIMIT=NONE] --> <string name="zen_mode_forever">Until you turn this off</string> + <!-- Zen mode active automatic rule name separator. [CHAR LIMIT=NONE] --> + <string name="zen_mode_rule_name_combination"><xliff:g id="first" example="Weeknights">%1$s</xliff:g> / <xliff:g id="rest" example="Meetings">%2$s</xliff:g></string> + <!-- Content description for the Toolbar icon used to collapse an expanded action mode. [CHAR LIMIT=NONE] --> <string name="toolbar_collapse_description">Collapse</string> - <!-- Zen mode condition - summary: until next alarm. [CHAR LIMIT=NONE] --> - <string name="zen_mode_next_alarm_summary">Until next alarm at <xliff:g id="formattedTime" example="7:30 AM">%1$s</xliff:g></string> + <!-- Zen mode - feature name. [CHAR LIMIT=40] --> + <string name="zen_mode_feature_name">Block interruptions</string> - <!-- Zen mode condition - line one: until next alarm. [CHAR LIMIT=NONE] --> - <string name="zen_mode_next_alarm_line_one">Until next alarm</string> + <!-- Zen mode - downtime legacy feature name. [CHAR LIMIT=40] --> + <string name="zen_mode_downtime_feature_name">Downtime</string> + + <!-- Zen mode - name of default automatic schedule for weeknights. [CHAR LIMIT=40] --> + <string name="zen_mode_default_weeknights_name">Weeknights</string> + + <!-- Zen mode - name of default automatic schedule for weekends. [CHAR LIMIT=40] --> + <string name="zen_mode_default_weekends_name">Weekends</string> <!-- Indication that the current volume and other effects (vibration) are being suppressed by a third party, such as a notification listener. [CHAR LIMIT=30] --> <string name="muted_by">Muted by <xliff:g id="third_party">%1$s</xliff:g></string> @@ -5237,4 +5240,10 @@ <!-- Model name for USB MIDI Peripheral port --> <string name="usb_midi_peripheral_model_name">USB Peripheral Port</string> + <!-- Floating toolbar strings --> + <!-- Content description for the button that opens the floating toolbar overflow. [CHAR LIMIT=NONE] --> + <string name="floating_toolbar_open_overflow_description">More options</string> + <!-- Content description for the button that closes the floating toolbar overflow. [CHAR LIMIT=NONE] --> + <string name="floating_toolbar_close_overflow_description">Close overflow</string> + </resources> diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml index 9cf7884..7cb8144 100644 --- a/core/res/res/values/styles_material.xml +++ b/core/res/res/values/styles_material.xml @@ -943,9 +943,10 @@ please see styles_device_defaults.xml. <item name="thumbMinWidth">0dp</item> <item name="thumbMinHeight">0dp</item> <item name="textSize">45sp</item> - <item name="minWidth">88dp</item> + <item name="minWidth">104dp</item> <item name="minHeight">88dp</item> <item name="padding">0dp</item> + <item name="thumbPosition">inside</item> </style> <style name="Widget.Material.PreferenceFrameLayout"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b4ba316..524a8c3 100755 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2037,19 +2037,18 @@ <java-symbol type="dimen" name="timepicker_text_size_normal" /> <java-symbol type="dimen" name="timepicker_text_size_inner" /> <java-symbol type="string" name="battery_saver_description" /> - <java-symbol type="string" name="downtime_condition_summary" /> - <java-symbol type="string" name="downtime_condition_line_one" /> <java-symbol type="string" name="zen_mode_forever" /> + <java-symbol type="string" name="zen_mode_rule_name_combination" /> <java-symbol type="plurals" name="zen_mode_duration_minutes" /> <java-symbol type="plurals" name="zen_mode_duration_hours" /> <java-symbol type="plurals" name="zen_mode_duration_minutes_summary" /> <java-symbol type="plurals" name="zen_mode_duration_hours_summary" /> <java-symbol type="string" name="zen_mode_until" /> - <java-symbol type="string" name="zen_mode_next_alarm_summary" /> - <java-symbol type="string" name="zen_mode_next_alarm_line_one" /> + <java-symbol type="string" name="zen_mode_feature_name" /> + <java-symbol type="string" name="zen_mode_downtime_feature_name" /> + <java-symbol type="string" name="zen_mode_default_weeknights_name" /> + <java-symbol type="string" name="zen_mode_default_weekends_name" /> <java-symbol type="array" name="config_system_condition_providers" /> - <java-symbol type="integer" name="config_next_alarm_condition_lookahead_threshold_hrs" /> - <java-symbol type="integer" name="config_downtime_condition_lookahead_threshold_hrs" /> <java-symbol type="string" name="muted_by" /> <java-symbol type="string" name="select_day" /> @@ -2214,12 +2213,16 @@ <java-symbol type="layout" name="floating_popup_container" /> <java-symbol type="layout" name="floating_popup_menu_button" /> <java-symbol type="layout" name="floating_popup_open_overflow_button" /> + <java-symbol type="layout" name="floating_popup_close_overflow_button" /> + <java-symbol type="layout" name="floating_popup_overflow_list_item" /> <java-symbol type="dimen" name="floating_toolbar_height" /> <java-symbol type="dimen" name="floating_toolbar_menu_button_side_padding" /> <java-symbol type="dimen" name="floating_toolbar_text_size" /> <java-symbol type="dimen" name="floating_toolbar_menu_button_minimum_width" /> <java-symbol type="dimen" name="floating_toolbar_default_width" /> <java-symbol type="dimen" name="floating_toolbar_minimum_overflow_height" /> + <java-symbol type="dimen" name="floating_toolbar_overflow_width" /> + <java-symbol type="dimen" name="floating_toolbar_margin" /> <java-symbol type="drawable" name="ic_chevron_left" /> <java-symbol type="drawable" name="ic_chevron_right" /> diff --git a/core/res/res/xml/default_zen_mode_config.xml b/core/res/res/xml/default_zen_mode_config.xml index 1bdc1ec..5f4199a 100644 --- a/core/res/res/xml/default_zen_mode_config.xml +++ b/core/res/res/xml/default_zen_mode_config.xml @@ -18,7 +18,6 @@ --> <!-- Default configuration for zen mode. See android.service.notification.ZenModeConfig. --> -<zen version="1"> - <allow calls="false" messages="false" /> - <sleep startHour="22" startMin="0" endHour="7" endMin="0" /> +<zen version="2"> + <allow calls="true" messages="false" reminders="true" events="true" /> </zen> diff --git a/docs/html/google/play-services/setup.jd b/docs/html/google/play-services/setup.jd index e75235e..70e7107 100644 --- a/docs/html/google/play-services/setup.jd +++ b/docs/html/google/play-services/setup.jd @@ -9,7 +9,7 @@ page.title=Setting Up Google Play Services <h2>In this document</h2> <ol> <li><a href="#Setup">Add Google Play Services to Your Project</a></li> - <li><a href="#Proguard">Create a Proguard Exception</a></li> + <li><a href="#Proguard">Create a ProGuard Exception</a></li> <li><a href="#ensure">Ensure Devices Have the Google Play services APK</a></li> </ol> @@ -195,6 +195,17 @@ you include an API that does have a separate library.)</p> </tr> </table> +<p class="note"><strong>Note:</strong> ProGuard directives are included in the Play services +client libraries to preserve the required classes. The +<a href="{@docRoot}tools/building/plugin-for-gradle.html">Android Plugin for Gradle</a> +automatically appends ProGuard configuration files in an AAR (Android ARchive) package and appends +that package to your ProGuard configuration. During project creation, Android Studio automatically +creates the ProGuard configuration files and <code>build.gradle</code> properties for ProGuard use. +To use ProGuard with Android Studio, you must enable the ProGuard setting in your +<code>build.gradle</code> <code>buildTypes</code>. For more information, see the +<a href="{@docRoot}tools/help/proguard.html">ProGuard</a> topic. </p> + + </div><!-- end studio --> <div class="select-ide eclipse"> @@ -230,6 +241,33 @@ element: you can begin developing features with the <a href="{@docRoot}reference/gms-packages.html">Google Play services APIs</a>.</p> + +<h2 id="Proguard">Create a ProGuard Exception</h2> + +<p>To prevent <a href="{@docRoot}tools/help/proguard.html">ProGuard</a> from stripping away +required classes, add the following lines in the +<code><project_directory>/proguard-project.txt</code> file: +<pre> +-keep class * extends java.util.ListResourceBundle { + protected Object[][] getContents(); +} + +-keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { + public static final *** NULL; +} + +-keepnames @com.google.android.gms.common.annotation.KeepName class * +-keepclassmembernames class * { + @com.google.android.gms.common.annotation.KeepName *; +} + +-keepnames class * implements android.os.Parcelable { + public static final ** CREATOR; +} +</pre> + + + </div><!-- end eclipse --> <div class="select-ide other"> @@ -263,8 +301,6 @@ workspace—you should not reference the library directly from the Android S you can begin developing features with the <a href="{@docRoot}reference/gms-packages.html">Google Play services APIs</a>.</p> -</div><!-- end other --> - <h2 id="Proguard">Create a Proguard Exception</h2> @@ -290,11 +326,9 @@ required classes, add the following lines in the } </pre> -<p class="note"><strong>Note:</strong> When using Android Studio, you must add Proguard -to your <code>build.gradle</code> file's build types. For more information, see the -<a href="http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Running-ProGuard" ->Gradle Plugin User Guide</a>. -</ol> + +</div><!-- end other --> + <h2 id="ensure">Ensure Devices Have the Google Play services APK</h2> diff --git a/graphics/java/android/graphics/drawable/LayerDrawable.java b/graphics/java/android/graphics/drawable/LayerDrawable.java index a2f71e5..78eee37 100644 --- a/graphics/java/android/graphics/drawable/LayerDrawable.java +++ b/graphics/java/android/graphics/drawable/LayerDrawable.java @@ -211,7 +211,11 @@ public class LayerDrawable extends Drawable implements Drawable.Callback { updateLayerFromTypedArray(layer, a); a.recycle(); - if (layer.mDrawable == null) { + // If the layer doesn't have a drawable or unresolved theme + // attribute for a drawable, attempt to parse one from the child + // element. + if (layer.mDrawable == null && (layer.mThemeAttrs == null || + layer.mThemeAttrs[R.styleable.LayerDrawableItem_drawable] == 0)) { while ((type = parser.next()) == XmlPullParser.TEXT) { } if (type != XmlPullParser.START_TAG) { diff --git a/include/androidfw/ResourceTypes.h b/include/androidfw/ResourceTypes.h index a5776a4..da70e9b 100644 --- a/include/androidfw/ResourceTypes.h +++ b/include/androidfw/ResourceTypes.h @@ -112,7 +112,7 @@ struct __assertChar16Size { * * The PNG chunk type is "npTc". */ -struct Res_png_9patch +struct alignas(uintptr_t) Res_png_9patch { Res_png_9patch() : wasDeserialized(false), xDivsOffset(0), yDivsOffset(0), colorsOffset(0) { } diff --git a/keystore/java/android/security/AndroidKeyStore.java b/keystore/java/android/security/AndroidKeyStore.java index ed690de..c5b6a68 100644 --- a/keystore/java/android/security/AndroidKeyStore.java +++ b/keystore/java/android/security/AndroidKeyStore.java @@ -512,12 +512,23 @@ public class AndroidKeyStore extends KeyStoreSpi { } } - int purposes = params.getPurposes(); + @KeyStoreKeyConstraints.PurposeEnum int purposes = params.getPurposes(); + @KeyStoreKeyConstraints.BlockModeEnum int blockModes = params.getBlockModes(); + if (((purposes & KeyStoreKeyConstraints.Purpose.ENCRYPT) != 0) + && (params.isRandomizedEncryptionRequired())) { + @KeyStoreKeyConstraints.BlockModeEnum int incompatibleBlockModes = + blockModes & ~KeyStoreKeyConstraints.BlockMode.IND_CPA_COMPATIBLE_MODES; + if (incompatibleBlockModes != 0) { + throw new KeyStoreException("Randomized encryption (IND-CPA) required but may be" + + " violated by block mode(s): " + + KeyStoreKeyConstraints.BlockMode.allToString(incompatibleBlockModes) + + ". See KeyStoreParameter documentation."); + } + } for (int keymasterPurpose : KeyStoreKeyConstraints.Purpose.allToKeymaster(purposes)) { args.addInt(KeymasterDefs.KM_TAG_PURPOSE, keymasterPurpose); } - for (int keymasterBlockMode : - KeyStoreKeyConstraints.BlockMode.allToKeymaster(params.getBlockModes())) { + for (int keymasterBlockMode : KeyStoreKeyConstraints.BlockMode.allToKeymaster(blockModes)) { args.addInt(KeymasterDefs.KM_TAG_BLOCK_MODE, keymasterBlockMode); } for (int keymasterPadding : @@ -553,8 +564,8 @@ public class AndroidKeyStore extends KeyStoreSpi { args.addInt(KeymasterDefs.KM_TAG_KEY_SIZE, keyMaterial.length * 8); if (((purposes & KeyStoreKeyConstraints.Purpose.ENCRYPT) != 0) - || ((purposes & KeyStoreKeyConstraints.Purpose.DECRYPT) != 0)) { - // Permit caller-specified IV. This is needed for the Cipher abstraction. + && (!params.isRandomizedEncryptionRequired())) { + // Permit caller-provided IV when encrypting with this key args.addBoolean(KeymasterDefs.KM_TAG_CALLER_NONCE); } diff --git a/keystore/java/android/security/AndroidKeyStoreProvider.java b/keystore/java/android/security/AndroidKeyStoreProvider.java index 635b2fa..43f3b30 100644 --- a/keystore/java/android/security/AndroidKeyStoreProvider.java +++ b/keystore/java/android/security/AndroidKeyStoreProvider.java @@ -64,6 +64,7 @@ public class AndroidKeyStoreProvider extends Provider { putSecretKeyFactoryImpl("HmacSHA512"); // javax.crypto.Mac + putMacImpl("HmacSHA1", PACKAGE_NAME + ".KeyStoreHmacSpi$HmacSHA1"); putMacImpl("HmacSHA224", PACKAGE_NAME + ".KeyStoreHmacSpi$HmacSHA224"); putMacImpl("HmacSHA256", PACKAGE_NAME + ".KeyStoreHmacSpi$HmacSHA256"); putMacImpl("HmacSHA384", PACKAGE_NAME + ".KeyStoreHmacSpi$HmacSHA384"); diff --git a/keystore/java/android/security/KeyGeneratorSpec.java b/keystore/java/android/security/KeyGeneratorSpec.java index 0e490cd..4eedb24 100644 --- a/keystore/java/android/security/KeyGeneratorSpec.java +++ b/keystore/java/android/security/KeyGeneratorSpec.java @@ -22,6 +22,7 @@ import android.text.TextUtils; import java.security.spec.AlgorithmParameterSpec; import java.util.Date; +import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -51,6 +52,7 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { private final @KeyStoreKeyConstraints.PurposeEnum int mPurposes; private final @KeyStoreKeyConstraints.PaddingEnum int mPaddings; private final @KeyStoreKeyConstraints.BlockModeEnum int mBlockModes; + private final boolean mRandomizedEncryptionRequired; private final @KeyStoreKeyConstraints.UserAuthenticatorEnum int mUserAuthenticators; private final int mUserAuthenticationValidityDurationSeconds; private final boolean mInvalidatedOnNewFingerprintEnrolled; @@ -66,6 +68,7 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { @KeyStoreKeyConstraints.PurposeEnum int purposes, @KeyStoreKeyConstraints.PaddingEnum int paddings, @KeyStoreKeyConstraints.BlockModeEnum int blockModes, + boolean randomizedEncryptionRequired, @KeyStoreKeyConstraints.UserAuthenticatorEnum int userAuthenticators, int userAuthenticationValidityDurationSeconds, boolean invalidatedOnNewFingerprintEnrolled) { @@ -89,6 +92,7 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { mPurposes = purposes; mPaddings = paddings; mBlockModes = blockModes; + mRandomizedEncryptionRequired = randomizedEncryptionRequired; mUserAuthenticators = userAuthenticators; mUserAuthenticationValidityDurationSeconds = userAuthenticationValidityDurationSeconds; mInvalidatedOnNewFingerprintEnrolled = invalidatedOnNewFingerprintEnrolled; @@ -172,6 +176,19 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { } /** + * Returns {@code true} if encryption using this key must be sufficiently randomized to produce + * different ciphertexts for the same plaintext every time. The formal cryptographic property + * being required is <em>indistinguishability under chosen-plaintext attack ({@code + * IND-CPA})</em>. This property is important because it mitigates several classes of + * weaknesses due to which ciphertext may leak information about plaintext. For example, if a + * given plaintext always produces the same ciphertext, an attacker may see the repeated + * ciphertexts and be able to deduce something about the plaintext. + */ + public boolean isRandomizedEncryptionRequired() { + return mRandomizedEncryptionRequired; + } + + /** * Gets the set of user authenticators which protect access to this key. The key can only be * used iff the user has authenticated to at least one of these user authenticators. * @@ -223,6 +240,7 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { private @KeyStoreKeyConstraints.PurposeEnum int mPurposes; private @KeyStoreKeyConstraints.PaddingEnum int mPaddings; private @KeyStoreKeyConstraints.BlockModeEnum int mBlockModes; + private boolean mRandomizedEncryptionRequired = true; private @KeyStoreKeyConstraints.UserAuthenticatorEnum int mUserAuthenticators; private int mUserAuthenticationValidityDurationSeconds = -1; private boolean mInvalidatedOnNewFingerprintEnrolled; @@ -281,7 +299,7 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { /** * Sets the time instant before which the key is not yet valid. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityEnd(Date) */ @@ -293,7 +311,7 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { /** * Sets the time instant after which the key is no longer valid. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityStart(Date) * @see #setKeyValidityForConsumptionEnd(Date) @@ -308,7 +326,7 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { /** * Sets the time instant after which the key is no longer valid for encryption and signing. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityForConsumptionEnd(Date) */ @@ -321,7 +339,7 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { * Sets the time instant after which the key is no longer valid for decryption and * verification. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityForOriginationEnd(Date) */ @@ -363,6 +381,43 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { } /** + * Sets whether encryption using this key must be sufficiently randomized to produce + * different ciphertexts for the same plaintext every time. The formal cryptographic + * property being required is <em>indistinguishability under chosen-plaintext attack + * ({@code IND-CPA})</em>. This property is important because it mitigates several classes + * of weaknesses due to which ciphertext may leak information about plaintext. For example, + * if a given plaintext always produces the same ciphertext, an attacker may see the + * repeated ciphertexts and be able to deduce something about the plaintext. + * + * <p>By default, {@code IND-CPA} is required. + * + * <p>When {@code IND-CPA} is required: + * <ul> + * <li>block modes which do not offer {@code IND-CPA}, such as {@code ECB}, are prohibited; + * </li> + * <li>in block modes which use an IV, such as {@code CBC}, {@code CTR}, and {@code GCM}, + * caller-provided IVs are rejected when encrypting, to ensure that only random IVs are + * used.</li> + * + * <p>Before disabling this requirement, consider the following approaches instead: + * <ul> + * <li>If you are generating a random IV for encryption and then initializing a {@code} + * Cipher using the IV, the solution is to let the {@code Cipher} generate a random IV + * instead. This will occur if the {@code Cipher} is initialized for encryption without an + * IV. The IV can then be queried via {@link Cipher#getIV()}.</li> + * <li>If you are generating a non-random IV (e.g., an IV derived from something not fully + * random, such as the name of the file being encrypted, or transaction ID, or password, + * or a device identifier), consider changing your design to use a random IV which will then + * be provided in addition to the ciphertext to the entities which need to decrypt the + * ciphertext.</li> + * </ul> + */ + public Builder setRandomizedEncryptionRequired(boolean required) { + mRandomizedEncryptionRequired = required; + return this; + } + + /** * Sets the user authenticators which protect access to this key. The key can only be used * iff the user has authenticated to at least one of these user authenticators. * @@ -427,6 +482,7 @@ public class KeyGeneratorSpec implements AlgorithmParameterSpec { mPurposes, mPaddings, mBlockModes, + mRandomizedEncryptionRequired, mUserAuthenticators, mUserAuthenticationValidityDurationSeconds, mInvalidatedOnNewFingerprintEnrolled); diff --git a/keystore/java/android/security/KeyPairGeneratorSpec.java b/keystore/java/android/security/KeyPairGeneratorSpec.java index 52b7097..4ca220d 100644 --- a/keystore/java/android/security/KeyPairGeneratorSpec.java +++ b/keystore/java/android/security/KeyPairGeneratorSpec.java @@ -86,6 +86,8 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { private final @KeyStoreKeyConstraints.BlockModeEnum int mBlockModes; + private final boolean mRandomizedEncryptionRequired; + private final @KeyStoreKeyConstraints.UserAuthenticatorEnum int mUserAuthenticators; private final int mUserAuthenticationValidityDurationSeconds; @@ -134,6 +136,7 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { @KeyStoreKeyConstraints.DigestEnum int digests, @KeyStoreKeyConstraints.PaddingEnum int paddings, @KeyStoreKeyConstraints.BlockModeEnum int blockModes, + boolean randomizedEncryptionRequired, @KeyStoreKeyConstraints.UserAuthenticatorEnum int userAuthenticators, int userAuthenticationValidityDurationSeconds, boolean invalidatedOnNewFingerprintEnrolled) { @@ -174,6 +177,7 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { mDigests = digests; mPaddings = paddings; mBlockModes = blockModes; + mRandomizedEncryptionRequired = randomizedEncryptionRequired; mUserAuthenticators = userAuthenticators; mUserAuthenticationValidityDurationSeconds = userAuthenticationValidityDurationSeconds; mInvalidatedOnNewFingerprintEnrolled = invalidatedOnNewFingerprintEnrolled; @@ -186,8 +190,28 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { public KeyPairGeneratorSpec(Context context, String keyStoreAlias, String keyType, int keySize, AlgorithmParameterSpec spec, X500Principal subjectDN, BigInteger serialNumber, Date startDate, Date endDate, int flags) { - this(context, keyStoreAlias, keyType, keySize, spec, subjectDN, serialNumber, startDate, - endDate, flags, startDate, endDate, endDate, 0, 0, 0, 0, 0, -1, false); + + this(context, + keyStoreAlias, + keyType, + keySize, + spec, + subjectDN, + serialNumber, + startDate, + endDate, + flags, + startDate, + endDate, + endDate, + 0, + 0, + 0, + 0, + true, + 0, + -1, + false); } /** @@ -347,6 +371,21 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { } /** + * Returns {@code true} if encryption using this key must be sufficiently randomized to produce + * different ciphertexts for the same plaintext every time. The formal cryptographic property + * being required is <em>indistinguishability under chosen-plaintext attack ({@code + * IND-CPA})</em>. This property is important because it mitigates several classes of + * weaknesses due to which ciphertext may leak information about plaintext. For example, if a + * given plaintext always produces the same ciphertext, an attacker may see the repeated + * ciphertexts and be able to deduce something about the plaintext. + * + * @hide + */ + public boolean isRandomizedEncryptionRequired() { + return mRandomizedEncryptionRequired; + } + + /** * Gets the set of user authenticators which protect access to the private key. The key can only * be used iff the user has authenticated to at least one of these user authenticators. * @@ -446,6 +485,8 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { private @KeyStoreKeyConstraints.BlockModeEnum int mBlockModes; + private boolean mRandomizedEncryptionRequired = true; + private @KeyStoreKeyConstraints.UserAuthenticatorEnum int mUserAuthenticators; private int mUserAuthenticationValidityDurationSeconds = -1; @@ -580,7 +621,7 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { /** * Sets the time instant before which the key is not yet valid. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityEnd(Date) * @@ -594,7 +635,7 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { /** * Sets the time instant after which the key is no longer valid. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityStart(Date) * @see #setKeyValidityForConsumptionEnd(Date) @@ -611,7 +652,7 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { /** * Sets the time instant after which the key is no longer valid for encryption and signing. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityForConsumptionEnd(Date) * @@ -626,7 +667,7 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { * Sets the time instant after which the key is no longer valid for decryption and * verification. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityForOriginationEnd(Date) * @@ -689,6 +730,33 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { } /** + * Sets whether encryption using this key must be sufficiently randomized to produce + * different ciphertexts for the same plaintext every time. The formal cryptographic + * property being required is <em>indistinguishability under chosen-plaintext attack + * ({@code IND-CPA})</em>. This property is important because it mitigates several classes + * of weaknesses due to which ciphertext may leak information about plaintext. For example, + * if a given plaintext always produces the same ciphertext, an attacker may see the + * repeated ciphertexts and be able to deduce something about the plaintext. + * + * <p>By default, {@code IND-CPA} is required. + * + * <p>When {@code IND-CPA} is required, encryption/decryption transformations which do not + * offer {@code IND-CPA}, such as RSA without padding, are prohibited. + * + * <p>Before disabling this requirement, consider the following approaches instead: + * <ul> + * <li>If you are using RSA encryption without padding, consider switching to padding + * schemes which offer {@code IND-CPA}, such as PKCS#1 or OAEP.</li> + * </ul> + * + * @hide + */ + public Builder setRandomizedEncryptionRequired(boolean required) { + mRandomizedEncryptionRequired = required; + return this; + } + + /** * Sets the user authenticators which protect access to this key. The key can only be used * iff the user has authenticated to at least one of these user authenticators. * @@ -771,6 +839,7 @@ public final class KeyPairGeneratorSpec implements AlgorithmParameterSpec { mDigests, mPaddings, mBlockModes, + mRandomizedEncryptionRequired, mUserAuthenticators, mUserAuthenticationValidityDurationSeconds, mInvalidatedOnNewFingerprintEnrolled); diff --git a/keystore/java/android/security/KeyStoreKeyConstraints.java b/keystore/java/android/security/KeyStoreKeyConstraints.java index 097c20f..e61092f 100644 --- a/keystore/java/android/security/KeyStoreKeyConstraints.java +++ b/keystore/java/android/security/KeyStoreKeyConstraints.java @@ -123,7 +123,7 @@ public abstract class KeyStoreKeyConstraints { } @Retention(RetentionPolicy.SOURCE) - @IntDef({Algorithm.AES, Algorithm.HMAC}) + @IntDef({Algorithm.AES, Algorithm.HMAC, Algorithm.RSA, Algorithm.EC}) public @interface AlgorithmEnum {} /** @@ -135,12 +135,22 @@ public abstract class KeyStoreKeyConstraints { /** * Key algorithm: AES. */ - public static final int AES = 0; + public static final int AES = 1 << 0; /** * Key algorithm: HMAC. */ - public static final int HMAC = 1; + public static final int HMAC = 1 << 1; + + /** + * Key algorithm: RSA. + */ + public static final int RSA = 1 << 2; + + /** + * Key algorithm: EC. + */ + public static final int EC = 1 << 3; /** * @hide @@ -151,6 +161,10 @@ public abstract class KeyStoreKeyConstraints { return KeymasterDefs.KM_ALGORITHM_AES; case HMAC: return KeymasterDefs.KM_ALGORITHM_HMAC; + case RSA: + return KeymasterDefs.KM_ALGORITHM_RSA; + case EC: + return KeymasterDefs.KM_ALGORITHM_ECDSA; default: throw new IllegalArgumentException("Unknown algorithm: " + algorithm); } @@ -165,6 +179,10 @@ public abstract class KeyStoreKeyConstraints { return AES; case KeymasterDefs.KM_ALGORITHM_HMAC: return HMAC; + case KeymasterDefs.KM_ALGORITHM_RSA: + return RSA; + case KeymasterDefs.KM_ALGORITHM_ECDSA: + return EC; default: throw new IllegalArgumentException("Unknown algorithm: " + algorithm); } @@ -179,6 +197,10 @@ public abstract class KeyStoreKeyConstraints { return "AES"; case HMAC: return "HMAC"; + case RSA: + return "RSA"; + case EC: + return "EC"; default: throw new IllegalArgumentException("Unknown algorithm: " + algorithm); } @@ -213,8 +235,18 @@ public abstract class KeyStoreKeyConstraints { throw new IllegalArgumentException("HMAC digest not specified"); } switch (digest) { + case Digest.MD5: + return "HmacMD5"; + case Digest.SHA1: + return "HmacSHA1"; + case Digest.SHA224: + return "HmacSHA224"; case Digest.SHA256: return "HmacSHA256"; + case Digest.SHA384: + return "HmacSHA384"; + case Digest.SHA512: + return "HmacSHA512"; default: throw new IllegalArgumentException( "Unsupported HMAC digest: " + digest); @@ -223,11 +255,32 @@ public abstract class KeyStoreKeyConstraints { throw new IllegalArgumentException("Unsupported key algorithm: " + algorithm); } } + + /** + * @hide + */ + public static String toJCAKeyPairAlgorithm(@AlgorithmEnum int algorithm) { + switch (algorithm) { + case RSA: + return "RSA"; + case EC: + return "EC"; + default: + throw new IllegalArgumentException("Unsupported key alorithm: " + algorithm); + } + } } @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, - value = {Padding.NONE, Padding.PKCS7}) + value = { + Padding.NONE, + Padding.PKCS7, + Padding.RSA_PKCS1_ENCRYPTION, + Padding.RSA_PKCS1_SIGNATURE, + Padding.RSA_OAEP, + Padding.RSA_PSS, + }) public @interface PaddingEnum {} /** @@ -247,6 +300,26 @@ public abstract class KeyStoreKeyConstraints { public static final int PKCS7 = 1 << 1; /** + * RSA PKCS#1 v1.5 padding for encryption/decryption. + */ + public static final int RSA_PKCS1_ENCRYPTION = 1 << 2; + + /** + * RSA PKCS#1 v1.5 padding for signatures. + */ + public static final int RSA_PKCS1_SIGNATURE = 1 << 3; + + /** + * RSA Optimal Asymmetric Encryption Padding (OAEP). + */ + public static final int RSA_OAEP = 1 << 4; + + /** + * RSA PKCS#1 v2.1 Probabilistic Signature Scheme (PSS) padding. + */ + public static final int RSA_PSS = 1 << 5; + + /** * @hide */ public static int toKeymaster(int padding) { @@ -255,6 +328,14 @@ public abstract class KeyStoreKeyConstraints { return KeymasterDefs.KM_PAD_NONE; case PKCS7: return KeymasterDefs.KM_PAD_PKCS7; + case RSA_PKCS1_ENCRYPTION: + return KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_ENCRYPT; + case RSA_PKCS1_SIGNATURE: + return KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_SIGN; + case RSA_OAEP: + return KeymasterDefs.KM_PAD_RSA_OAEP; + case RSA_PSS: + return KeymasterDefs.KM_PAD_RSA_PSS; default: throw new IllegalArgumentException("Unknown padding: " + padding); } @@ -269,6 +350,14 @@ public abstract class KeyStoreKeyConstraints { return NONE; case KeymasterDefs.KM_PAD_PKCS7: return PKCS7; + case KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_ENCRYPT: + return RSA_PKCS1_ENCRYPTION; + case KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_SIGN: + return RSA_PKCS1_SIGNATURE; + case KeymasterDefs.KM_PAD_RSA_OAEP: + return RSA_OAEP; + case KeymasterDefs.KM_PAD_RSA_PSS: + return RSA_PSS; default: throw new IllegalArgumentException("Unknown padding: " + padding); } @@ -283,6 +372,14 @@ public abstract class KeyStoreKeyConstraints { return "NONE"; case PKCS7: return "PKCS#7"; + case RSA_PKCS1_ENCRYPTION: + return "RSA PKCS#1 (encryption)"; + case RSA_PKCS1_SIGNATURE: + return "RSA PKCS#1 (signature)"; + case RSA_OAEP: + return "RSA OAEP"; + case RSA_PSS: + return "RSA PSS"; default: throw new IllegalArgumentException("Unknown padding: " + padding); } @@ -291,12 +388,18 @@ public abstract class KeyStoreKeyConstraints { /** * @hide */ - public static @PaddingEnum int fromJCAPadding(String padding) { + public static @PaddingEnum int fromJCACipherPadding(String padding) { String paddingLower = padding.toLowerCase(Locale.US); if ("nopadding".equals(paddingLower)) { return NONE; } else if ("pkcs7padding".equals(paddingLower)) { return PKCS7; + } else if ("pkcs1padding".equals(paddingLower)) { + return RSA_PKCS1_ENCRYPTION; + } else if (("oaeppadding".equals(paddingLower)) + || ((paddingLower.startsWith("oaepwith")) + && (paddingLower.endsWith("padding")))) { + return RSA_OAEP; } else { throw new IllegalArgumentException("Unknown padding: " + padding); } @@ -592,6 +695,14 @@ public abstract class KeyStoreKeyConstraints { public static final int GCM = 1 << 3; /** + * Set of block modes compatible with IND-CPA if used correctly. + * + * @hide + */ + public static final @BlockModeEnum int IND_CPA_COMPATIBLE_MODES = + CBC | CTR | GCM; + + /** * @hide */ public static int toKeymaster(@BlockModeEnum int mode) { @@ -670,6 +781,24 @@ public abstract class KeyStoreKeyConstraints { /** * @hide */ + public static String allToString(@BlockModeEnum int modes) { + StringBuilder result = new StringBuilder("["); + boolean firstValue = true; + for (@BlockModeEnum int mode : getSetFlags(modes)) { + if (firstValue) { + firstValue = false; + } else { + result.append(", "); + } + result.append(toString(mode)); + } + result.append(']'); + return result.toString(); + } + + /** + * @hide + */ public static @BlockModeEnum int fromJCAMode(String mode) { String modeLower = mode.toLowerCase(Locale.US); if ("ecb".equals(modeLower)) { diff --git a/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java b/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java index 279acd6..b39d16d 100644 --- a/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java +++ b/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java @@ -138,13 +138,26 @@ public abstract class KeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { } int keySizeBits = (spec.getKeySize() != null) ? spec.getKeySize() : mDefaultKeySizeBits; args.addInt(KeymasterDefs.KM_TAG_KEY_SIZE, keySizeBits); - int purposes = spec.getPurposes(); + @KeyStoreKeyConstraints.PurposeEnum int purposes = spec.getPurposes(); + @KeyStoreKeyConstraints.BlockModeEnum int blockModes = spec.getBlockModes(); + if (((purposes & KeyStoreKeyConstraints.Purpose.ENCRYPT) != 0) + && (spec.isRandomizedEncryptionRequired())) { + @KeyStoreKeyConstraints.BlockModeEnum int incompatibleBlockModes = + blockModes & ~KeyStoreKeyConstraints.BlockMode.IND_CPA_COMPATIBLE_MODES; + if (incompatibleBlockModes != 0) { + throw new IllegalStateException( + "Randomized encryption (IND-CPA) required but may be violated by block" + + " mode(s): " + + KeyStoreKeyConstraints.BlockMode.allToString(incompatibleBlockModes) + + ". See KeyGeneratorSpec documentation."); + } + } + for (int keymasterPurpose : KeyStoreKeyConstraints.Purpose.allToKeymaster(purposes)) { args.addInt(KeymasterDefs.KM_TAG_PURPOSE, keymasterPurpose); } - for (int keymasterBlockMode : - KeyStoreKeyConstraints.BlockMode.allToKeymaster(spec.getBlockModes())) { + for (int keymasterBlockMode : KeyStoreKeyConstraints.BlockMode.allToKeymaster(blockModes)) { args.addInt(KeymasterDefs.KM_TAG_BLOCK_MODE, keymasterBlockMode); } for (int keymasterPadding : @@ -177,8 +190,8 @@ public abstract class KeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { ? spec.getKeyValidityForConsumptionEnd() : new Date(Long.MAX_VALUE)); if (((purposes & KeyStoreKeyConstraints.Purpose.ENCRYPT) != 0) - || ((purposes & KeyStoreKeyConstraints.Purpose.DECRYPT) != 0)) { - // Permit caller-specified IV. This is needed due to the Cipher abstraction. + && (!spec.isRandomizedEncryptionRequired())) { + // Permit caller-provided IV when encrypting with this key args.addBoolean(KeymasterDefs.KM_TAG_CALLER_NONCE); } diff --git a/keystore/java/android/security/KeyStoreParameter.java b/keystore/java/android/security/KeyStoreParameter.java index 0b2f9b6..751eef5 100644 --- a/keystore/java/android/security/KeyStoreParameter.java +++ b/keystore/java/android/security/KeyStoreParameter.java @@ -19,13 +19,14 @@ package android.security; import android.content.Context; import java.security.Key; -import java.security.KeyPairGenerator; import java.security.KeyStore.ProtectionParameter; import java.util.Date; +import javax.crypto.Cipher; + /** - * This provides the optional parameters that can be specified for - * {@code KeyStore} entries that work with + * Parameters specifying how to secure and restrict the use of a key being + * imported into the * <a href="{@docRoot}training/articles/keystore.html">Android KeyStore * facility</a>. The Android KeyStore facility is accessed through a * {@link java.security.KeyStore} API using the {@code AndroidKeyStore} @@ -36,12 +37,6 @@ import java.util.Date; * there is only one logical instance of the {@code KeyStore} per application * UID so apps using the {@code sharedUid} facility will also share a * {@code KeyStore}. - * <p> - * Keys may be generated using the {@link KeyPairGenerator} facility with a - * {@link KeyPairGeneratorSpec} to specify the entry's {@code alias}. A - * self-signed X.509 certificate will be attached to generated entries, but that - * may be replaced at a later time by a certificate signed by a real Certificate - * Authority. */ public final class KeyStoreParameter implements ProtectionParameter { private int mFlags; @@ -52,6 +47,7 @@ public final class KeyStoreParameter implements ProtectionParameter { private final @KeyStoreKeyConstraints.PaddingEnum int mPaddings; private final @KeyStoreKeyConstraints.DigestEnum Integer mDigests; private final @KeyStoreKeyConstraints.BlockModeEnum int mBlockModes; + private final boolean mRandomizedEncryptionRequired; private final @KeyStoreKeyConstraints.UserAuthenticatorEnum int mUserAuthenticators; private final int mUserAuthenticationValidityDurationSeconds; private final boolean mInvalidatedOnNewFingerprintEnrolled; @@ -64,6 +60,7 @@ public final class KeyStoreParameter implements ProtectionParameter { @KeyStoreKeyConstraints.PaddingEnum int paddings, @KeyStoreKeyConstraints.DigestEnum Integer digests, @KeyStoreKeyConstraints.BlockModeEnum int blockModes, + boolean randomizedEncryptionRequired, @KeyStoreKeyConstraints.UserAuthenticatorEnum int userAuthenticators, int userAuthenticationValidityDurationSeconds, boolean invalidatedOnNewFingerprintEnrolled) { @@ -81,6 +78,7 @@ public final class KeyStoreParameter implements ProtectionParameter { mPaddings = paddings; mDigests = digests; mBlockModes = blockModes; + mRandomizedEncryptionRequired = randomizedEncryptionRequired; mUserAuthenticators = userAuthenticators; mUserAuthenticationValidityDurationSeconds = userAuthenticationValidityDurationSeconds; mInvalidatedOnNewFingerprintEnrolled = invalidatedOnNewFingerprintEnrolled; @@ -188,6 +186,21 @@ public final class KeyStoreParameter implements ProtectionParameter { } /** + * Returns {@code true} if encryption using this key must be sufficiently randomized to produce + * different ciphertexts for the same plaintext every time. The formal cryptographic property + * being required is <em>indistinguishability under chosen-plaintext attack ({@code + * IND-CPA})</em>. This property is important because it mitigates several classes of + * weaknesses due to which ciphertext may leak information about plaintext. For example, if a + * given plaintext always produces the same ciphertext, an attacker may see the repeated + * ciphertexts and be able to deduce something about the plaintext. + * + * @hide + */ + public boolean isRandomizedEncryptionRequired() { + return mRandomizedEncryptionRequired; + } + + /** * Gets the set of user authenticators which protect access to this key. The key can only be * used iff the user has authenticated to at least one of these user authenticators. * @@ -251,6 +264,7 @@ public final class KeyStoreParameter implements ProtectionParameter { private @KeyStoreKeyConstraints.PaddingEnum int mPaddings; private @KeyStoreKeyConstraints.DigestEnum Integer mDigests; private @KeyStoreKeyConstraints.BlockModeEnum int mBlockModes; + private boolean mRandomizedEncryptionRequired = true; private @KeyStoreKeyConstraints.UserAuthenticatorEnum int mUserAuthenticators; private int mUserAuthenticationValidityDurationSeconds = -1; private boolean mInvalidatedOnNewFingerprintEnrolled; @@ -287,7 +301,7 @@ public final class KeyStoreParameter implements ProtectionParameter { /** * Sets the time instant before which the key is not yet valid. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityEnd(Date) * @@ -301,7 +315,7 @@ public final class KeyStoreParameter implements ProtectionParameter { /** * Sets the time instant after which the key is no longer valid. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityStart(Date) * @see #setKeyValidityForConsumptionEnd(Date) @@ -318,7 +332,7 @@ public final class KeyStoreParameter implements ProtectionParameter { /** * Sets the time instant after which the key is no longer valid for encryption and signing. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityForConsumptionEnd(Date) * @@ -333,7 +347,7 @@ public final class KeyStoreParameter implements ProtectionParameter { * Sets the time instant after which the key is no longer valid for decryption and * verification. * - * <b>By default, the key is valid at any instant. + * <p>By default, the key is valid at any instant. * * @see #setKeyValidityForOriginationEnd(Date) * @@ -398,6 +412,47 @@ public final class KeyStoreParameter implements ProtectionParameter { } /** + * Sets whether encryption using this key must be sufficiently randomized to produce + * different ciphertexts for the same plaintext every time. The formal cryptographic + * property being required is <em>indistinguishability under chosen-plaintext attack + * ({@code IND-CPA})</em>. This property is important because it mitigates several classes + * of weaknesses due to which ciphertext may leak information about plaintext. For example, + * if a given plaintext always produces the same ciphertext, an attacker may see the + * repeated ciphertexts and be able to deduce something about the plaintext. + * + * <p>By default, {@code IND-CPA} is required. + * + * <p>When {@code IND-CPA} is required: + * <ul> + * <li>transformation which do not offer {@code IND-CPA}, such as symmetric ciphers using + * {@code ECB} mode or RSA encryption without padding, are prohibited;</li> + * <li>in transformations which use an IV, such as symmetric ciphers in {@code CBC}, + * {@code CTR}, and {@code GCM} block modes, caller-provided IVs are rejected when + * encrypting, to ensure that only random IVs are used.</li> + * + * <p>Before disabling this requirement, consider the following approaches instead: + * <ul> + * <li>If you are generating a random IV for encryption and then initializing a {@code} + * Cipher using the IV, the solution is to let the {@code Cipher} generate a random IV + * instead. This will occur if the {@code Cipher} is initialized for encryption without an + * IV. The IV can then be queried via {@link Cipher#getIV()}.</li> + * <li>If you are generating a non-random IV (e.g., an IV derived from something not fully + * random, such as the name of the file being encrypted, or transaction ID, or password, + * or a device identifier), consider changing your design to use a random IV which will then + * be provided in addition to the ciphertext to the entities which need to decrypt the + * ciphertext.</li> + * <li>If you are using RSA encryption without padding, consider switching to padding + * schemes which offer {@code IND-CPA}, such as PKCS#1 or OAEP.</li> + * </ul> + * + * @hide + */ + public Builder setRandomizedEncryptionRequired(boolean required) { + mRandomizedEncryptionRequired = required; + return this; + } + + /** * Sets the user authenticators which protect access to this key. The key can only be used * iff the user has authenticated to at least one of these user authenticators. * @@ -465,6 +520,7 @@ public final class KeyStoreParameter implements ProtectionParameter { mPaddings, mDigests, mBlockModes, + mRandomizedEncryptionRequired, mUserAuthenticators, mUserAuthenticationValidityDurationSeconds, mInvalidatedOnNewFingerprintEnrolled); diff --git a/libs/hwui/AmbientShadow.cpp b/libs/hwui/AmbientShadow.cpp index 0a210d6..a4100a2 100644 --- a/libs/hwui/AmbientShadow.cpp +++ b/libs/hwui/AmbientShadow.cpp @@ -45,8 +45,9 @@ /** * Other constants: */ -// For the edge of the penumbra, the opacity is 0. -#define OUTER_OPACITY (0.0f) +// For the edge of the penumbra, the opacity is 0. After transform (1 - alpha), +// it is 1. +#define TRANSFORMED_OUTER_OPACITY (1.0f) // Once the alpha difference is greater than this threshold, we will allocate extra // edge vertices. @@ -83,11 +84,13 @@ inline float getAlphaFromFactoredZ(float factoredZ) { return 1.0 / (1 + MathUtils::max(factoredZ, 0.0f)); } +// The shader is using gaussian function e^-(1-x)*(1-x)*4, therefore, we transform +// the alpha value to (1 - alpha) inline float getTransformedAlphaFromAlpha(float alpha) { - return acosf(1.0f - 2.0f * alpha); + return 1.0f - alpha; } -// The output is ranged from 0 to M_PI. +// The output is ranged from 0 to 1. inline float getTransformedAlphaFromFactoredZ(float factoredZ) { return getTransformedAlphaFromAlpha(getAlphaFromFactoredZ(factoredZ)); } @@ -249,7 +252,7 @@ void AmbientShadow::createAmbientShadow(bool isCasterOpaque, indexBuffer[indexBufferIndex++] = vertexBufferIndex; indexBuffer[indexBufferIndex++] = currentInnerVertexIndex; AlphaVertex::set(&shadowVertices[vertexBufferIndex++], outerVertex.x, - outerVertex.y, OUTER_OPACITY); + outerVertex.y, TRANSFORMED_OUTER_OPACITY); if (j == 0) { outerStart = outerVertex; @@ -285,7 +288,7 @@ void AmbientShadow::createAmbientShadow(bool isCasterOpaque, (outerLast * startWeight + outerNext * k) / extraVerticesNumber; indexBuffer[indexBufferIndex++] = vertexBufferIndex; AlphaVertex::set(&shadowVertices[vertexBufferIndex++], currentOuter.x, - currentOuter.y, OUTER_OPACITY); + currentOuter.y, TRANSFORMED_OUTER_OPACITY); if (!isCasterOpaque) { umbraVertices[umbraIndex++] = vertexBufferIndex; diff --git a/libs/hwui/ProgramCache.cpp b/libs/hwui/ProgramCache.cpp index e9b22e2..41adda1 100644 --- a/libs/hwui/ProgramCache.cpp +++ b/libs/hwui/ProgramCache.cpp @@ -240,8 +240,9 @@ const char* gFS_Main_ModulateColor = const char* gFS_Main_ApplyVertexAlphaLinearInterp = " fragColor *= alpha;\n"; const char* gFS_Main_ApplyVertexAlphaShadowInterp = - " fragColor *= (1.0 - cos(alpha)) / 2.0;\n"; - + // Use a gaussian function for the shadow fall off. Note that alpha here + // is actually (1.0 - alpha) for saving computation. + " fragColor *= exp(- alpha * alpha * 4.0) - 0.018;\n"; const char* gFS_Main_FetchTexture[2] = { // Don't modulate " fragColor = texture2D(baseSampler, outTexCoords);\n", diff --git a/libs/hwui/SpotShadow.cpp b/libs/hwui/SpotShadow.cpp index b3b06d6..db3c2d9 100644 --- a/libs/hwui/SpotShadow.cpp +++ b/libs/hwui/SpotShadow.cpp @@ -44,6 +44,9 @@ // For each RADIANS_DIVISOR, we would allocate one more vertex b/t the normals. #define SPOT_CORNER_RADIANS_DIVISOR (M_PI / SPOT_EXTRA_CORNER_VERTEX_PER_PI) +// For performance, we use (1 - alpha) value for the shader input. +#define TRANSFORMED_PENUMBRA_ALPHA 1.0f +#define TRANSFORMED_UMBRA_ALPHA 0.0f #include <math.h> #include <stdlib.h> @@ -964,11 +967,11 @@ void SpotShadow::generateTriangleStrip(bool isCasterOpaque, float shadowStrength // Fill the IB and VB for the penumbra area. for (int i = 0; i < newPenumbraLength; i++) { AlphaVertex::set(&shadowVertices[vertexBufferIndex++], newPenumbra[i].x, - newPenumbra[i].y, 0.0f); + newPenumbra[i].y, TRANSFORMED_PENUMBRA_ALPHA); } for (int i = 0; i < umbraLength; i++) { AlphaVertex::set(&shadowVertices[vertexBufferIndex++], umbra[i].x, umbra[i].y, - M_PI); + TRANSFORMED_UMBRA_ALPHA); } for (int i = 0; i < verticesPairIndex; i++) { @@ -1008,14 +1011,14 @@ void SpotShadow::generateTriangleStrip(bool isCasterOpaque, float shadowStrength indexBuffer[indexBufferIndex++] = newPenumbraLength + i; indexBuffer[indexBufferIndex++] = vertexBufferIndex; AlphaVertex::set(&shadowVertices[vertexBufferIndex++], - closerVertex.x, closerVertex.y, M_PI); + closerVertex.x, closerVertex.y, TRANSFORMED_UMBRA_ALPHA); } } else { // If there is no occluded umbra at all, then draw the triangle fan // starting from the centroid to all umbra vertices. int lastCentroidIndex = vertexBufferIndex; AlphaVertex::set(&shadowVertices[vertexBufferIndex++], centroid.x, - centroid.y, M_PI); + centroid.y, TRANSFORMED_UMBRA_ALPHA); for (int i = 0; i < umbraLength; i++) { indexBuffer[indexBufferIndex++] = newPenumbraLength + i; indexBuffer[indexBufferIndex++] = lastCentroidIndex; diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index fd7fca6..e028e3f 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -325,6 +325,13 @@ final public class MediaCodec { */ public static final int BUFFER_FLAG_END_OF_STREAM = 4; + /** + * This indicates that the codec is released because the media resources used by the codec + * have been reclaimed, for example by the resource manager. + * This is used by the {@link Callback#onCodecReleased} callback. + */ + public static final int REASON_RECLAIMED = 1; + private EventHandler mEventHandler; private Callback mCallback; @@ -335,6 +342,7 @@ final public class MediaCodec { private static final int CB_OUTPUT_AVAILABLE = 2; private static final int CB_ERROR = 3; private static final int CB_OUTPUT_FORMAT_CHANGE = 4; + private static final int CB_CODEC_RELEASED = 5; private class EventHandler extends Handler { private MediaCodec mCodec; @@ -405,6 +413,13 @@ final public class MediaCodec { break; } + case CB_CODEC_RELEASED: + { + int reason = msg.arg2; + mCallback.onCodecReleased(mCodec, reason); + break; + } + default: { break; @@ -720,6 +735,7 @@ final public class MediaCodec { } /* Must be in sync with android_media_MediaCodec.cpp */ + private final static int ACTION_FATAL = 0; private final static int ACTION_TRANSIENT = 1; private final static int ACTION_RECOVERABLE = 2; @@ -1654,6 +1670,22 @@ final public class MediaCodec { * @param format The new output format. */ public abstract void onOutputFormatChanged(MediaCodec codec, MediaFormat format); + + /** + * Called when the underlying codec component has been released. + * <p> + * At this point the MediaCodec must be released, as it has moved to terminal + * Uninitialized state. + * + * @param codec The MediaCodec object. + * @param reason The reason of the release. + */ + public void onCodecReleased(MediaCodec codec, int reason) { + int errorCode = -1; + String detailMessage = "resources reclaimed"; + onError(codec, + new CodecException(errorCode, CodecException.ACTION_FATAL, detailMessage)); + } } private void postEventFromNative( diff --git a/media/java/android/media/MediaDrm.java b/media/java/android/media/MediaDrm.java index 069f7ff..fc5fc43 100644 --- a/media/java/android/media/MediaDrm.java +++ b/media/java/android/media/MediaDrm.java @@ -17,9 +17,10 @@ package android.media; import java.lang.ref.WeakReference; -import java.util.UUID; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.UUID; import android.annotation.SystemApi; import android.os.Handler; import android.os.Looper; @@ -98,12 +99,14 @@ import android.util.Log; */ public final class MediaDrm { - private final static String TAG = "MediaDrm"; + private static final String TAG = "MediaDrm"; private static final String PERMISSION = android.Manifest.permission.ACCESS_DRM_CERTIFICATES; private EventHandler mEventHandler; private OnEventListener mOnEventListener; + private OnKeysChangeListener mOnKeysChangeListener; + private OnExpirationUpdateListener mOnExpirationUpdateListener; private long mNativeContext; @@ -227,6 +230,148 @@ public final class MediaDrm { } /** + * Register a callback to be invoked when a session expiration update + * occurs. The app's OnExpirationUpdateListener will be notified + * when the expiration time of the keys in the session have changed. + * @param listener the callback that will be run + * @param handler the handler on which the listener should be invoked, or + * null if the listener should be invoked on the calling thread's looper. + */ + public void setOnExpirationUpdateListener(OnExpirationUpdateListener listener, + Handler handler) + { + if (listener != null) { + Looper looper = handler != null ? handler.getLooper() : Looper.myLooper(); + if (looper != null) { + if (mEventHandler == null || mEventHandler.getLooper() != looper) { + mEventHandler = new EventHandler(this, looper); + } + } + } + mOnExpirationUpdateListener = listener; + } + + /** + * Interface definition for a callback to be invoked when a drm session + * expiration update occurs + */ + public interface OnExpirationUpdateListener + { + /** + * Called when a session expiration update occurs, to inform the app + * about the change in expiration time + * + * @param md the MediaDrm object on which the event occurred + * @param sessionId the DRM session ID on which the event occurred + * @param expirationTime the new expiration time for the keys in the session. + * The time is in milliseconds, relative to the Unix epoch. + */ + void onExpirationUpdate(MediaDrm md, byte[] sessionId, long expirationTime); + } + + /** + * Register a callback to be invoked when the state of keys in a session + * change, e.g. when a license update occurs or when a license expires. + * + * @param listener the callback that will be run when key status changes + * @param handler the handler on which the listener should be invoked, or + * null if the listener should be invoked on the calling thread's looper. + */ + public void setOnKeysChangeListener(OnKeysChangeListener listener, + Handler handler) + { + if (listener != null) { + Looper looper = handler != null ? handler.getLooper() : Looper.myLooper(); + if (looper != null) { + if (mEventHandler == null || mEventHandler.getLooper() != looper) { + mEventHandler = new EventHandler(this, looper); + } + } + } + mOnKeysChangeListener = listener; + } + + /** + * Interface definition for a callback to be invoked when the keys in a drm + * session change states. + */ + public interface OnKeysChangeListener + { + /** + * Called when the keys in a session change status, such as when the license + * is renewed or expires. + * + * @param md the MediaDrm object on which the event occurred + * @param sessionId the DRM session ID on which the event occurred + * @param keyInformation a list of {@link MediaDrm.KeyStatus} + * instances indicating the status for each key in the session + * @param hasNewUsableKey indicates if a key has been added that is usable, + * which may trigger an attempt to resume playback on the media stream + * if it is currently blocked waiting for a key. + */ + void onKeysChange(MediaDrm md, byte[] sessionId, List<KeyStatus> keyInformation, + boolean hasNewUsableKey); + } + + /** + * The key is currently usable to decrypt media data + */ + public static final int KEY_STATUS_USABLE = 0; + + /** + * The key is no longer usable to decrypt media data because its + * expiration time has passed. + */ + public static final int KEY_STATUS_EXPIRED = 1; + + /** + * The key is not currently usable to decrypt media data because its + * output requirements cannot currently be met. + */ + public static final int KEY_STATUS_OUTPUT_NOT_ALLOWED = 2; + + /** + * The status of the key is not yet known and is being determined. + * The status will be updated with the actual status when it has + * been determined. + */ + public static final int KEY_STATUS_PENDING = 3; + + /** + * The key is not currently usable to decrypt media data because of an + * internal error in processing unrelated to input parameters. This error + * is not actionable by an app. + */ + public static final int KEY_STATUS_INTERNAL_ERROR = 4; + + + /** + * Defines the status of a key. + * A KeyStatus for each key in a session is provided to the + * {@link OnKeysChangeListener#onKeysChange} + * listener. + */ + public static final class KeyStatus { + private final byte[] mKeyId; + private final int mStatusCode; + + KeyStatus(byte[] keyId, int statusCode) { + mKeyId = keyId; + mStatusCode = statusCode; + } + + /** + * Returns the status code for the key + */ + public int getStatusCode() { return mStatusCode; } + + /** + * Returns the id for the key + */ + public byte[] getKeyId() { return mKeyId; } + } + + /** * Register a callback to be invoked when an event occurs * * @param listener the callback that will be run @@ -289,6 +434,8 @@ public final class MediaDrm { public static final int EVENT_SESSION_RECLAIMED = 5; private static final int DRM_EVENT = 200; + private static final int EXPIRATION_UPDATE = 201; + private static final int KEYS_CHANGE = 202; private class EventHandler extends Handler { @@ -308,8 +455,6 @@ public final class MediaDrm { switch(msg.what) { case DRM_EVENT: - Log.i(TAG, "Drm event (" + msg.arg1 + "," + msg.arg2 + ")"); - if (mOnEventListener != null) { if (msg.obj != null && msg.obj instanceof Parcel) { Parcel parcel = (Parcel)msg.obj; @@ -321,11 +466,46 @@ public final class MediaDrm { if (data.length == 0) { data = null; } + + Log.i(TAG, "Drm event (" + msg.arg1 + "," + msg.arg2 + ")"); mOnEventListener.onEvent(mMediaDrm, sessionId, msg.arg1, msg.arg2, data); } } return; + case KEYS_CHANGE: + if (mOnKeysChangeListener != null) { + if (msg.obj != null && msg.obj instanceof Parcel) { + Parcel parcel = (Parcel)msg.obj; + byte[] sessionId = parcel.createByteArray(); + if (sessionId.length > 0) { + List<KeyStatus> keyStatusList = keyStatusListFromParcel(parcel); + boolean hasNewUsableKey = (parcel.readInt() != 0); + + Log.i(TAG, "Drm keys change"); + mOnKeysChangeListener.onKeysChange(mMediaDrm, sessionId, keyStatusList, + hasNewUsableKey); + } + } + } + return; + + case EXPIRATION_UPDATE: + if (mOnExpirationUpdateListener != null) { + if (msg.obj != null && msg.obj instanceof Parcel) { + Parcel parcel = (Parcel)msg.obj; + byte[] sessionId = parcel.createByteArray(); + if (sessionId.length > 0) { + long expirationTime = parcel.readLong(); + + Log.i(TAG, "Drm key expiration update: " + expirationTime); + mOnExpirationUpdateListener.onExpirationUpdate(mMediaDrm, sessionId, + expirationTime); + } + } + } + return; + default: Log.e(TAG, "Unknown message type " + msg.what); return; @@ -333,7 +513,21 @@ public final class MediaDrm { } } - /* + /** + * Parse a list of KeyStatus objects from an event parcel + */ + private List<KeyStatus> keyStatusListFromParcel(Parcel parcel) { + int nelems = parcel.readInt(); + List<KeyStatus> keyStatusList = new ArrayList(nelems); + while (nelems-- > 0) { + byte[] keyId = parcel.createByteArray(); + int keyStatusCode = parcel.readInt(); + keyStatusList.add(new KeyStatus(keyId, keyStatusCode)); + } + return keyStatusList; + } + + /** * This method is called from native code when an event occurs. This method * just uses the EventHandler system to post the event back to the main app thread. * We use a weak reference to the original MediaPlayer object so that the native @@ -341,14 +535,14 @@ public final class MediaDrm { * the cookie passed to native_setup().) */ private static void postEventFromNative(Object mediadrm_ref, - int eventType, int extra, Object obj) + int what, int eventType, int extra, Object obj) { - MediaDrm md = (MediaDrm)((WeakReference)mediadrm_ref).get(); + MediaDrm md = (MediaDrm)((WeakReference<MediaDrm>)mediadrm_ref).get(); if (md == null) { return; } if (md.mEventHandler != null) { - Message m = md.mEventHandler.obtainMessage(DRM_EVENT, eventType, extra, obj); + Message m = md.mEventHandler.obtainMessage(what, eventType, extra, obj); md.mEventHandler.sendMessage(m); } } @@ -404,7 +598,7 @@ public final class MediaDrm { /** * Contains the opaque data an app uses to request keys from a license server */ - public final static class KeyRequest { + public static final class KeyRequest { private byte[] mData; private String mDefaultUrl; private int mRequestType; @@ -521,7 +715,7 @@ public final class MediaDrm { * Contains the opaque data an app uses to request a certificate from a provisioning * server */ - public final static class ProvisionRequest { + public static final class ProvisionRequest { ProvisionRequest() {} /** @@ -812,7 +1006,7 @@ public final class MediaDrm { * * @hide - not part of the public API at this time */ - public final static class CertificateRequest { + public static final class CertificateRequest { private byte[] mData; private String mDefaultUrl; @@ -860,7 +1054,7 @@ public final class MediaDrm { * * @hide - not part of the public API at this time */ - public final static class Certificate { + public static final class Certificate { Certificate() {} /** diff --git a/media/java/android/media/MediaMetadataRetriever.java b/media/java/android/media/MediaMetadataRetriever.java index 9a69c06..9aa8003 100644 --- a/media/java/android/media/MediaMetadataRetriever.java +++ b/media/java/android/media/MediaMetadataRetriever.java @@ -498,5 +498,11 @@ public class MediaMetadataRetriever * The video rotation angle may be 0, 90, 180, or 270 degrees. */ public static final int METADATA_KEY_VIDEO_ROTATION = 24; + /** + * This key retrieves the original capture framerate, if it's + * available. The capture framerate will be a floating point + * number. + */ + public static final int METADATA_KEY_CAPTURE_FRAMERATE = 25; // Add more here... } diff --git a/media/java/android/media/midi/IMidiDeviceServer.aidl b/media/java/android/media/midi/IMidiDeviceServer.aidl index 642078a..96d12fd 100644 --- a/media/java/android/media/midi/IMidiDeviceServer.aidl +++ b/media/java/android/media/midi/IMidiDeviceServer.aidl @@ -16,6 +16,7 @@ package android.media.midi; +import android.media.midi.MidiDeviceInfo; import android.os.ParcelFileDescriptor; /** @hide */ @@ -27,4 +28,6 @@ interface IMidiDeviceServer // connects the input port pfd to the specified output port void connectPorts(IBinder token, in ParcelFileDescriptor pfd, int outputPortNumber); + + MidiDeviceInfo getDeviceInfo(); } diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java index bc85f92..a316a44 100644 --- a/media/java/android/media/midi/MidiDeviceServer.java +++ b/media/java/android/media/midi/MidiDeviceServer.java @@ -252,6 +252,11 @@ public final class MidiDeviceServer implements Closeable { mPortClients.put(token, client); } } + + @Override + public MidiDeviceInfo getDeviceInfo() { + return mDeviceInfo; + } }; /* package */ MidiDeviceServer(IMidiManager midiManager, MidiReceiver[] inputPortReceivers, @@ -279,6 +284,10 @@ public final class MidiDeviceServer implements Closeable { return mServer; } + public IBinder asBinder() { + return mServer.asBinder(); + } + /* package */ void setDeviceInfo(MidiDeviceInfo deviceInfo) { if (mDeviceInfo != null) { throw new IllegalStateException("setDeviceInfo should only be called once"); diff --git a/media/java/android/media/midi/MidiInputPort.java b/media/java/android/media/midi/MidiInputPort.java index 1d3b37a..ff16a57 100644 --- a/media/java/android/media/midi/MidiInputPort.java +++ b/media/java/android/media/midi/MidiInputPort.java @@ -83,7 +83,18 @@ public final class MidiInputPort extends MidiReceiver implements Closeable { if (mOutputStream == null) { throw new IOException("MidiInputPort is closed"); } - int length = MidiPortImpl.packMessage(msg, offset, count, timestamp, mBuffer); + int length = MidiPortImpl.packData(msg, offset, count, timestamp, mBuffer); + mOutputStream.write(mBuffer, 0, length); + } + } + + @Override + public void flush() throws IOException { + synchronized (mBuffer) { + if (mOutputStream == null) { + throw new IOException("MidiInputPort is closed"); + } + int length = MidiPortImpl.packFlush(mBuffer); mOutputStream.write(mBuffer, 0, length); } } diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java index d62b2dc..0ba1744 100644 --- a/media/java/android/media/midi/MidiManager.java +++ b/media/java/android/media/midi/MidiManager.java @@ -16,6 +16,7 @@ package android.media.midi; +import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -42,6 +43,24 @@ import java.util.HashMap; public final class MidiManager { private static final String TAG = "MidiManager"; + /** + * Intent for starting BluetoothMidiService + * @hide + */ + public static final String BLUETOOTH_MIDI_SERVICE_INTENT = + "android.media.midi.BluetoothMidiService"; + + /** + * BluetoothMidiService package name + */ + private static final String BLUETOOTH_MIDI_SERVICE_PACKAGE = "com.android.bluetoothmidiservice"; + + /** + * BluetoothMidiService class name + */ + private static final String BLUETOOTH_MIDI_SERVICE_CLASS = + "com.android.bluetoothmidiservice.BluetoothMidiService"; + private final Context mContext; private final IMidiManager mService; private final IBinder mToken = new Binder(); @@ -145,6 +164,19 @@ public final class MidiManager { } /** + * Callback class used for receiving the results of {@link #openBluetoothDevice} + */ + abstract public static class BluetoothOpenCallback { + /** + * Called to respond to a {@link #openBluetoothDevice} request + * + * @param bluetoothDevice the {@link android.bluetooth.BluetoothDevice} to open + * @param device a {@link MidiDevice} for opened device, or null if opening failed + */ + abstract public void onDeviceOpened(BluetoothDevice bluetoothDevice, MidiDevice device); + } + + /** * @hide */ public MidiManager(Context context, IMidiManager service) { @@ -214,6 +246,19 @@ public final class MidiManager { } } + private void sendBluetoothDeviceResponse(final BluetoothDevice bluetoothDevice, + final MidiDevice device, final BluetoothOpenCallback callback, Handler handler) { + if (handler != null) { + handler.post(new Runnable() { + @Override public void run() { + callback.onDeviceOpened(bluetoothDevice, device); + } + }); + } else { + callback.onDeviceOpened(bluetoothDevice, device); + } + } + /** * Opens a MIDI device for reading and writing. * @@ -260,7 +305,7 @@ public final class MidiManager { // return immediately to avoid calling sendOpenDeviceResponse below return; } else { - Log.e(TAG, "Unable to bind service: " + intent); + Log.e(TAG, "Unable to bind service: " + intent); } } } else { @@ -272,6 +317,51 @@ public final class MidiManager { sendOpenDeviceResponse(deviceInfo, device, callback, handler); } + /** + * Opens a Bluetooth MIDI device for reading and writing. + * + * @param bluetoothDevice a {@link android.bluetooth.BluetoothDevice} to open as a MIDI device + * @param callback a {@link MidiManager.BluetoothOpenCallback} to be called to receive the + * result + * @param handler the {@link android.os.Handler Handler} that will be used for delivering + * the result. If handler is null, then the thread used for the + * callback is unspecified. + */ + public void openBluetoothDevice(final BluetoothDevice bluetoothDevice, + final BluetoothOpenCallback callback, final Handler handler) { + Intent intent = new Intent(BLUETOOTH_MIDI_SERVICE_INTENT); + intent.setComponent(new ComponentName(BLUETOOTH_MIDI_SERVICE_PACKAGE, + BLUETOOTH_MIDI_SERVICE_CLASS)); + intent.putExtra("device", bluetoothDevice); + if (!mContext.bindService(intent, + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + IMidiDeviceServer server = + IMidiDeviceServer.Stub.asInterface(binder); + try { + // fetch MidiDeviceInfo from the server + MidiDeviceInfo deviceInfo = server.getDeviceInfo(); + MidiDevice device = new MidiDevice(deviceInfo, server, mContext, this); + sendBluetoothDeviceResponse(bluetoothDevice, device, callback, handler); + } catch (RemoteException e) { + Log.e(TAG, "remote exception in onServiceConnected"); + sendBluetoothDeviceResponse(bluetoothDevice, null, callback, handler); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + // FIXME - anything to do here? + } + }, + Context.BIND_AUTO_CREATE)) + { + Log.e(TAG, "Unable to bind service: " + intent); + sendBluetoothDeviceResponse(bluetoothDevice, null, callback, handler); + } + } + /** @hide */ public MidiDeviceServer createDeviceServer(MidiReceiver[] inputPortReceivers, int numOutputPorts, String[] inputPortNames, String[] outputPortNames, diff --git a/media/java/android/media/midi/MidiOutputPort.java b/media/java/android/media/midi/MidiOutputPort.java index 0290a76..7491f3c 100644 --- a/media/java/android/media/midi/MidiOutputPort.java +++ b/media/java/android/media/midi/MidiOutputPort.java @@ -62,12 +62,24 @@ public final class MidiOutputPort extends MidiSender implements Closeable { // FIXME - inform receivers here? } - int offset = MidiPortImpl.getMessageOffset(buffer, count); - int size = MidiPortImpl.getMessageSize(buffer, count); - long timestamp = MidiPortImpl.getMessageTimeStamp(buffer, count); - - // dispatch to all our receivers - mDispatcher.sendWithTimestamp(buffer, offset, size, timestamp); + int packetType = MidiPortImpl.getPacketType(buffer, count); + switch (packetType) { + case MidiPortImpl.PACKET_TYPE_DATA: { + int offset = MidiPortImpl.getDataOffset(buffer, count); + int size = MidiPortImpl.getDataSize(buffer, count); + long timestamp = MidiPortImpl.getPacketTimestamp(buffer, count); + + // dispatch to all our receivers + mDispatcher.sendWithTimestamp(buffer, offset, size, timestamp); + break; + } + case MidiPortImpl.PACKET_TYPE_FLUSH: + mDispatcher.flush(); + break; + default: + Log.e(TAG, "Unknown packet type " + packetType); + break; + } } } catch (IOException e) { // FIXME report I/O failure? diff --git a/media/java/android/media/midi/MidiPortImpl.java b/media/java/android/media/midi/MidiPortImpl.java index 5795045..16fc214 100644 --- a/media/java/android/media/midi/MidiPortImpl.java +++ b/media/java/android/media/midi/MidiPortImpl.java @@ -24,6 +24,16 @@ package android.media.midi; private static final String TAG = "MidiPort"; /** + * Packet type for data packet + */ + public static final int PACKET_TYPE_DATA = 1; + + /** + * Packet type for flush packet + */ + public static final int PACKET_TYPE_FLUSH = 2; + + /** * Maximum size of a packet that can pass through our ParcelFileDescriptor. */ public static final int MAX_PACKET_SIZE = 1024; @@ -34,12 +44,17 @@ package android.media.midi; private static final int TIMESTAMP_SIZE = 8; /** + * Data packet overhead is timestamp size plus packet type byte + */ + private static final int DATA_PACKET_OVERHEAD = TIMESTAMP_SIZE + 1; + + /** * Maximum amount of MIDI data that can be included in a packet */ - public static final int MAX_PACKET_DATA_SIZE = MAX_PACKET_SIZE - TIMESTAMP_SIZE; + public static final int MAX_PACKET_DATA_SIZE = MAX_PACKET_SIZE - DATA_PACKET_OVERHEAD; /** - * Utility function for packing a MIDI message to be sent through our ParcelFileDescriptor + * Utility function for packing MIDI data to be sent through our ParcelFileDescriptor * * message byte array contains variable length MIDI message. * messageSize is size of variable length MIDI message @@ -47,46 +62,65 @@ package android.media.midi; * dest is buffer to pack into * returns size of packed message */ - public static int packMessage(byte[] message, int offset, int size, long timestamp, + public static int packData(byte[] message, int offset, int size, long timestamp, byte[] dest) { - if (size + TIMESTAMP_SIZE > MAX_PACKET_SIZE) { - size = MAX_PACKET_SIZE - TIMESTAMP_SIZE; + if (size > MAX_PACKET_DATA_SIZE) { + size = MAX_PACKET_DATA_SIZE; } - // message data goes first - System.arraycopy(message, offset, dest, 0, size); + int length = 0; + // packet type goes first + dest[length++] = PACKET_TYPE_DATA; + // data goes next + System.arraycopy(message, offset, dest, length, size); + length += size; // followed by timestamp for (int i = 0; i < TIMESTAMP_SIZE; i++) { - dest[size++] = (byte)timestamp; + dest[length++] = (byte)timestamp; timestamp >>= 8; } - return size; + return length; + } + + /** + * Utility function for packing a flush command to be sent through our ParcelFileDescriptor + */ + public static int packFlush(byte[] dest) { + dest[0] = PACKET_TYPE_FLUSH; + return 1; + } + + /** + * Returns the packet type (PACKET_TYPE_DATA or PACKET_TYPE_FLUSH) + */ + public static int getPacketType(byte[] buffer, int bufferLength) { + return buffer[0]; } /** - * Utility function for unpacking a MIDI message received from our ParcelFileDescriptor + * Utility function for unpacking MIDI data received from our ParcelFileDescriptor * returns the offset of the MIDI message in packed buffer */ - public static int getMessageOffset(byte[] buffer, int bufferLength) { - // message is at the beginning - return 0; + public static int getDataOffset(byte[] buffer, int bufferLength) { + // data follows packet type byte + return 1; } /** - * Utility function for unpacking a MIDI message received from our ParcelFileDescriptor + * Utility function for unpacking MIDI data received from our ParcelFileDescriptor * returns size of MIDI data in packed buffer */ - public static int getMessageSize(byte[] buffer, int bufferLength) { + public static int getDataSize(byte[] buffer, int bufferLength) { // message length is total buffer length minus size of the timestamp - return bufferLength - TIMESTAMP_SIZE; + return bufferLength - DATA_PACKET_OVERHEAD; } /** - * Utility function for unpacking a MIDI message received from our ParcelFileDescriptor + * Utility function for unpacking MIDI data received from our ParcelFileDescriptor * unpacks timestamp from packed buffer */ - public static long getMessageTimeStamp(byte[] buffer, int bufferLength) { + public static long getPacketTimestamp(byte[] buffer, int bufferLength) { // timestamp is at end of the packet int offset = bufferLength; long timestamp = 0; diff --git a/media/java/android/media/midi/MidiReceiver.java b/media/java/android/media/midi/MidiReceiver.java index 6f4c266..d069075 100644 --- a/media/java/android/media/midi/MidiReceiver.java +++ b/media/java/android/media/midi/MidiReceiver.java @@ -42,6 +42,13 @@ abstract public class MidiReceiver { throws IOException; /** + * Instructs the receiver to discard all pending events. + * @throws IOException + */ + public void flush() throws IOException { + } + + /** * Returns the maximum size of a message this receiver can receive. * Defaults to {@link java.lang.Integer#MAX_VALUE} unless overridden. * @return maximum message size diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 16758d0..71457b7 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -669,6 +669,14 @@ void JMediaCodec::handleCallback(const sp<AMessage> &msg) { break; } + case MediaCodec::CB_CODEC_RELEASED: + { + if (!msg->findInt32("reason", &arg2)) { + arg2 = MediaCodec::REASON_UNKNOWN; + } + break; + } + default: TRESPASS(); } diff --git a/media/jni/android_media_MediaDrm.cpp b/media/jni/android_media_MediaDrm.cpp index 96d7133..f8146a7 100644 --- a/media/jni/android_media_MediaDrm.cpp +++ b/media/jni/android_media_MediaDrm.cpp @@ -96,6 +96,12 @@ struct EventTypes { jint kEventSessionReclaimed; } gEventTypes; +struct EventWhat { + jint kWhatDrmEvent; + jint kWhatExpirationUpdate; + jint kWhatKeysChange; +} gEventWhat; + struct KeyTypes { jint kKeyTypeStreaming; jint kKeyTypeOffline; @@ -186,25 +192,37 @@ JNIDrmListener::~JNIDrmListener() void JNIDrmListener::notify(DrmPlugin::EventType eventType, int extra, const Parcel *obj) { - jint jeventType; + jint jwhat; + jint jeventType = 0; // translate DrmPlugin event types into their java equivalents switch (eventType) { case DrmPlugin::kDrmPluginEventProvisionRequired: + jwhat = gEventWhat.kWhatDrmEvent; jeventType = gEventTypes.kEventProvisionRequired; break; case DrmPlugin::kDrmPluginEventKeyNeeded: + jwhat = gEventWhat.kWhatDrmEvent; jeventType = gEventTypes.kEventKeyRequired; break; case DrmPlugin::kDrmPluginEventKeyExpired: + jwhat = gEventWhat.kWhatDrmEvent; jeventType = gEventTypes.kEventKeyExpired; break; case DrmPlugin::kDrmPluginEventVendorDefined: + jwhat = gEventWhat.kWhatDrmEvent; jeventType = gEventTypes.kEventVendorDefined; break; case DrmPlugin::kDrmPluginEventSessionReclaimed: + jwhat = gEventWhat.kWhatDrmEvent; jeventType = gEventTypes.kEventSessionReclaimed; break; + case DrmPlugin::kDrmPluginEventExpirationUpdate: + jwhat = gEventWhat.kWhatExpirationUpdate; + break; + case DrmPlugin::kDrmPluginEventKeysChange: + jwhat = gEventWhat.kWhatKeysChange; + break; default: ALOGE("Invalid event DrmPlugin::EventType %d, ignored", (int)eventType); return; @@ -217,7 +235,7 @@ void JNIDrmListener::notify(DrmPlugin::EventType eventType, int extra, Parcel* nativeParcel = parcelForJavaObject(env, jParcel); nativeParcel->setData(obj->data(), obj->dataSize()); env->CallStaticVoidMethod(mClass, gFields.post_event, mObject, - jeventType, extra, jParcel); + jwhat, jeventType, extra, jParcel); env->DeleteLocalRef(jParcel); } } @@ -573,7 +591,7 @@ static void android_media_MediaDrm_native_init(JNIEnv *env) { FIND_CLASS(clazz, "android/media/MediaDrm"); GET_FIELD_ID(gFields.context, clazz, "mNativeContext", "J"); GET_STATIC_METHOD_ID(gFields.post_event, clazz, "postEventFromNative", - "(Ljava/lang/Object;IILjava/lang/Object;)V"); + "(Ljava/lang/Object;IIILjava/lang/Object;)V"); jfieldID field; GET_STATIC_FIELD_ID(field, clazz, "EVENT_PROVISION_REQUIRED", "I"); @@ -587,6 +605,13 @@ static void android_media_MediaDrm_native_init(JNIEnv *env) { GET_STATIC_FIELD_ID(field, clazz, "EVENT_SESSION_RECLAIMED", "I"); gEventTypes.kEventSessionReclaimed = env->GetStaticIntField(clazz, field); + GET_STATIC_FIELD_ID(field, clazz, "DRM_EVENT", "I"); + gEventWhat.kWhatDrmEvent = env->GetStaticIntField(clazz, field); + GET_STATIC_FIELD_ID(field, clazz, "EXPIRATION_UPDATE", "I"); + gEventWhat.kWhatExpirationUpdate = env->GetStaticIntField(clazz, field); + GET_STATIC_FIELD_ID(field, clazz, "KEYS_CHANGE", "I"); + gEventWhat.kWhatKeysChange = env->GetStaticIntField(clazz, field); + GET_STATIC_FIELD_ID(field, clazz, "KEY_TYPE_STREAMING", "I"); gKeyTypes.kKeyTypeStreaming = env->GetStaticIntField(clazz, field); GET_STATIC_FIELD_ID(field, clazz, "KEY_TYPE_OFFLINE", "I"); @@ -837,7 +862,7 @@ static jobject android_media_MediaDrm_getKeyRequest( env->SetIntField(keyObj, gFields.keyRequest.requestType, gKeyRequestTypes.kKeyRequestTypeRelease); break; - case DrmPlugin::kKeyRequestType_Unknown: + default: throwStateException(env, "DRM plugin failure: unknown key request type", ERROR_DRM_UNKNOWN); break; diff --git a/media/packages/BluetoothMidiService/Android.mk b/media/packages/BluetoothMidiService/Android.mk new file mode 100644 index 0000000..2c9c3c5 --- /dev/null +++ b/media/packages/BluetoothMidiService/Android.mk @@ -0,0 +1,11 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := BluetoothMidiService +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) diff --git a/media/packages/BluetoothMidiService/AndroidManifest.xml b/media/packages/BluetoothMidiService/AndroidManifest.xml new file mode 100644 index 0000000..15aa581 --- /dev/null +++ b/media/packages/BluetoothMidiService/AndroidManifest.xml @@ -0,0 +1,17 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.bluetoothmidiservice" + > + + <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> + <uses-feature android:name="android.software.midi" android:required="true"/> + <uses-permission android:name="android.permission.BLUETOOTH"/> + + <application + android:label="@string/app_name"> + <service android:name="BluetoothMidiService"> + <intent-filter> + <action android:name="android.media.midi.BluetoothMidiService" /> + </intent-filter> + </service> + </application> +</manifest> diff --git a/media/packages/BluetoothMidiService/res/values/strings.xml b/media/packages/BluetoothMidiService/res/values/strings.xml new file mode 100644 index 0000000..c98e56c --- /dev/null +++ b/media/packages/BluetoothMidiService/res/values/strings.xml @@ -0,0 +1,19 @@ +<?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. +--> + +<resources> + <string name="app_name">Bluetooth MIDI Service</string> +</resources> diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java new file mode 100644 index 0000000..8d194e5 --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java @@ -0,0 +1,276 @@ +/* + * 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.bluetoothmidiservice; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.media.midi.MidiReceiver; +import android.media.midi.MidiManager; +import android.media.midi.MidiDeviceServer; +import android.media.midi.MidiDeviceInfo; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; + +import com.android.internal.midi.MidiEventScheduler; +import com.android.internal.midi.MidiEventScheduler.MidiEvent; + +import libcore.io.IoUtils; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +/** + * Class used to implement a Bluetooth MIDI device. + */ +public final class BluetoothMidiDevice { + + private static final String TAG = "BluetoothMidiDevice"; + + private static final int MAX_PACKET_SIZE = 20; + + // Bluetooth MIDI Gatt service UUID + private static final UUID MIDI_SERVICE = UUID.fromString( + "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"); + // Bluetooth MIDI Gatt characteristic UUID + private static final UUID MIDI_CHARACTERISTIC = UUID.fromString( + "7772E5DB-3868-4112-A1A9-F2669D106BF3"); + // Descriptor UUID for enabling characteristic changed notifications + private static final UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString( + "00002902-0000-1000-8000-00805f9b34fb"); + + private final BluetoothDevice mBluetoothDevice; + private final BluetoothMidiService mService; + private final MidiManager mMidiManager; + private MidiReceiver mOutputReceiver; + private final MidiEventScheduler mEventScheduler = new MidiEventScheduler(); + + private MidiDeviceServer mDeviceServer; + private BluetoothGatt mBluetoothGatt; + + private BluetoothGattCharacteristic mCharacteristic; + + // PacketReceiver for receiving formatted packets from our BluetoothPacketEncoder + private final PacketReceiver mPacketReceiver = new PacketReceiver(); + + private final BluetoothPacketEncoder mPacketEncoder + = new BluetoothPacketEncoder(mPacketReceiver, MAX_PACKET_SIZE); + + private final BluetoothPacketDecoder mPacketDecoder + = new BluetoothPacketDecoder(MAX_PACKET_SIZE); + + private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, + int newState) { + String intentAction; + if (newState == BluetoothProfile.STATE_CONNECTED) { + Log.i(TAG, "Connected to GATT server."); + Log.i(TAG, "Attempting to start service discovery:" + + mBluetoothGatt.discoverServices()); + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + Log.i(TAG, "Disconnected from GATT server."); + // FIXME synchronize? + close(); + } + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + List<BluetoothGattService> services = mBluetoothGatt.getServices(); + for (BluetoothGattService service : services) { + if (MIDI_SERVICE.equals(service.getUuid())) { + Log.d(TAG, "found MIDI_SERVICE"); + List<BluetoothGattCharacteristic> characteristics + = service.getCharacteristics(); + for (BluetoothGattCharacteristic characteristic : characteristics) { + if (MIDI_CHARACTERISTIC.equals(characteristic.getUuid())) { + Log.d(TAG, "found MIDI_CHARACTERISTIC"); + mCharacteristic = characteristic; + + // Specification says to read the characteristic first and then + // switch to receiving notifications + mBluetoothGatt.readCharacteristic(characteristic); + break; + } + } + break; + } + } + } else { + Log.w(TAG, "onServicesDiscovered received: " + status); + // FIXME - report error back to client? + } + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + Log.d(TAG, "onCharacteristicRead " + status); + + // switch to receiving notifications after initial characteristic read + mBluetoothGatt.setCharacteristicNotification(characteristic, true); + + BluetoothGattDescriptor descriptor = characteristic.getDescriptor( + CLIENT_CHARACTERISTIC_CONFIG); + // FIXME null check + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + mBluetoothGatt.writeDescriptor(descriptor); + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + Log.d(TAG, "onCharacteristicWrite " + status); + mPacketEncoder.writeComplete(); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { +// logByteArray("Received ", characteristic.getValue(), 0, +// characteristic.getValue().length); + mPacketDecoder.decodePacket(characteristic.getValue(), mOutputReceiver); + } + }; + + // This receives MIDI data that has already been passed through our MidiEventScheduler + // and has been normalized by our MidiFramer. + + private class PacketReceiver implements PacketEncoder.PacketReceiver { + // buffers of every possible packet size + private final byte[][] mWriteBuffers; + + public PacketReceiver() { + // Create buffers of every possible packet size + mWriteBuffers = new byte[MAX_PACKET_SIZE + 1][]; + for (int i = 0; i <= MAX_PACKET_SIZE; i++) { + mWriteBuffers[i] = new byte[i]; + } + } + + @Override + public void writePacket(byte[] buffer, int count) { + if (mCharacteristic == null) { + Log.w(TAG, "not ready to send packet yet"); + return; + } + byte[] writeBuffer = mWriteBuffers[count]; + System.arraycopy(buffer, 0, writeBuffer, 0, count); + mCharacteristic.setValue(writeBuffer); +// logByteArray("Sent ", mCharacteristic.getValue(), 0, +// mCharacteristic.getValue().length); + mBluetoothGatt.writeCharacteristic(mCharacteristic); + } + } + + public BluetoothMidiDevice(Context context, BluetoothDevice device, + BluetoothMidiService service) { + mBluetoothDevice = device; + mService = service; + + mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, mGattCallback); + + mMidiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE); + + Bundle properties = new Bundle(); + properties.putString(MidiDeviceInfo.PROPERTY_NAME, mBluetoothGatt.getDevice().getName()); + properties.putParcelable(MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE, + mBluetoothGatt.getDevice()); + + MidiReceiver[] inputPortReceivers = new MidiReceiver[1]; + inputPortReceivers[0] = mEventScheduler.getReceiver(); + + mDeviceServer = mMidiManager.createDeviceServer(inputPortReceivers, 1, + null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, null); + + mOutputReceiver = mDeviceServer.getOutputPortReceivers()[0]; + + // This thread waits for outgoing messages from our MidiEventScheduler + // And forwards them to our MidiFramer to be prepared to send via Bluetooth. + new Thread("BluetoothMidiDevice " + mBluetoothDevice) { + @Override + public void run() { + while (true) { + MidiEvent event; + try { + event = (MidiEvent)mEventScheduler.waitNextEvent(); + } catch (InterruptedException e) { + // try again + continue; + } + if (event == null) { + break; + } + try { + mPacketEncoder.sendWithTimestamp(event.data, 0, event.count, + event.getTimestamp()); + } catch (IOException e) { + Log.e(TAG, "mPacketAccumulator.sendWithTimestamp failed", e); + } + mEventScheduler.addEventToPool(event); + } + Log.d(TAG, "BluetoothMidiDevice thread exit"); + } + }.start(); + } + + void close() { + mEventScheduler.close(); + if (mDeviceServer != null) { + IoUtils.closeQuietly(mDeviceServer); + mDeviceServer = null; + mService.deviceClosed(mBluetoothDevice); + } + if (mBluetoothGatt != null) { + mBluetoothGatt.close(); + mBluetoothGatt = null; + } + } + + public IBinder getBinder() { + return mDeviceServer.asBinder(); + } + + private static void logByteArray(String prefix, byte[] value, int offset, int count) { + StringBuilder builder = new StringBuilder(prefix); + for (int i = offset; i < count; i++) { + String hex = Integer.toHexString(value[i]); + int length = hex.length(); + if (length == 1) { + hex = "0x" + hex; + } else { + hex = hex.substring(length - 2, length); + } + builder.append(hex); + if (i != value.length - 1) { + builder.append(", "); + } + } + Log.d(TAG, builder.toString()); + } +} diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiService.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiService.java new file mode 100644 index 0000000..fbde2b4 --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiService.java @@ -0,0 +1,61 @@ +/* + * 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.bluetoothmidiservice; + +import android.app.Service; +import android.bluetooth.BluetoothDevice; +import android.content.Intent; +import android.media.midi.MidiManager; +import android.os.IBinder; +import android.util.Log; + +import java.util.HashMap; + +public class BluetoothMidiService extends Service { + private static final String TAG = "BluetoothMidiService"; + + // BluetoothMidiDevices keyed by BluetoothDevice + private final HashMap<BluetoothDevice,BluetoothMidiDevice> mDeviceServerMap + = new HashMap<BluetoothDevice,BluetoothMidiDevice>(); + + @Override + public IBinder onBind(Intent intent) { + if (MidiManager.BLUETOOTH_MIDI_SERVICE_INTENT.equals(intent.getAction())) { + BluetoothDevice bluetoothDevice = (BluetoothDevice)intent.getParcelableExtra("device"); + if (bluetoothDevice == null) { + Log.e(TAG, "no BluetoothDevice in onBind intent"); + return null; + } + + BluetoothMidiDevice device; + synchronized (mDeviceServerMap) { + device = mDeviceServerMap.get(bluetoothDevice); + if (device == null) { + device = new BluetoothMidiDevice(this, bluetoothDevice, this); + } + } + return device.getBinder(); + } + return null; + } + + void deviceClosed(BluetoothDevice device) { + synchronized (mDeviceServerMap) { + mDeviceServerMap.remove(device); + } + } +} diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketDecoder.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketDecoder.java new file mode 100644 index 0000000..c5bfb5f --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketDecoder.java @@ -0,0 +1,106 @@ +/* + * 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.bluetoothmidiservice; + +import android.media.midi.MidiReceiver; +import android.util.Log; + +import java.io.IOException; + +/** + * This is an abstract base class that decodes a packet buffer and passes it to a + * {@link android.media.midi.MidiReceiver} + */ +public class BluetoothPacketDecoder extends PacketDecoder { + + private static final String TAG = "BluetoothPacketDecoder"; + + private final byte[] mBuffer; + + private final int TIMESTAMP_MASK_HIGH = 0x1F80; + private final int TIMESTAMP_MASK_LOW = 0x7F; + private final int HEADER_TIMESTAMP_MASK = 0x3F; + + public BluetoothPacketDecoder(int maxPacketSize) { + mBuffer = new byte[maxPacketSize]; + } + + @Override + public void decodePacket(byte[] buffer, MidiReceiver receiver) { + int length = buffer.length; + + // NOTE his code allows running status across packets, + // although the specification does not allow that. + + if (length < 1) { + Log.e(TAG, "empty packet"); + return; + } + byte header = buffer[0]; + if ((header & 0xC0) != 0x80) { + Log.e(TAG, "packet does not start with header"); + return; + } + + // shift bits 0 - 5 to bits 7 - 12 + int timestamp = (header & HEADER_TIMESTAMP_MASK) << 7; + boolean lastWasTimestamp = false; + int dataCount = 0; + int previousLowTimestamp = 0; + + // iterate through the rest of the packet, separating MIDI data from timestamps + for (int i = 1; i < buffer.length; i++) { + byte b = buffer[i]; + + if ((b & 0x80) != 0 && !lastWasTimestamp) { + lastWasTimestamp = true; + int lowTimestamp = b & TIMESTAMP_MASK_LOW; + int newTimestamp = (timestamp & TIMESTAMP_MASK_HIGH) | lowTimestamp; + if (lowTimestamp < previousLowTimestamp) { + newTimestamp = (newTimestamp + 0x0080) & TIMESTAMP_MASK_HIGH; + } + previousLowTimestamp = lowTimestamp; + + if (newTimestamp != timestamp) { + if (dataCount > 0) { + // send previous message separately since it has a different timestamp + try { + // FIXME use sendWithTimestamp + receiver.send(mBuffer, 0, dataCount); + } catch (IOException e) { + // ??? + } + dataCount = 0; + } + } + timestamp = newTimestamp; + } else { + lastWasTimestamp = false; + mBuffer[dataCount++] = b; + } + } + + if (dataCount > 0) { + try { + // FIXME use sendWithTimestamp + receiver.send(mBuffer, 0, dataCount); + } catch (IOException e) { + // ??? + } + } + } +} diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketEncoder.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketEncoder.java new file mode 100644 index 0000000..463edcf --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketEncoder.java @@ -0,0 +1,157 @@ +/* + * 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.bluetoothmidiservice; + +import android.media.midi.MidiReceiver; + +import com.android.internal.midi.MidiConstants; +import com.android.internal.midi.MidiFramer; + +import java.io.IOException; + +/** + * This class accumulates MIDI messages to form a MIDI packet. + */ +public class BluetoothPacketEncoder extends PacketEncoder { + + private static final String TAG = "BluetoothPacketEncoder"; + + private static final long MILLISECOND_NANOS = 1000000L; + + // mask for generating 13 bit timestamps + private static final int MILLISECOND_MASK = 0x1FFF; + + private final PacketReceiver mPacketReceiver; + + // buffer for accumulating messages to write + private final byte[] mAccumulationBuffer; + // number of bytes currently in mAccumulationBuffer + private int mAccumulatedBytes; + // timestamp for first message in current packet + private int mPacketTimestamp; + // current running status, or zero if none + private int mRunningStatus; + + private boolean mWritePending; + + private final Object mLock = new Object(); + + // This receives normalized data from mMidiFramer and accumulates it into a packet buffer + private final MidiReceiver mFramedDataReceiver = new MidiReceiver() { + @Override + public void onReceive(byte[] msg, int offset, int count, long timestamp) + throws IOException { + + int milliTimestamp = (int)(timestamp / MILLISECOND_NANOS) & MILLISECOND_MASK; + int status = msg[0] & 0xFF; + + synchronized (mLock) { + boolean needsTimestamp = (milliTimestamp != mPacketTimestamp); + int bytesNeeded = count; + if (needsTimestamp) bytesNeeded++; // add one for timestamp byte + if (status == mRunningStatus) bytesNeeded--; // subtract one for status byte + + if (mAccumulatedBytes + bytesNeeded > mAccumulationBuffer.length) { + // write out our data if there is no more room + // if necessary, block until previous packet is sent + flushLocked(true); + } + + // write header if we are starting a new packet + if (mAccumulatedBytes == 0) { + // header byte with timestamp bits 7 - 12 + mAccumulationBuffer[mAccumulatedBytes++] = (byte)(0x80 | (milliTimestamp >> 7)); + mPacketTimestamp = milliTimestamp; + needsTimestamp = true; + } + + // write new timestamp byte and status byte if necessary + if (needsTimestamp) { + // timestamp byte with bits 0 - 6 of timestamp + mAccumulationBuffer[mAccumulatedBytes++] = + (byte)(0x80 | (milliTimestamp & 0x7F)); + mPacketTimestamp = milliTimestamp; + } + + if (status != mRunningStatus) { + mAccumulationBuffer[mAccumulatedBytes++] = (byte)status; + if (MidiConstants.allowRunningStatus(status)) { + mRunningStatus = status; + } else if (MidiConstants.allowRunningStatus(status)) { + mRunningStatus = 0; + } + } + + // now copy data bytes + int dataLength = count - 1; + System.arraycopy(msg, 1, mAccumulationBuffer, mAccumulatedBytes, dataLength); + // FIXME - handle long SysEx properly + mAccumulatedBytes += dataLength; + + // write the packet if possible, but do not block + flushLocked(false); + } + } + }; + + // MidiFramer for normalizing incoming data + private final MidiFramer mMidiFramer = new MidiFramer(mFramedDataReceiver); + + public BluetoothPacketEncoder(PacketReceiver packetReceiver, int maxPacketSize) { + mPacketReceiver = packetReceiver; + mAccumulationBuffer = new byte[maxPacketSize]; + } + + @Override + public void onReceive(byte[] msg, int offset, int count, long timestamp) + throws IOException { + // normalize the data by passing it through a MidiFramer first + mMidiFramer.sendWithTimestamp(msg, offset, count, timestamp); + } + + @Override + public void writeComplete() { + synchronized (mLock) { + mWritePending = false; + flushLocked(false); + mLock.notify(); + } + } + + private void flushLocked(boolean canBlock) { + if (mWritePending && !canBlock) { + return; + } + + while (mWritePending && mAccumulatedBytes > 0) { + try { + mLock.wait(); + } catch (InterruptedException e) { + // try again + continue; + } + } + + if (mAccumulatedBytes > 0) { + mPacketReceiver.writePacket(mAccumulationBuffer, mAccumulatedBytes); + mAccumulatedBytes = 0; + mPacketTimestamp = 0; + mRunningStatus = 0; + mWritePending = true; + } + } +} diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketDecoder.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketDecoder.java new file mode 100644 index 0000000..da4b63a --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketDecoder.java @@ -0,0 +1,33 @@ +/* + * 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.bluetoothmidiservice; + +import android.media.midi.MidiReceiver; + +/** + * This is an abstract base class that decodes a packet buffer and passes it to a + * {@link android.media.midi.MidiReceiver} + */ +public abstract class PacketDecoder { + + /** + * Decodes MIDI data in a packet and passes it to a {@link android.media.midi.MidiReceiver} + * @param buffer the packet to decode + * @param receiver the {@link android.media.midi.MidiReceiver} to receive the decoded MIDI data + */ + abstract public void decodePacket(byte[] buffer, MidiReceiver receiver); +} diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketEncoder.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketEncoder.java new file mode 100644 index 0000000..12c8b9b --- /dev/null +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketEncoder.java @@ -0,0 +1,41 @@ +/* + * 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.bluetoothmidiservice; + +import android.media.midi.MidiReceiver; + +/** + * This is an abstract base class that encodes MIDI data into a packet buffer. + * PacketEncoder receives data via its {@link android.media.midi.MidiReceiver#onReceive} method + * and notifies its client of packets to write via the {@link PacketEncoder.PacketReceiver} + * interface. + */ +public abstract class PacketEncoder extends MidiReceiver { + + public interface PacketReceiver { + /** Called to write an accumulated packet. + * @param buffer the packet buffer to write + * @param count the number of bytes in the packet buffer to write + */ + public void writePacket(byte[] buffer, int count); + } + + /** + * Called to inform PacketEncoder when the previous write is complete. + */ + abstract public void writeComplete(); +} diff --git a/packages/StatementService/Android.mk b/packages/StatementService/Android.mk index f0adb1c..470d824 100644 --- a/packages/StatementService/Android.mk +++ b/packages/StatementService/Android.mk @@ -24,6 +24,8 @@ LOCAL_PROGUARD_FLAG_FILES := proguard.flags LOCAL_PACKAGE_NAME := StatementService LOCAL_PRIVILEGED_MODULE := true +LOCAL_JAVA_LIBRARIES += org.apache.http.legacy + LOCAL_STATIC_JAVA_LIBRARIES := \ libprotobuf-java-nano \ volley diff --git a/packages/SystemUI/res/drawable/ic_volume_bt.xml b/packages/SystemUI/res/drawable/ic_volume_media_bt.xml index bce407a..3364d9c 100644 --- a/packages/SystemUI/res/drawable/ic_volume_bt.xml +++ b/packages/SystemUI/res/drawable/ic_volume_media_bt.xml @@ -14,13 +14,16 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:height="24dp" - android:viewportHeight="48.0" - android:viewportWidth="48.0" - android:width="24dp" > + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> <path android:fillColor="@color/volume_icon_color" - android:pathData="M35.4,15.4L24.0,4.0l-2.0,0.0l0.0,15.2L12.8,10.0L10.0,12.8L21.2,24.0L10.0,35.2l2.8,2.8l9.2,-9.2L22.0,44.0l2.0,0.0l11.4,-11.4L26.8,24.0L35.4,15.4zM26.0,11.7l3.8,3.8L26.0,19.2L26.0,11.7zM29.8,32.6L26.0,36.3l0.0,-7.5L29.8,32.6z" /> + android:pathData="M17.0,3.0l-7.0,0.0l0.0,9.3C9.5,12.1 9.0,12.0 8.5,12.0C6.0,12.0 4.0,14.0 4.0,16.5S6.0,21.0 8.5,21.0s4.5,-2.3 4.5,-4.5C13.0,14.7 13.0,6.0 13.0,6.0l4.0,0.0L17.0,3.0z"/> + <path + android:fillColor="@color/volume_icon_color" + android:pathData="M23.4,9.9L20.5,7.0L20.0,7.0l0.0,3.8l-2.3,-2.3L17.0,9.2l2.8,2.8L17.0,14.8l0.7,0.7l2.3,-2.3L20.0,17.0l0.5,0.0l2.8,-2.8L21.2,12.0L23.4,9.9zM21.0,8.9l0.9,0.9l-0.9,1.0L21.0,8.9zM21.9,14.2L21.0,15.1l0.0,-1.9L21.9,14.2z"/> -</vector>
\ No newline at end of file +</vector> diff --git a/packages/SystemUI/res/drawable/ic_volume_bt_mute.xml b/packages/SystemUI/res/drawable/ic_volume_media_bt_mute.xml index 98a8137..39f54f1 100644 --- a/packages/SystemUI/res/drawable/ic_volume_bt_mute.xml +++ b/packages/SystemUI/res/drawable/ic_volume_media_bt_mute.xml @@ -15,12 +15,18 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" - android:viewportHeight="48.0" - android:viewportWidth="48.0" + android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="24dp" > <path android:fillColor="@color/volume_icon_color" - android:pathData="M26.0,11.8l3.8,3.8l-3.2,3.2l2.8,2.8l6.0,-6.0L24.0,4.2l-2.0,0.0l0.0,10.1l4.0,4.0L26.0,11.8zM10.8,8.2L8.0,11.0l13.2,13.2L10.0,35.3l2.8,2.8L22.0,29.0l0.0,15.2l2.0,0.0l8.6,-8.6l4.6,4.6l2.8,-2.8L10.8,8.2zM26.0,36.5L26.0,29.0l3.8,3.8L26.0,36.5z" /> + android:pathData="M13.0,6.0l4.0,0.0L17.0,3.0l-7.0,0.0l0.0,5.6l3.0,3.0C13.0,8.8 13.0,6.0 13.0,6.0z"/> + <path + android:fillColor="@color/volume_icon_color" + android:pathData="M2.1,5.7L8.4,12.0C6.0,12.1 4.0,14.0 4.0,16.5S6.0,21.0 8.5,21.0c2.7,0.0 4.5,-2.3 4.5,-4.3l0.0,-0.1l3.9,3.9l1.3,-1.3L3.4,4.5L2.1,5.7z"/> + <path + android:fillColor="@color/volume_icon_color" + android:pathData="M23.4,9.9L20.5,7.0L20.0,7.0l0.0,3.8l-2.3,-2.3L17.0,9.2l2.8,2.8L17.0,14.8l0.7,0.7l2.3,-2.3L20.0,17.0l0.5,0.0l2.8,-2.8L21.2,12.0L23.4,9.9zM21.0,8.9l0.9,0.9l-0.9,1.0L21.0,8.9zM21.9,14.2L21.0,15.1l0.0,-1.9L21.9,14.2z"/> </vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 3705157..779b55e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -981,5 +981,5 @@ <string name="volumeui_notification_text">Touch to restore the original.</string> <!-- Volume dialog zen toggle switch title --> - <string name="volume_zen_switch_text">Block interruptions</string> + <string name="volume_zen_switch_text">@*android:string/zen_mode_feature_name</string> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java index 64730c2..8aa0d7a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java @@ -85,9 +85,9 @@ public class DndTile extends QSTile<QSTile.BooleanState> { @Override public void handleClick() { if (mState.value) { - mController.setZen(Global.ZEN_MODE_OFF); + mController.setZen(Global.ZEN_MODE_OFF, null, TAG); } else { - mController.setZen(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); + mController.setZen(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG); showDetail(true); } } @@ -199,7 +199,7 @@ public class DndTile extends QSTile<QSTile.BooleanState> { @Override public void setToggleState(boolean state) { if (!state) { - mController.setZen(Global.ZEN_MODE_OFF); + mController.setZen(Global.ZEN_MODE_OFF, null, TAG); showDetail(false); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java index 0e21457..67cc788 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java @@ -17,16 +17,19 @@ package com.android.systemui.statusbar.policy; import android.content.ComponentName; +import android.net.Uri; import android.service.notification.Condition; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ZenRule; public interface ZenModeController { void addCallback(Callback callback); void removeCallback(Callback callback); - void setZen(int zen); + void setZen(int zen, Uri conditionId, String reason); int getZen(); void requestConditions(boolean request); - void setExitCondition(Condition exitCondition); - Condition getExitCondition(); + ZenRule getManualRule(); + ZenModeConfig getConfig(); long getNextAlarm(); void setUserId(int userId); boolean isZenAvailable(); @@ -35,10 +38,11 @@ public interface ZenModeController { public static class Callback { public void onZenChanged(int zen) {} - public void onExitConditionChanged(Condition exitCondition) {} public void onConditionsChanged(Condition[] conditions) {} public void onNextAlarmChanged() {} public void onZenAvailableChanged(boolean available) {} public void onEffectsSupressorChanged() {} + public void onManualRuleChanged(ZenRule rule) {} + public void onConfigChanged(ZenModeConfig config) {} } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java index bea0c86..830a197 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java @@ -33,6 +33,7 @@ import android.provider.Settings.Secure; import android.service.notification.Condition; import android.service.notification.IConditionListener; import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ZenRule; import android.util.Log; import android.util.Slog; @@ -40,6 +41,7 @@ import com.android.systemui.qs.GlobalSetting; import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.Objects; /** Platform implementation of the zen mode controller. **/ public class ZenModeControllerImpl implements ZenModeController { @@ -58,6 +60,7 @@ public class ZenModeControllerImpl implements ZenModeController { private int mUserId; private boolean mRequesting; private boolean mRegistered; + private ZenModeConfig mConfig; public ZenModeControllerImpl(Context context, Handler handler) { mContext = context; @@ -70,12 +73,13 @@ public class ZenModeControllerImpl implements ZenModeController { mConfigSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE_CONFIG_ETAG) { @Override protected void handleValueChanged(int value) { - fireExitConditionChanged(); + updateZenModeConfig(); } }; + mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + mConfig = mNoMan.getZenModeConfig(); mModeSetting.setListening(true); mConfigSetting.setListening(true); - mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mSetupObserver = new SetupObserver(handler); mSetupObserver.register(); @@ -97,8 +101,8 @@ public class ZenModeControllerImpl implements ZenModeController { } @Override - public void setZen(int zen) { - mModeSetting.setValue(zen); + public void setZen(int zen, Uri conditionId, String reason) { + mNoMan.setZenMode(zen, conditionId, reason); } @Override @@ -116,13 +120,13 @@ public class ZenModeControllerImpl implements ZenModeController { } @Override - public void setExitCondition(Condition exitCondition) { - mNoMan.setZenModeCondition(exitCondition); + public ZenRule getManualRule() { + return mConfig == null ? null : mConfig.manualRule; } @Override - public Condition getExitCondition() { - return mNoMan.getZenModeCondition(); + public ZenModeConfig getConfig() { + return mConfig; } @Override @@ -185,11 +189,15 @@ public class ZenModeControllerImpl implements ZenModeController { } } - private void fireExitConditionChanged() { - final Condition exitCondition = getExitCondition(); - if (DEBUG) Slog.d(TAG, "exitCondition changed: " + exitCondition); + private void fireManualRuleChanged(ZenRule rule) { + for (Callback cb : mCallbacks) { + cb.onManualRuleChanged(rule); + } + } + + private void fireConfigChanged(ZenModeConfig config) { for (Callback cb : mCallbacks) { - cb.onExitConditionChanged(exitCondition); + cb.onConfigChanged(config); } } @@ -203,6 +211,17 @@ public class ZenModeControllerImpl implements ZenModeController { mConditions.values().toArray(new Condition[mConditions.values().size()])); } + private void updateZenModeConfig() { + final ZenModeConfig config = mNoMan.getZenModeConfig(); + if (Objects.equals(config, mConfig)) return; + final ZenRule oldRule = mConfig != null ? mConfig.manualRule : null; + mConfig = config; + fireConfigChanged(config); + final ZenRule newRule = config != null ? config.manualRule : null; + if (Objects.equals(oldRule, newRule)) return; + fireManualRuleChanged(newRule); + } + private final IConditionListener mListener = new IConditionListener.Stub() { @Override public void onConditionsReceived(Condition[] conditions) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/Util.java b/packages/SystemUI/src/com/android/systemui/volume/Util.java index 78baf67..216a4da 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/Util.java +++ b/packages/SystemUI/src/com/android/systemui/volume/Util.java @@ -21,7 +21,6 @@ import android.media.MediaMetadata; import android.media.VolumeProvider; import android.media.session.MediaController.PlaybackInfo; import android.media.session.PlaybackState; -import android.service.notification.ZenModeConfig.DowntimeInfo; import android.view.View; import android.widget.TextView; @@ -145,10 +144,6 @@ class Util { return HMMAA.format(new Date(millis)); } - public static String getShortTime(DowntimeInfo info) { - return ((info.endHour + 1) % 12) + ":" + (info.endMinute < 10 ? " " : "") + info.endMinute; - } - public static void setText(TextView tv, CharSequence text) { if (Objects.equals(tv.getText(), text)) return; tv.setText(text); diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java index d8b3965..539fec8 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java @@ -38,7 +38,6 @@ import android.os.Message; import android.os.SystemClock; import android.provider.Settings.Global; import android.service.notification.ZenModeConfig; -import android.service.notification.ZenModeConfig.DowntimeInfo; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseBooleanArray; @@ -606,14 +605,6 @@ public class VolumeDialog { text = mContext.getString(R.string.volume_dnd_ends_at, Util.getShortTime(countdown)); action = mContext.getString(R.string.volume_end_now); - } else { - final DowntimeInfo info = ZenModeConfig.tryParseDowntimeConditionId(mState. - exitCondition.id); - if (info != null) { - text = mContext.getString(R.string.volume_dnd_ends_at, - Util.getShortTime(info)); - action = mContext.getString(R.string.volume_end_now); - } } } if (text == null) { @@ -700,7 +691,8 @@ public class VolumeDialog { final int iconRes = isRingVibrate ? R.drawable.ic_volume_ringer_vibrate : ss.routedToBluetooth ? - (ss.muted ? R.drawable.ic_volume_bt_mute : R.drawable.ic_volume_bt) + (ss.muted ? R.drawable.ic_volume_media_bt_mute + : R.drawable.ic_volume_media_bt) : mAutomute && ss.level == 0 ? row.iconMuteRes : (ss.muted ? row.iconMuteRes : row.iconRes); if (iconRes != row.cachedIconRes) { @@ -712,9 +704,9 @@ public class VolumeDialog { } row.iconState = iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE - : (iconRes == R.drawable.ic_volume_bt_mute || iconRes == row.iconMuteRes) + : (iconRes == R.drawable.ic_volume_media_bt_mute || iconRes == row.iconMuteRes) ? Events.ICON_STATE_MUTE - : (iconRes == R.drawable.ic_volume_bt || iconRes == row.iconRes) + : (iconRes == R.drawable.ic_volume_media_bt || iconRes == row.iconRes) ? Events.ICON_STATE_UNMUTE : Events.ICON_STATE_UNKNOWN; diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java index 265e2c6..5bc8c3e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java @@ -41,6 +41,7 @@ import android.os.RemoteException; import android.os.Vibrator; import android.provider.Settings; import android.service.notification.Condition; +import android.service.notification.ZenModeConfig; import android.util.Log; import android.util.SparseArray; @@ -393,8 +394,15 @@ public class VolumeDialogController { return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION; } + private Condition getExitCondition() { + final ZenModeConfig config = mNoMan.getZenModeConfig(); + return config == null ? null + : config.manualRule == null ? null + : config.manualRule.condition; + } + private boolean updateExitConditionW() { - final Condition exitCondition = mNoMan.getZenModeCondition(); + final Condition exitCondition = getExitCondition(); if (Objects.equals(mState.exitCondition, exitCondition)) return false; mState.exitCondition = exitCondition; return true; @@ -476,12 +484,12 @@ public class VolumeDialogController { } private void onSetExitConditionW(Condition condition) { - mNoMan.setZenModeCondition(condition); + mNoMan.setZenMode(mState.zenMode, condition != null ? condition.id : null, TAG); } private void onSetZenModeW(int mode) { if (D.BUG) Log.d(TAG, "onSetZenModeW " + mode); - mNoMan.setZenMode(mode); + mNoMan.setZenMode(mode, null, TAG); } private void onDismissRequestedW(int reason) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java index f99eb6d..ef8257c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java @@ -17,10 +17,11 @@ package com.android.systemui.volume; import android.animation.LayoutTransition; import android.animation.ValueAnimator; +import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; import android.provider.Settings.Global; -import android.service.notification.Condition; +import android.service.notification.ZenModeConfig; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; @@ -34,6 +35,8 @@ import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.statusbar.policy.ZenModeController; +import java.util.Objects; + /** * Switch bar + zen mode panel (conditions) attached to the bottom of the volume dialog. */ @@ -57,6 +60,7 @@ public class ZenFooter extends LinearLayout { private TextView mSummaryLine2; private boolean mFooterExpanded; private int mZen = -1; + private ZenModeConfig mConfig; private Callback mCallback; public ZenFooter(Context context, AttributeSet attrs) { @@ -102,8 +106,8 @@ public class ZenFooter extends LinearLayout { setZen(zen); } @Override - public void onExitConditionChanged(Condition exitCondition) { - update(); + public void onConfigChanged(ZenModeConfig config) { + setConfig(config); } }); mSwitchBar.setOnClickListener(new OnClickListener() { @@ -129,6 +133,7 @@ public class ZenFooter extends LinearLayout { } }); mZen = mController.getZen(); + mConfig = mController.getConfig(); update(); } @@ -138,6 +143,12 @@ public class ZenFooter extends LinearLayout { update(); } + private void setConfig(ZenModeConfig config) { + if (Objects.equals(mConfig, config)) return; + mConfig = config; + update(); + } + public boolean isZen() { return isZenPriority() || isZenAlarms() || isZenNone(); } @@ -196,7 +207,9 @@ public class ZenFooter extends LinearLayout { : isZenNone() ? mContext.getString(R.string.interruption_level_none) : null; Util.setText(mSummaryLine1, line1); - Util.setText(mSummaryLine2, mZenModePanel.getExitConditionText()); + final String line2 = ZenModeConfig.getConditionSummary(mContext, mConfig, + ActivityManager.getCurrentUser()); + Util.setText(mSummaryLine2, line2); } private final OnCheckedChangeListener mCheckedListener = new OnCheckedChangeListener() { @@ -208,7 +221,7 @@ public class ZenFooter extends LinearLayout { : Global.ZEN_MODE_OFF; mZen = newZen; // this one's optimistic setFooterExpanded(isChecked); - mController.setZen(newZen); + mController.setZen(newZen, null, TAG); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java index cb6c29f..f6d4c36 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java @@ -32,6 +32,7 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.service.notification.Condition; import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ZenRule; import android.text.TextUtils; import android.util.ArraySet; import android.util.AttributeSet; @@ -157,7 +158,6 @@ public class ZenModePanel extends LinearLayout { } mZenButtons.getChildAt(3).setVisibility(mEmbedded ? GONE : VISIBLE); mZenEmbeddedDivider.setVisibility(mEmbedded ? VISIBLE : GONE); - setExpanded(mEmbedded); updateWidgets(); } @@ -278,7 +278,7 @@ public class ZenModePanel extends LinearLayout { if (expanded == mExpanded) return; if (DEBUG) Log.d(mTag, "setExpanded " + expanded); mExpanded = expanded; - if (mExpanded) { + if (mExpanded && isShown()) { ensureSelection(); } updateWidgets(); @@ -299,7 +299,7 @@ public class ZenModePanel extends LinearLayout { }); } if (mRequestingConditions) { - mTimeCondition = parseExistingTimeCondition(mExitCondition); + mTimeCondition = parseExistingTimeCondition(mContext, mExitCondition); if (mTimeCondition != null) { mBucketIndex = -1; } else { @@ -327,10 +327,9 @@ public class ZenModePanel extends LinearLayout { for (int i = 0; i < mMaxConditions; i++) { mZenConditions.addView(mInflater.inflate(R.layout.zen_mode_condition, this, false)); } - setExitCondition(mController.getExitCondition()); refreshExitConditionText(); mSessionZen = getSelectedZen(-1); - handleUpdateZen(mController.getZen()); + handleUpdateManualRule(mController.getManualRule()); if (DEBUG) Log.d(mTag, "init mExitCondition=" + mExitCondition); hideAllConditions(); mController.addCallback(mZenCallback); @@ -352,6 +351,10 @@ public class ZenModePanel extends LinearLayout { return condition != null ? condition.id : null; } + private Uri getRealConditionId(Condition condition) { + return isForever(condition) ? null : getConditionId(condition); + } + private static boolean sameConditionId(Condition lhs, Condition rhs) { return lhs == null ? rhs == null : rhs != null && lhs.id.equals(rhs.id); } @@ -360,18 +363,18 @@ public class ZenModePanel extends LinearLayout { return condition == null ? null : condition.copy(); } - public String getExitConditionText() { - return mExitConditionText; + private void refreshExitConditionText() { + mExitConditionText = getExitConditionText(mContext, mExitCondition); } - private void refreshExitConditionText() { - if (mExitCondition == null) { - mExitConditionText = foreverSummary(); - } else if (isCountdown(mExitCondition)) { - final Condition condition = parseExistingTimeCondition(mExitCondition); - mExitConditionText = condition != null ? condition.summary : foreverSummary(); + public static String getExitConditionText(Context context, Condition exitCondition) { + if (exitCondition == null) { + return foreverSummary(context); + } else if (isCountdown(exitCondition)) { + final Condition condition = parseExistingTimeCondition(context, exitCondition); + return condition != null ? condition.summary : foreverSummary(context); } else { - mExitConditionText = mExitCondition.summary; + return exitCondition.summary; } } @@ -386,9 +389,16 @@ public class ZenModePanel extends LinearLayout { mIconPulser.start(noneButton); } + private void handleUpdateManualRule(ZenRule rule) { + final int zen = rule != null ? rule.zenMode : Global.ZEN_MODE_OFF; + handleUpdateZen(zen); + final Condition c = rule != null ? rule.condition : null; + handleExitConditionChanged(c); + } + private void handleUpdateZen(int zen) { if (mSessionZen != -1 && mSessionZen != zen) { - setExpanded(mEmbedded || zen != Global.ZEN_MODE_OFF); + setExpanded(mEmbedded && isShown() || !mEmbedded && zen != Global.ZEN_MODE_OFF); mSessionZen = zen; } mZenButtons.setSelectedValue(zen); @@ -402,6 +412,20 @@ public class ZenModePanel extends LinearLayout { } } + private void handleExitConditionChanged(Condition exitCondition) { + setExitCondition(exitCondition); + if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitCondition); + final int N = getVisibleConditions(); + for (int i = 0; i < N; i++) { + final ConditionTag tag = getConditionTagAt(i); + if (tag != null) { + if (sameConditionId(tag.condition, mExitCondition)) { + bind(exitCondition, mZenConditions.getChildAt(i)); + } + } + } + } + private Condition getSelectedCondition() { final int N = getVisibleConditions(); for (int i = 0; i < N; i++) { @@ -447,14 +471,14 @@ public class ZenModePanel extends LinearLayout { ? mSubheadWarningColor : mSubheadColor); } - private Condition parseExistingTimeCondition(Condition condition) { + private static Condition parseExistingTimeCondition(Context context, Condition condition) { if (condition == null) return null; final long time = ZenModeConfig.tryParseCountdownConditionId(condition.id); if (time == 0) return null; final long now = System.currentTimeMillis(); final long span = time - now; if (span <= 0 || span > MAX_BUCKET_MINUTES * MINUTES_MS) return null; - return ZenModeConfig.toTimeCondition(mContext, + return ZenModeConfig.toTimeCondition(context, time, Math.round(span / (float) MINUTES_MS), now, ActivityManager.getCurrentUser()); } @@ -514,18 +538,18 @@ public class ZenModePanel extends LinearLayout { mZenConditions.getChildAt(i).setVisibility(GONE); } // ensure something is selected - if (mExpanded) { + if (mExpanded && isShown()) { ensureSelection(); } } private Condition forever() { - return new Condition(mForeverId, foreverSummary(), "", "", 0 /*icon*/, Condition.STATE_TRUE, - 0 /*flags*/); + return new Condition(mForeverId, foreverSummary(mContext), "", "", 0 /*icon*/, + Condition.STATE_TRUE, 0 /*flags*/); } - private String foreverSummary() { - return mContext.getString(com.android.internal.R.string.zen_mode_forever); + private static String foreverSummary(Context context) { + return context.getString(com.android.internal.R.string.zen_mode_forever); } private ConditionTag getConditionTagAt(int index) { @@ -574,21 +598,7 @@ public class ZenModePanel extends LinearLayout { } } - private void handleExitConditionChanged(Condition exitCondition) { - setExitCondition(exitCondition); - if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitCondition); - final int N = getVisibleConditions(); - for (int i = 0; i < N; i++) { - final ConditionTag tag = getConditionTagAt(i); - if (tag != null) { - if (sameConditionId(tag.condition, mExitCondition)) { - bind(exitCondition, mZenConditions.getChildAt(i)); - } - } - } - } - - private boolean isCountdown(Condition c) { + private static boolean isCountdown(Condition c) { return c != null && ZenModeConfig.isValidCountdownConditionId(c.id); } @@ -770,17 +780,21 @@ public class ZenModePanel extends LinearLayout { private void select(final Condition condition) { if (DEBUG) Log.d(mTag, "select " + condition); - final boolean isForever = isForever(condition); + if (mSessionZen == -1 || mSessionZen == Global.ZEN_MODE_OFF) { + if (DEBUG) Log.d(mTag, "Ignoring condition selection outside of manual zen"); + return; + } + final Uri realConditionId = getRealConditionId(condition); if (mController != null) { AsyncTask.execute(new Runnable() { @Override public void run() { - mController.setExitCondition(isForever ? null : condition); + mController.setZen(mSessionZen, realConditionId, TAG + ".selectCondition"); } }); } setExitCondition(condition); - if (isForever) { + if (realConditionId == null) { mPrefs.setMinuteIndex(-1); } else if (isCountdown(condition) && mBucketIndex != -1) { mPrefs.setMinuteIndex(mBucketIndex); @@ -808,24 +822,19 @@ public class ZenModePanel extends LinearLayout { private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() { @Override - public void onZenChanged(int zen) { - mHandler.obtainMessage(H.UPDATE_ZEN, zen, 0).sendToTarget(); - } - @Override public void onConditionsChanged(Condition[] conditions) { mHandler.obtainMessage(H.UPDATE_CONDITIONS, conditions).sendToTarget(); } @Override - public void onExitConditionChanged(Condition exitCondition) { - mHandler.obtainMessage(H.EXIT_CONDITION_CHANGED, exitCondition).sendToTarget(); + public void onManualRuleChanged(ZenRule rule) { + mHandler.obtainMessage(H.MANUAL_RULE_CHANGED, rule).sendToTarget(); } }; private final class H extends Handler { private static final int UPDATE_CONDITIONS = 1; - private static final int EXIT_CONDITION_CHANGED = 2; - private static final int UPDATE_ZEN = 3; + private static final int MANUAL_RULE_CHANGED = 2; private H() { super(Looper.getMainLooper()); @@ -835,10 +844,8 @@ public class ZenModePanel extends LinearLayout { public void handleMessage(Message msg) { if (msg.what == UPDATE_CONDITIONS) { handleUpdateConditions((Condition[]) msg.obj); - } else if (msg.what == EXIT_CONDITION_CHANGED) { - handleExitConditionChanged((Condition) msg.obj); - } else if (msg.what == UPDATE_ZEN) { - handleUpdateZen(msg.arg1); + } else if (msg.what == MANUAL_RULE_CHANGED) { + handleUpdateManualRule((ZenRule) msg.obj); } } } @@ -930,12 +937,13 @@ public class ZenModePanel extends LinearLayout { private final SegmentedButtons.Callback mZenButtonsCallback = new SegmentedButtons.Callback() { @Override public void onSelected(final Object value) { - if (value != null && mZenButtons.isShown()) { + if (value != null && mZenButtons.isShown() && isAttachedToWindow()) { if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + value); + final Uri realConditionId = getRealConditionId(mSessionExitCondition); AsyncTask.execute(new Runnable() { @Override public void run() { - mController.setZen((Integer) value); + mController.setZen((Integer) value, realConditionId, TAG + ".selectZen"); } }); } diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java index 96840a2..1bed4f3 100644 --- a/services/backup/java/com/android/server/backup/BackupManagerService.java +++ b/services/backup/java/com/android/server/backup/BackupManagerService.java @@ -2209,7 +2209,10 @@ public class BackupManagerService { // Get the restore-set token for the best-available restore set for this package: // the active set if possible, else the ancestral one. Returns zero if none available. - long getAvailableRestoreToken(String packageName) { + public long getAvailableRestoreToken(String packageName) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "getAvailableRestoreToken"); + long token = mAncestralToken; synchronized (mQueueLock) { if (mEverStoredApps.contains(packageName)) { diff --git a/services/backup/java/com/android/server/backup/Trampoline.java b/services/backup/java/com/android/server/backup/Trampoline.java index 99bbdae..5859c6a 100644 --- a/services/backup/java/com/android/server/backup/Trampoline.java +++ b/services/backup/java/com/android/server/backup/Trampoline.java @@ -317,6 +317,12 @@ public class Trampoline extends IBackupManager.Stub { } @Override + public long getAvailableRestoreToken(String packageName) { + BackupManagerService svc = mService; + return (svc != null) ? svc.getAvailableRestoreToken(packageName) : 0; + } + + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java index 7d156df..34e8b78 100644 --- a/services/core/java/com/android/server/AlarmManagerService.java +++ b/services/core/java/com/android/server/AlarmManagerService.java @@ -34,6 +34,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.PowerManager; +import android.os.Process; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; @@ -61,6 +62,7 @@ import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.Locale; +import java.util.Random; import java.util.TimeZone; import static android.app.AlarmManager.RTC_WAKEUP; @@ -128,6 +130,7 @@ class AlarmManagerService extends SystemService { final ResultReceiver mResultReceiver = new ResultReceiver(); PendingIntent mTimeTickSender; PendingIntent mDateChangeSender; + Random mRandom; boolean mInteractive = true; long mNonInteractiveStartTime; long mNonInteractiveTime; @@ -185,18 +188,20 @@ class AlarmManagerService extends SystemService { final class Batch { long start; // These endpoints are always in ELAPSED long end; - boolean standalone; // certain "batches" don't participate in coalescing + int flags; // Flags for alarms, such as FLAG_STANDALONE. final ArrayList<Alarm> alarms = new ArrayList<Alarm>(); Batch() { start = 0; end = Long.MAX_VALUE; + flags = 0; } Batch(Alarm seed) { start = seed.whenElapsed; - end = seed.maxWhen; + end = seed.maxWhenElapsed; + flags = seed.flags; alarms.add(seed); } @@ -227,9 +232,10 @@ class AlarmManagerService extends SystemService { start = alarm.whenElapsed; newStart = true; } - if (alarm.maxWhen < end) { - end = alarm.maxWhen; + if (alarm.maxWhenElapsed < end) { + end = alarm.maxWhenElapsed; } + flags |= alarm.flags; if (DEBUG_BATCH) { Slog.v(TAG, " => now " + this); @@ -241,6 +247,7 @@ class AlarmManagerService extends SystemService { boolean didRemove = false; long newStart = 0; // recalculate endpoints as we go long newEnd = Long.MAX_VALUE; + int newFlags = 0; for (int i = 0; i < alarms.size(); ) { Alarm alarm = alarms.get(i); if (alarm.operation.equals(operation)) { @@ -253,9 +260,10 @@ class AlarmManagerService extends SystemService { if (alarm.whenElapsed > newStart) { newStart = alarm.whenElapsed; } - if (alarm.maxWhen < newEnd) { - newEnd = alarm.maxWhen; + if (alarm.maxWhenElapsed < newEnd) { + newEnd = alarm.maxWhenElapsed; } + newFlags |= alarm.flags; i++; } } @@ -263,6 +271,7 @@ class AlarmManagerService extends SystemService { // commit the new batch bounds start = newStart; end = newEnd; + flags = newFlags; } return didRemove; } @@ -271,6 +280,7 @@ class AlarmManagerService extends SystemService { boolean didRemove = false; long newStart = 0; // recalculate endpoints as we go long newEnd = Long.MAX_VALUE; + int newFlags = 0; for (int i = 0; i < alarms.size(); ) { Alarm alarm = alarms.get(i); if (alarm.operation.getTargetPackage().equals(packageName)) { @@ -283,9 +293,10 @@ class AlarmManagerService extends SystemService { if (alarm.whenElapsed > newStart) { newStart = alarm.whenElapsed; } - if (alarm.maxWhen < newEnd) { - newEnd = alarm.maxWhen; + if (alarm.maxWhenElapsed < newEnd) { + newEnd = alarm.maxWhenElapsed; } + newFlags |= alarm.flags; i++; } } @@ -293,6 +304,7 @@ class AlarmManagerService extends SystemService { // commit the new batch bounds start = newStart; end = newEnd; + flags = newFlags; } return didRemove; } @@ -313,8 +325,8 @@ class AlarmManagerService extends SystemService { if (alarm.whenElapsed > newStart) { newStart = alarm.whenElapsed; } - if (alarm.maxWhen < newEnd) { - newEnd = alarm.maxWhen; + if (alarm.maxWhenElapsed < newEnd) { + newEnd = alarm.maxWhenElapsed; } i++; } @@ -357,8 +369,9 @@ class AlarmManagerService extends SystemService { b.append(" num="); b.append(size()); b.append(" start="); b.append(start); b.append(" end="); b.append(end); - if (standalone) { - b.append(" STANDALONE"); + if (flags != 0) { + b.append(" flgs=0x"); + b.append(Integer.toHexString(flags)); } b.append('}'); return b.toString(); @@ -441,7 +454,12 @@ class AlarmManagerService extends SystemService { // minimum recurrence period or alarm futurity for us to be able to fuzz it static final long MIN_FUZZABLE_INTERVAL = 10000; static final BatchTimeOrder sBatchOrder = new BatchTimeOrder(); - final ArrayList<Batch> mAlarmBatches = new ArrayList<Batch>(); + final ArrayList<Batch> mAlarmBatches = new ArrayList<>(); + + // set to null if in idle mode; while in this mode, any alarms we don't want + // to run during this time are placed in mPendingWhileIdleAlarms + Alarm mPendingIdleUntil = null; + final ArrayList<Alarm> mPendingWhileIdleAlarms = new ArrayList<>(); public AlarmManagerService(Context context) { super(context); @@ -486,7 +504,7 @@ class AlarmManagerService extends SystemService { final int N = mAlarmBatches.size(); for (int i = 0; i < N; i++) { Batch b = mAlarmBatches.get(i); - if (!b.standalone && b.canHold(whenElapsed, maxWhen)) { + if ((b.flags&AlarmManager.FLAG_STANDALONE) == 0 && b.canHold(whenElapsed, maxWhen)) { return i; } } @@ -503,31 +521,56 @@ class AlarmManagerService extends SystemService { void rebatchAllAlarmsLocked(boolean doValidate) { ArrayList<Batch> oldSet = (ArrayList<Batch>) mAlarmBatches.clone(); mAlarmBatches.clear(); + Alarm oldPendingIdleUntil = mPendingIdleUntil; final long nowElapsed = SystemClock.elapsedRealtime(); final int oldBatches = oldSet.size(); for (int batchNum = 0; batchNum < oldBatches; batchNum++) { Batch batch = oldSet.get(batchNum); final int N = batch.size(); for (int i = 0; i < N; i++) { - Alarm a = batch.get(i); - long whenElapsed = convertToElapsed(a.when, a.type); - final long maxElapsed; - if (a.whenElapsed == a.maxWhen) { - // Exact - maxElapsed = whenElapsed; - } else { - // Not exact. Preserve any explicit window, otherwise recalculate - // the window based on the alarm's new futurity. Note that this - // reflects a policy of preferring timely to deferred delivery. - maxElapsed = (a.windowLength > 0) - ? (whenElapsed + a.windowLength) - : maxTriggerTime(nowElapsed, whenElapsed, a.repeatInterval); - } - setImplLocked(a.type, a.when, whenElapsed, a.windowLength, maxElapsed, - a.repeatInterval, a.operation, batch.standalone, doValidate, a.workSource, - a.alarmClock, a.userId); + reAddAlarmLocked(batch.get(i), nowElapsed, doValidate); + } + } + if (oldPendingIdleUntil != null && oldPendingIdleUntil != mPendingIdleUntil) { + Slog.wtf(TAG, "Rebatching: idle until changed from " + oldPendingIdleUntil + + " to " + mPendingIdleUntil); + if (mPendingIdleUntil == null) { + // Somehow we lost this... we need to restore all of the pending alarms. + restorePendingWhileIdleAlarmsLocked(); } } + rescheduleKernelAlarmsLocked(); + updateNextAlarmClockLocked(); + } + + void reAddAlarmLocked(Alarm a, long nowElapsed, boolean doValidate) { + a.when = a.origWhen; + long whenElapsed = convertToElapsed(a.when, a.type); + final long maxElapsed; + if (a.whenElapsed == a.maxWhenElapsed) { + // Exact + maxElapsed = whenElapsed; + } else { + // Not exact. Preserve any explicit window, otherwise recalculate + // the window based on the alarm's new futurity. Note that this + // reflects a policy of preferring timely to deferred delivery. + maxElapsed = (a.windowLength > 0) + ? (whenElapsed + a.windowLength) + : maxTriggerTime(nowElapsed, whenElapsed, a.repeatInterval); + } + a.whenElapsed = whenElapsed; + a.maxWhenElapsed = maxElapsed; + setImplLocked(a, true, doValidate); + } + + void restorePendingWhileIdleAlarmsLocked() { + final long nowElapsed = SystemClock.elapsedRealtime(); + for (int i=mPendingWhileIdleAlarms.size() - 1; i >= 0 && mPendingIdleUntil != null; i --) { + Alarm a = mPendingWhileIdleAlarms.remove(i); + reAddAlarmLocked(a, nowElapsed, false); + } + rescheduleKernelAlarmsLocked(); + updateNextAlarmClockLocked(); } static final class InFlight extends Intent { @@ -687,7 +730,7 @@ class AlarmManagerService extends SystemService { } void setImpl(int type, long triggerAtTime, long windowLength, long interval, - PendingIntent operation, boolean isStandalone, WorkSource workSource, + PendingIntent operation, int flags, WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock) { if (operation == null) { Slog.w(TAG, "set/setRepeating ignored because there is no intent"); @@ -745,25 +788,66 @@ class AlarmManagerService extends SystemService { Slog.v(TAG, "set(" + operation + ") : type=" + type + " triggerAtTime=" + triggerAtTime + " win=" + windowLength + " tElapsed=" + triggerElapsed + " maxElapsed=" + maxElapsed - + " interval=" + interval + " standalone=" + isStandalone); + + " interval=" + interval + " flags=0x" + Integer.toHexString(flags)); } setImplLocked(type, triggerAtTime, triggerElapsed, windowLength, maxElapsed, - interval, operation, isStandalone, true, workSource, alarmClock, userId); + interval, operation, flags, true, workSource, alarmClock, userId); } } private void setImplLocked(int type, long when, long whenElapsed, long windowLength, - long maxWhen, long interval, PendingIntent operation, boolean isStandalone, + long maxWhen, long interval, PendingIntent operation, int flags, boolean doValidate, WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock, int userId) { Alarm a = new Alarm(type, when, whenElapsed, windowLength, maxWhen, interval, - operation, workSource, alarmClock, userId); + operation, workSource, flags, alarmClock, userId); removeLocked(operation); + setImplLocked(a, false, doValidate); + } + + private void setImplLocked(Alarm a, boolean rebatching, boolean doValidate) { + if ((a.flags&AlarmManager.FLAG_IDLE_UNTIL) != 0) { + // This is a special alarm that will put the system idle until it goes off. + // The caller has given the time they want this to happen at, however we need + // to pull that earlier if there are existing alarms that have requested to + // bring us out of idle. + final int N = mAlarmBatches.size(); + for (int i = 0; i < N; i++) { + Batch b = mAlarmBatches.get(i); + if (a.whenElapsed > b.end) { + // There are no interesting things happening before our idle until, + // so keep the requested time. + break; + } + if ((b.flags&AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) { + a.when = a.whenElapsed = a.maxWhenElapsed = b.end; + break; + } + } + // Add fuzz to make the alarm go off some time before the actual desired time. + final long nowElapsed = SystemClock.elapsedRealtime(); + long fuzz = fuzzForDuration(a.whenElapsed-nowElapsed); + if (fuzz > 0) { + if (mRandom == null) { + mRandom = new Random(); + } + a.whenElapsed -= mRandom.nextLong() % fuzz; + } - int whichBatch = (isStandalone) ? -1 : attemptCoalesceLocked(whenElapsed, maxWhen); + } else if (mPendingIdleUntil != null) { + // We currently have an idle until alarm scheduled; if the new alarm has + // not explicitly stated it wants to run while idle, then put it on hold. + if ((a.flags&(AlarmManager.FLAG_ALLOW_WHILE_IDLE|AlarmManager.FLAG_WAKE_FROM_IDLE)) + == 0) { + mPendingWhileIdleAlarms.add(a); + return; + } + } + + int whichBatch = ((a.flags&AlarmManager.FLAG_STANDALONE) != 0) + ? -1 : attemptCoalesceLocked(a.whenElapsed, a.maxWhenElapsed); if (whichBatch < 0) { Batch batch = new Batch(a); - batch.standalone = isStandalone; addBatchLocked(mAlarmBatches, batch); } else { Batch batch = mAlarmBatches.get(whichBatch); @@ -775,28 +859,48 @@ class AlarmManagerService extends SystemService { } } - if (alarmClock != null) { + if (a.alarmClock != null) { mNextAlarmClockMayChange = true; - updateNextAlarmClockLocked(); } - if (DEBUG_VALIDATE) { - if (doValidate && !validateConsistencyLocked()) { - Slog.v(TAG, "Tipping-point operation: type=" + type + " when=" + when - + " when(hex)=" + Long.toHexString(when) - + " whenElapsed=" + whenElapsed + " maxWhen=" + maxWhen - + " interval=" + interval + " op=" + operation - + " standalone=" + isStandalone); + boolean needRebatch = false; + + if ((a.flags&AlarmManager.FLAG_IDLE_UNTIL) != 0) { + mPendingIdleUntil = a; + needRebatch = true; + } else if ((a.flags&AlarmManager.FLAG_WAKE_FROM_IDLE) != 0 && mPendingIdleUntil != null) { + // If we are adding an alarm that asks to wake from idle, and we are currently + // idling, then we need to rebatch alarms in case the idle until time needs to + // be updated. + needRebatch = true; + } + + if (!rebatching) { + if (DEBUG_VALIDATE) { + if (doValidate && !validateConsistencyLocked()) { + Slog.v(TAG, "Tipping-point operation: type=" + a.type + " when=" + a.when + + " when(hex)=" + Long.toHexString(a.when) + + " whenElapsed=" + a.whenElapsed + + " maxWhenElapsed=" + a.maxWhenElapsed + + " interval=" + a.repeatInterval + " op=" + a.operation + + " flags=0x" + Integer.toHexString(a.flags)); + rebatchAllAlarmsLocked(false); + needRebatch = false; + } + } + + if (needRebatch) { rebatchAllAlarmsLocked(false); } - } - rescheduleKernelAlarmsLocked(); + rescheduleKernelAlarmsLocked(); + updateNextAlarmClockLocked(); + } } private final IBinder mService = new IAlarmManager.Stub() { @Override - public void set(int type, long triggerAtTime, long windowLength, long interval, + public void set(int type, long triggerAtTime, long windowLength, long interval, int flags, PendingIntent operation, WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock) { if (workSource != null) { @@ -805,8 +909,17 @@ class AlarmManagerService extends SystemService { "AlarmManager.set"); } + if (windowLength == AlarmManager.WINDOW_EXACT) { + flags |= AlarmManager.FLAG_STANDALONE; + } + if (alarmClock != null) { + flags |= AlarmManager.FLAG_WAKE_FROM_IDLE | AlarmManager.FLAG_STANDALONE; + } + if (Binder.getCallingUid() < Process.FIRST_APPLICATION_UID) { + flags |= AlarmManager.FLAG_ALLOW_WHILE_IDLE; + } setImpl(type, triggerAtTime, windowLength, interval, operation, - windowLength == AlarmManager.WINDOW_EXACT, workSource, alarmClock); + flags, workSource, alarmClock); } @Override @@ -912,6 +1025,14 @@ class AlarmManagerService extends SystemService { dumpAlarmList(pw, b.alarms, " ", nowELAPSED, nowRTC, sdf); } } + if (mPendingIdleUntil != null) { + pw.println(); + pw.println("Idle mode state:"); + pw.print(" Idling until: "); pw.println(mPendingIdleUntil); + mPendingIdleUntil.dump(pw, " ", nowELAPSED, nowRTC, sdf); + pw.println(" Pending alarms:"); + dumpAlarmList(pw, mPendingWhileIdleAlarms, " ", nowELAPSED, nowRTC, sdf); + } pw.println(); pw.print("Past-due non-wakeup alarms: "); @@ -1224,6 +1345,15 @@ class AlarmManagerService extends SystemService { } void rescheduleKernelAlarmsLocked() { + if (mPendingIdleUntil != null) { + // If we have a pending "idle until" alarm, we will just blindly wait until + // it is time for that alarm to go off. We don't want to wake up for any + // other reasons. + mNextWakeup = mNextNonWakeup = mPendingIdleUntil.whenElapsed; + setLocked(ELAPSED_REALTIME_WAKEUP, mNextWakeup); + setLocked(ELAPSED_REALTIME, mNextNonWakeup); + return; + } // Schedule the next upcoming wakeup alarm. If there is a deliverable batch // prior to that which contains no wakeups, we schedule that as well. long nextNonWakeup = 0; @@ -1260,13 +1390,26 @@ class AlarmManagerService extends SystemService { mAlarmBatches.remove(i); } } + for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) { + if (mPendingWhileIdleAlarms.get(i).operation.equals(operation)) { + // Don't set didRemove, since this doesn't impact the scheduled alarms. + mPendingWhileIdleAlarms.remove(i); + } + } if (didRemove) { if (DEBUG_BATCH) { Slog.v(TAG, "remove(operation) changed bounds; rebatching"); } + boolean restorePending = false; + if (mPendingIdleUntil != null && mPendingIdleUntil.operation.equals(operation)) { + mPendingIdleUntil = null; + restorePending = true; + } rebatchAllAlarmsLocked(true); - rescheduleKernelAlarmsLocked(); + if (restorePending) { + restorePendingWhileIdleAlarmsLocked(); + } updateNextAlarmClockLocked(); } } @@ -1280,6 +1423,12 @@ class AlarmManagerService extends SystemService { mAlarmBatches.remove(i); } } + for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) { + if (mPendingWhileIdleAlarms.get(i).operation.getTargetPackage().equals(packageName)) { + // Don't set didRemove, since this doesn't impact the scheduled alarms. + mPendingWhileIdleAlarms.remove(i); + } + } if (didRemove) { if (DEBUG_BATCH) { @@ -1300,6 +1449,13 @@ class AlarmManagerService extends SystemService { mAlarmBatches.remove(i); } } + for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) { + if (UserHandle.getUserId(mPendingWhileIdleAlarms.get(i).operation.getCreatorUid()) + == userHandle) { + // Don't set didRemove, since this doesn't impact the scheduled alarms. + mPendingWhileIdleAlarms.remove(i); + } + } if (didRemove) { if (DEBUG_BATCH) { @@ -1344,6 +1500,11 @@ class AlarmManagerService extends SystemService { return true; } } + for (int i = 0; i < mPendingWhileIdleAlarms.size(); i++) { + if (mPendingWhileIdleAlarms.get(i).operation.getTargetPackage().equals(packageName)) { + return true; + } + } return false; } @@ -1413,6 +1574,13 @@ class AlarmManagerService extends SystemService { boolean triggerAlarmsLocked(ArrayList<Alarm> triggerList, final long nowELAPSED, final long nowRTC) { boolean hasWakeup = false; + if (mPendingIdleUntil != null) { + // If we have a pending "idle until" alarm, don't trigger any alarms + // until we are past the idle period. + if (nowELAPSED < mPendingIdleUntil.whenElapsed) { + return false; + } + } // batches are temporally sorted, so we need only pull from the // start of the list until we either empty it or hit a batch // that is not yet deliverable @@ -1432,6 +1600,11 @@ class AlarmManagerService extends SystemService { Alarm alarm = batch.get(i); alarm.count = 1; triggerList.add(alarm); + if (mPendingIdleUntil == alarm) { + mPendingIdleUntil = null; + rebatchAllAlarmsLocked(false); + restorePendingWhileIdleAlarmsLocked(); + } // Recurring alarms may have passed several alarm intervals while the // phone was asleep or off, so pass a trigger count when sending them. @@ -1445,7 +1618,7 @@ class AlarmManagerService extends SystemService { final long nextElapsed = alarm.whenElapsed + delta; setImplLocked(alarm.type, alarm.when + delta, nextElapsed, alarm.windowLength, maxTriggerTime(nowELAPSED, nextElapsed, alarm.repeatInterval), - alarm.repeatInterval, alarm.operation, batch.standalone, true, + alarm.repeatInterval, alarm.operation, alarm.flags, true, alarm.workSource, alarm.alarmClock, alarm.userId); } @@ -1494,34 +1667,38 @@ class AlarmManagerService extends SystemService { private static class Alarm { public final int type; + public final long origWhen; public final boolean wakeup; public final PendingIntent operation; public final String tag; public final WorkSource workSource; + public final int flags; public int count; public long when; public long windowLength; public long whenElapsed; // 'when' in the elapsed time base - public long maxWhen; // also in the elapsed time base + public long maxWhenElapsed; // also in the elapsed time base public long repeatInterval; public final AlarmManager.AlarmClockInfo alarmClock; public final int userId; public PriorityClass priorityClass; public Alarm(int _type, long _when, long _whenElapsed, long _windowLength, long _maxWhen, - long _interval, PendingIntent _op, WorkSource _ws, + long _interval, PendingIntent _op, WorkSource _ws, int _flags, AlarmManager.AlarmClockInfo _info, int _userId) { type = _type; + origWhen = _when; wakeup = _type == AlarmManager.ELAPSED_REALTIME_WAKEUP || _type == AlarmManager.RTC_WAKEUP; when = _when; whenElapsed = _whenElapsed; windowLength = _windowLength; - maxWhen = _maxWhen; + maxWhenElapsed = _maxWhen; repeatInterval = _interval; operation = _op; tag = makeTag(_op, _type); workSource = _ws; + flags = _flags; alarmClock = _info; userId = _userId; } @@ -1561,7 +1738,8 @@ class AlarmManagerService extends SystemService { pw.println(); pw.print(prefix); pw.print("window="); pw.print(windowLength); pw.print(" repeatInterval="); pw.print(repeatInterval); - pw.print(" count="); pw.println(count); + pw.print(" count="); pw.print(count); + pw.print(" flags=0x"); pw.println(Integer.toHexString(flags)); pw.print(prefix); pw.print("operation="); pw.println(operation); } } @@ -1599,6 +1777,20 @@ class AlarmManagerService extends SystemService { } } + static long fuzzForDuration(long duration) { + if (duration < 15*60*1000) { + // If the duration until the time is less than 15 minutes, the maximum fuzz + // is the duration. + return duration; + } else if (duration < 90*60*1000) { + // If duration is less than 1 1/2 hours, the maximum fuzz is 15 minutes, + return 15*60*1000; + } else { + // Otherwise, we will fuzz by at most half an hour. + return 30*60*1000; + } + } + boolean checkAllowNonWakeupDelayLocked(long nowELAPSED) { if (mInteractive) { return false; @@ -1886,7 +2078,7 @@ class AlarmManagerService extends SystemService { final WorkSource workSource = null; // Let system take blame for time tick events. setImpl(ELAPSED_REALTIME, SystemClock.elapsedRealtime() + tickEventDelay, 0, - 0, mTimeTickSender, true, workSource, null); + 0, mTimeTickSender, AlarmManager.FLAG_STANDALONE, workSource, null); } public void scheduleDateChangedEvent() { @@ -1899,8 +2091,8 @@ class AlarmManagerService extends SystemService { calendar.add(Calendar.DAY_OF_MONTH, 1); final WorkSource workSource = null; // Let system take blame for date change events. - setImpl(RTC, calendar.getTimeInMillis(), 0, 0, mDateChangeSender, true, workSource, - null); + setImpl(RTC, calendar.getTimeInMillis(), 0, 0, mDateChangeSender, + AlarmManager.FLAG_STANDALONE, workSource, null); } } diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java index 4677f65..d92a89f 100644 --- a/services/core/java/com/android/server/InputMethodManagerService.java +++ b/services/core/java/com/android/server/InputMethodManagerService.java @@ -15,6 +15,7 @@ package com.android.server; +import android.annotation.NonNull; import com.android.internal.content.PackageMonitor; import com.android.internal.inputmethod.InputMethodSubtypeSwitchingController; import com.android.internal.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; @@ -1285,13 +1286,24 @@ public class InputMethodManagerService extends IInputMethodManager.Stub return startInputUncheckedLocked(cs, inputContext, attribute, controlFlags); } - InputBindResult startInputUncheckedLocked(ClientState cs, + InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext, EditorInfo attribute, int controlFlags) { // If no method is currently selected, do nothing. if (mCurMethodId == null) { return mNoBinding; } + if (attribute != null) { + // We accept an empty package name as a valid data. + if (!TextUtils.isEmpty(attribute.packageName) && + !InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, cs.uid, + attribute.packageName)) { + Slog.e(TAG, "Rejecting this client as it reported an invalid package name." + + " uid=" + cs.uid + " package=" + attribute.packageName); + return mNoBinding; + } + } + if (mCurClient != cs) { // Was the keyguard locked when switching over to the new client? mCurClientInKeyguard = isKeyguardLocked(); @@ -1855,16 +1867,10 @@ public class InputMethodManagerService extends IInputMethodManager.Stub } if (mCurClient != null && mCurAttribute != null) { - final int uid = mCurClient.uid; - final String packageName = mCurAttribute.packageName; - if (SystemConfig.getInstance().getFixedImeApps().contains(packageName)) { - if (InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, uid, packageName)) { - return; - } - // TODO: Do we need to lock the input method when the application reported an - // incorrect package name? - Slog.e(TAG, "Ignoring FixedImeApps due to the validation failure. uid=" + uid - + " package=" + packageName); + // We have already made sure that the package name belongs to the application's UID. + // No further UID check is required. + if (SystemConfig.getInstance().getFixedImeApps().contains(mCurAttribute.packageName)) { + return; } } @@ -2148,7 +2154,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub // more quickly (not get stuck behind it initializing itself for the // new focused input, even if its window wants to hide the IME). boolean didStart = false; - + switch (softInputMode&WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE) { case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED: if (!isTextEditor || !doAutoShow) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index d2f52b4..7193384 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -100,6 +100,7 @@ import com.google.android.collect.Lists; import com.google.android.collect.Maps; import libcore.io.IoUtils; +import libcore.util.EmptyArray; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -986,6 +987,12 @@ public final class ActivityManagerService extends ActivityManagerNative private boolean mSleeping = false; /** + * The process state used for processes that are running the top activities. + * This changes between TOP and TOP_SLEEPING to following mSleeping. + */ + int mTopProcessState = ActivityManager.PROCESS_STATE_TOP; + + /** * Set while we are running a voice interaction. This overrides * sleeping while it is active. */ @@ -2466,6 +2473,13 @@ public final class ActivityManagerService extends ActivityManagerNative } } + @Override + public void batterySendBroadcast(Intent intent) { + broadcastIntentLocked(null, null, intent, null, + null, 0, null, null, null, AppOpsManager.OP_NONE, false, false, -1, + Process.SYSTEM_UID, UserHandle.USER_ALL); + } + /** * Initialize the application bind args. These are passed to each * process when the bindApplication() IPC is sent to the process. They're @@ -9726,10 +9740,14 @@ public final class ActivityManagerService extends ActivityManagerNative void updateSleepIfNeededLocked() { if (mSleeping && !shouldSleepLocked()) { mSleeping = false; + mTopProcessState = ActivityManager.PROCESS_STATE_TOP; mStackSupervisor.comeOutOfSleepIfNeededLocked(); + updateOomAdjLocked(); } else if (!mSleeping && shouldSleepLocked()) { mSleeping = true; + mTopProcessState = ActivityManager.PROCESS_STATE_TOP_SLEEPING; mStackSupervisor.goingToSleepLocked(); + updateOomAdjLocked(); // Initialize the wake times of all processes. checkExcessivePowerUsageLocked(false); @@ -10700,7 +10718,8 @@ public final class ActivityManagerService extends ActivityManagerNative for (int i = mLruProcesses.size() - 1 ; i >= 0 ; i--) { ProcessRecord proc = mLruProcesses.get(i); if (proc.notCachedSinceIdle) { - if (proc.setProcState > ActivityManager.PROCESS_STATE_TOP + if (proc.setProcState != ActivityManager.PROCESS_STATE_TOP + && proc.setProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND && proc.setProcState <= ActivityManager.PROCESS_STATE_SERVICE) { if (doKilling && proc.initialIdlePss != 0 && proc.lastPss > ((proc.initialIdlePss*3)/2)) { @@ -12893,7 +12912,7 @@ public final class ActivityManagerService extends ActivityManagerNative StringBuilder sb = new StringBuilder(); sb.append(" ").append(proc).append('/'); UserHandle.formatUid(sb, uids.keyAt(j)); - Pair<Long, String> val = uids.valueAt(i); + Pair<Long, String> val = uids.valueAt(j); sb.append(": "); DebugUtils.sizeValueToString(val.first, sb); if (val.second != null) { sb.append(", report to ").append(val.second); @@ -13929,7 +13948,7 @@ public final class ActivityManagerService extends ActivityManagerNative } else if ("-h".equals(opt)) { pw.println("meminfo dump options: [-a] [-d] [-c] [--oom] [process]"); pw.println(" -a: include all available information for each process."); - pw.println(" -d: include dalvik details when dumping process details."); + pw.println(" -d: include dalvik details."); pw.println(" -c: dump in a compact machine-parseable representation."); pw.println(" --oom: only show processes organized by oom adj."); pw.println(" --local: only collect details locally, don't call process."); @@ -14016,6 +14035,8 @@ public final class ActivityManagerService extends ActivityManagerNative final SparseArray<MemItem> procMemsMap = new SparseArray<MemItem>(); long nativePss = 0; long dalvikPss = 0; + long[] dalvikSubitemPss = dumpDalvik ? new long[Debug.MemoryInfo.NUM_DVK_STATS] : + EmptyArray.LONG; long otherPss = 0; long[] miscPss = new long[Debug.MemoryInfo.NUM_OTHER_STATS]; @@ -14093,6 +14114,9 @@ public final class ActivityManagerService extends ActivityManagerNative nativePss += mi.nativePss; dalvikPss += mi.dalvikPss; + for (int j=0; j<dalvikSubitemPss.length; j++) { + dalvikSubitemPss[j] += mi.getOtherPss(Debug.MemoryInfo.NUM_OTHER_STATS + j); + } otherPss += mi.otherPss; for (int j=0; j<Debug.MemoryInfo.NUM_OTHER_STATS; j++) { long mem = mi.getOtherPss(j); @@ -14151,6 +14175,10 @@ public final class ActivityManagerService extends ActivityManagerNative nativePss += mi.nativePss; dalvikPss += mi.dalvikPss; + for (int j=0; j<dalvikSubitemPss.length; j++) { + dalvikSubitemPss[j] += mi.getOtherPss( + Debug.MemoryInfo.NUM_OTHER_STATS + j); + } otherPss += mi.otherPss; for (int j=0; j<Debug.MemoryInfo.NUM_OTHER_STATS; j++) { long mem = mi.getOtherPss(j); @@ -14169,7 +14197,16 @@ public final class ActivityManagerService extends ActivityManagerNative ArrayList<MemItem> catMems = new ArrayList<MemItem>(); catMems.add(new MemItem("Native", "Native", nativePss, -1)); - catMems.add(new MemItem("Dalvik", "Dalvik", dalvikPss, -2)); + final MemItem dalvikItem = new MemItem("Dalvik", "Dalvik", dalvikPss, -2); + if (dalvikSubitemPss.length > 0) { + dalvikItem.subitems = new ArrayList<MemItem>(); + for (int j=0; j<dalvikSubitemPss.length; j++) { + final String name = Debug.MemoryInfo.getOtherLabel( + Debug.MemoryInfo.NUM_OTHER_STATS + j); + dalvikItem.subitems.add(new MemItem(name, name, dalvikSubitemPss[j], j)); + } + } + catMems.add(dalvikItem); catMems.add(new MemItem("Unknown", "Unknown", otherPss, -3)); for (int j=0; j<Debug.MemoryInfo.NUM_OTHER_STATS; j++) { String label = Debug.MemoryInfo.getOtherLabel(j); @@ -16824,6 +16861,8 @@ public final class ActivityManagerService extends ActivityManagerNative app.systemNoUi = false; + final int PROCESS_STATE_TOP = mTopProcessState; + // Determine the importance of the process, starting with most // important to least, and assign an appropriate OOM adjustment. int adj; @@ -16837,7 +16876,7 @@ public final class ActivityManagerService extends ActivityManagerNative schedGroup = Process.THREAD_GROUP_DEFAULT; app.adjType = "top-activity"; foregroundActivities = true; - procState = ActivityManager.PROCESS_STATE_TOP; + procState = PROCESS_STATE_TOP; } else if (app.instrumentationClass != null) { // Don't want to kill running instrumentation. adj = ProcessList.FOREGROUND_APP_ADJ; @@ -16890,8 +16929,8 @@ public final class ActivityManagerService extends ActivityManagerNative adj = ProcessList.VISIBLE_APP_ADJ; app.adjType = "visible"; } - if (procState > ActivityManager.PROCESS_STATE_TOP) { - procState = ActivityManager.PROCESS_STATE_TOP; + if (procState > PROCESS_STATE_TOP) { + procState = PROCESS_STATE_TOP; } schedGroup = Process.THREAD_GROUP_DEFAULT; app.cached = false; @@ -16903,8 +16942,8 @@ public final class ActivityManagerService extends ActivityManagerNative adj = ProcessList.PERCEPTIBLE_APP_ADJ; app.adjType = "pausing"; } - if (procState > ActivityManager.PROCESS_STATE_TOP) { - procState = ActivityManager.PROCESS_STATE_TOP; + if (procState > PROCESS_STATE_TOP) { + procState = PROCESS_STATE_TOP; } schedGroup = Process.THREAD_GROUP_DEFAULT; app.cached = false; @@ -16943,7 +16982,7 @@ public final class ActivityManagerService extends ActivityManagerNative if (app.foregroundServices) { // The user is aware of this app, so make it visible. adj = ProcessList.PERCEPTIBLE_APP_ADJ; - procState = ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND; + procState = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; app.cached = false; app.adjType = "fg-service"; schedGroup = Process.THREAD_GROUP_DEFAULT; @@ -17472,7 +17511,7 @@ public final class ActivityManagerService extends ActivityManagerNative IApplicationThread thread = myProc.thread; if (thread != null) { try { - if (true || DEBUG_PSS) Slog.d(TAG_PSS, + if (DEBUG_PSS) Slog.d(TAG_PSS, "Requesting dump heap from " + myProc + " to " + heapdumpFile); thread.dumpHeap(true, heapdumpFile.toString(), fd); @@ -18757,7 +18796,7 @@ public final class ActivityManagerService extends ActivityManagerNative + " does not match last path " + mMemWatchDumpFile); return; } - if (true || DEBUG_PSS) Slog.d(TAG_PSS, "Dump heap finished for " + path); + if (DEBUG_PSS) Slog.d(TAG_PSS, "Dump heap finished for " + path); mHandler.sendEmptyMessage(POST_DUMP_HEAP_NOTIFICATION_MSG); } } diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java index ddba1eb..2362d28 100644 --- a/services/core/java/com/android/server/am/ActivityStack.java +++ b/services/core/java/com/android/server/am/ActivityStack.java @@ -1909,7 +1909,7 @@ final class ActivityStack { next.sleeping = false; mService.showAskCompatModeDialogLocked(next); next.app.pendingUiClean = true; - next.app.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_TOP); + next.app.forceProcessStateUpTo(mService.mTopProcessState); next.clearOptionsLocked(); next.app.thread.scheduleResumeActivity(next.appToken, next.app.repProcState, mService.isNextTransitionForward(), resumeAnimOptions); diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java index d08cddc..c2f6bfd 100644 --- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java @@ -1229,7 +1229,7 @@ public final class ActivityStackSupervisor implements DisplayListener { app.hasShownUi = true; app.pendingUiClean = true; } - app.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_TOP); + app.forceProcessStateUpTo(mService.mTopProcessState); app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken, System.identityHashCode(r), r.info, new Configuration(mService.mConfiguration), new Configuration(stack.mOverrideConfig), r.compat, r.launchedFromPackage, diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index ac70d88..ed108c2 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -260,6 +260,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub } } + public boolean isCharging() { + synchronized (mStats) { + return mStats.isCharging(); + } + } + public long computeBatteryTimeRemaining() { synchronized (mStats) { long time = mStats.computeBatteryTimeRemaining(SystemClock.elapsedRealtime()); diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index c7aa94c..cdfcd0c 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -368,6 +368,12 @@ final class ProcessList { case ActivityManager.PROCESS_STATE_TOP: procState = "T "; break; + case ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE: + procState = "FS"; + break; + case ActivityManager.PROCESS_STATE_TOP_SLEEPING: + procState = "TS"; + break; case ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND: procState = "IF"; break; @@ -475,6 +481,8 @@ final class ProcessList { PROC_MEM_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT PROC_MEM_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT_UI PROC_MEM_TOP, // ActivityManager.PROCESS_STATE_TOP + PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE + PROC_MEM_TOP, // ActivityManager.PROCESS_STATE_TOP_SLEEPING PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_BACKUP @@ -492,6 +500,8 @@ final class ProcessList { PSS_SHORT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT PSS_SHORT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI PSS_FIRST_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_TOP + PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE + PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_BACKUP @@ -509,6 +519,8 @@ final class ProcessList { PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI PSS_SHORT_INTERVAL, // ActivityManager.PROCESS_STATE_TOP + PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE + PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_BACKUP @@ -526,6 +538,8 @@ final class ProcessList { PSS_TEST_FIRST_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT PSS_TEST_FIRST_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI PSS_TEST_FIRST_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_TOP + PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE + PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_BACKUP @@ -543,6 +557,8 @@ final class ProcessList { PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_TOP + PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE + PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_BACKUP diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java index 191df2f..7866ddc 100644 --- a/services/core/java/com/android/server/content/SyncManager.java +++ b/services/core/java/com/android/server/content/SyncManager.java @@ -176,6 +176,7 @@ public class SyncManager { volatile private PowerManager.WakeLock mSyncManagerWakeLock; volatile private boolean mDataConnectionIsConnected = false; volatile private boolean mStorageIsLow = false; + volatile private boolean mDeviceIsIdle = false; private final NotificationManager mNotificationMgr; private AlarmManager mAlarmService = null; @@ -221,6 +222,20 @@ public class SyncManager { } }; + private BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + boolean idle = mPowerManager.isDeviceIdleMode(); + mDeviceIsIdle = idle; + if (idle) { + cancelActiveSync( + SyncStorageEngine.EndPoint.USER_ALL_PROVIDER_ALL_ACCOUNTS_ALL, + null /* any sync */); + } else { + sendCheckAlarmsMessage(); + } + } + }; + private BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -425,6 +440,9 @@ public class SyncManager { intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); context.registerReceiver(mStorageIntentReceiver, intentFilter); + intentFilter = new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + context.registerReceiver(mDeviceIdleReceiver, intentFilter); + intentFilter = new IntentFilter(Intent.ACTION_SHUTDOWN); intentFilter.setPriority(100); context.registerReceiver(mShutdownIntentReceiver, intentFilter); @@ -1312,6 +1330,7 @@ public class SyncManager { pw.println(); } pw.print("memory low: "); pw.println(mStorageIsLow); + pw.print("device idle: "); pw.println(mDeviceIsIdle); final AccountAndUser[] accounts = AccountManagerService.getSingleton().getAllAccounts(); @@ -2358,6 +2377,13 @@ public class SyncManager { return Long.MAX_VALUE; } + if (mDeviceIsIdle) { + if (isLoggable) { + Log.v(TAG, "maybeStartNextSync: device idle, skipping"); + } + return Long.MAX_VALUE; + } + // If the accounts aren't known yet then we aren't ready to run. We will be kicked // when the account lookup request does complete. if (mRunningAccounts == INITIAL_ACCOUNTS_ARRAY) { @@ -2984,6 +3010,7 @@ public class SyncManager { // method to be called again if (!mDataConnectionIsConnected) return; if (mStorageIsLow) return; + if (mDeviceIsIdle) return; // When the status bar notification should be raised final long notificationTime = diff --git a/services/core/java/com/android/server/job/controllers/BatteryController.java b/services/core/java/com/android/server/job/controllers/BatteryController.java index 309e034..7c2aead 100644 --- a/services/core/java/com/android/server/job/controllers/BatteryController.java +++ b/services/core/java/com/android/server/job/controllers/BatteryController.java @@ -47,10 +47,6 @@ public class BatteryController extends StateController { private static final Object sCreationLock = new Object(); private static volatile BatteryController sController; - private static final String ACTION_CHARGING_STABLE = - "com.android.server.task.controllers.BatteryController.ACTION_CHARGING_STABLE"; - /** Wait this long after phone is plugged in before doing any work. */ - private static final long STABLE_CHARGING_THRESHOLD_MILLIS = 2 * 60 * 1000; // 2 minutes. private List<JobStatus> mTrackedTasks = new ArrayList<JobStatus>(); private ChargingTracker mChargeTracker; @@ -91,9 +87,6 @@ public class BatteryController extends StateController { taskStatus.chargingConstraintSatisfied.set(isOnStablePower); } } - if (isOnStablePower) { - mChargeTracker.setStableChargingAlarm(); - } } @Override @@ -131,8 +124,6 @@ public class BatteryController extends StateController { } public class ChargingTracker extends BroadcastReceiver { - private final AlarmManager mAlarm; - private final PendingIntent mStableChargingTriggerIntent; /** * Track whether we're "charging", where charging means that we're ready to commit to * doing work. @@ -142,9 +133,6 @@ public class BatteryController extends StateController { private boolean mBatteryHealthy; public ChargingTracker() { - mAlarm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - Intent intent = new Intent(ACTION_CHARGING_STABLE); - mStableChargingTriggerIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); } public void startTracking() { @@ -154,10 +142,8 @@ public class BatteryController extends StateController { filter.addAction(Intent.ACTION_BATTERY_LOW); filter.addAction(Intent.ACTION_BATTERY_OKAY); // Charging/not charging. - filter.addAction(Intent.ACTION_POWER_CONNECTED); - filter.addAction(Intent.ACTION_POWER_DISCONNECTED); - // Charging stable. - filter.addAction(ACTION_CHARGING_STABLE); + filter.addAction(BatteryManager.ACTION_CHARGING); + filter.addAction(BatteryManager.ACTION_DISCHARGING); mContext.registerReceiver(this, filter); // Initialise tracker state. @@ -195,44 +181,20 @@ public class BatteryController extends StateController { } mBatteryHealthy = true; maybeReportNewChargingState(); - } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) { + } else if (BatteryManager.ACTION_CHARGING.equals(action)) { if (DEBUG) { - Slog.d(TAG, "Received charging intent, setting alarm for " - + STABLE_CHARGING_THRESHOLD_MILLIS); + Slog.d(TAG, "Received charging intent, fired @ " + + SystemClock.elapsedRealtime()); } - // Set up an alarm for ACTION_CHARGING_STABLE - we don't want to kick off tasks - // here if the user unplugs the phone immediately. - setStableChargingAlarm(); mCharging = true; - } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) { + maybeReportNewChargingState(); + } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) { if (DEBUG) { - Slog.d(TAG, "Disconnected from power, cancelling any set alarms."); + Slog.d(TAG, "Disconnected from power."); } - // If an alarm is set, breathe a sigh of relief and cancel it - crisis averted. - mAlarm.cancel(mStableChargingTriggerIntent); mCharging = false; maybeReportNewChargingState(); - }else if (ACTION_CHARGING_STABLE.equals(action)) { - // Here's where we actually do the notify for a task being ready. - if (DEBUG) { - Slog.d(TAG, "Stable charging fired @ " + SystemClock.elapsedRealtime() - + " charging: " + mCharging); - } - if (mCharging) { // Should never receive this intent if mCharging is false. - maybeReportNewChargingState(); - } - } - } - - void setStableChargingAlarm() { - final long alarmTriggerElapsed = - SystemClock.elapsedRealtime() + STABLE_CHARGING_THRESHOLD_MILLIS; - if (DEBUG) { - Slog.d(TAG, "Setting stable alarm to go off in " + - (STABLE_CHARGING_THRESHOLD_MILLIS / 1000) + "s"); } - mAlarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTriggerElapsed, - mStableChargingTriggerIntent); } } diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java index ab53fbc..fc2eced 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -25,12 +25,10 @@ import android.os.IInterface; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; -import android.provider.Settings.Global; import android.service.notification.Condition; import android.service.notification.ConditionProviderService; import android.service.notification.IConditionListener; import android.service.notification.IConditionProvider; -import android.service.notification.ZenModeConfig; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; @@ -41,50 +39,44 @@ import com.android.server.notification.NotificationManagerService.DumpFilter; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; -import java.util.Objects; public class ConditionProviders extends ManagedServices { - private static final Condition[] NO_CONDITIONS = new Condition[0]; - - private final ZenModeHelper mZenModeHelper; - private final ArrayMap<IBinder, IConditionListener> mListeners - = new ArrayMap<IBinder, IConditionListener>(); - private final ArrayList<ConditionRecord> mRecords = new ArrayList<ConditionRecord>(); - private final ArraySet<String> mSystemConditionProviders; - private final CountdownConditionProvider mCountdown; - private final DowntimeConditionProvider mDowntime; - private final NextAlarmConditionProvider mNextAlarm; - private final NextAlarmTracker mNextAlarmTracker; - - private Condition mExitCondition; - private ComponentName mExitConditionComponent; - - public ConditionProviders(Context context, Handler handler, - UserProfiles userProfiles, ZenModeHelper zenModeHelper) { + private final ArrayList<ConditionRecord> mRecords = new ArrayList<>(); + private final ArrayMap<IBinder, IConditionListener> mListeners = new ArrayMap<>(); + private final ArraySet<String> mSystemConditionProviderNames; + private final ArraySet<SystemConditionProviderService> mSystemConditionProviders + = new ArraySet<>(); + + private Callback mCallback; + + public ConditionProviders(Context context, Handler handler, UserProfiles userProfiles) { super(context, handler, new Object(), userProfiles); - mZenModeHelper = zenModeHelper; - mZenModeHelper.addCallback(new ZenModeHelperCallback()); - mSystemConditionProviders = safeSet(PropConfig.getStringArray(mContext, + mSystemConditionProviderNames = safeSet(PropConfig.getStringArray(mContext, "system.condition.providers", R.array.config_system_condition_providers)); - final boolean countdown = mSystemConditionProviders.contains(ZenModeConfig.COUNTDOWN_PATH); - final boolean downtime = mSystemConditionProviders.contains(ZenModeConfig.DOWNTIME_PATH); - final boolean nextAlarm = mSystemConditionProviders.contains(ZenModeConfig.NEXT_ALARM_PATH); - mNextAlarmTracker = (downtime || nextAlarm) ? new NextAlarmTracker(mContext) : null; - mCountdown = countdown ? new CountdownConditionProvider() : null; - mDowntime = downtime ? new DowntimeConditionProvider(this, mNextAlarmTracker, - mZenModeHelper) : null; - mNextAlarm = nextAlarm ? new NextAlarmConditionProvider(mNextAlarmTracker) : null; - loadZenConfig(); } - public boolean isSystemConditionProviderEnabled(String path) { - return mSystemConditionProviders.contains(path); + public void setCallback(Callback callback) { + mCallback = callback; + } + + public boolean isSystemProviderEnabled(String path) { + return mSystemConditionProviderNames.contains(path); + } + + public void addSystemProvider(SystemConditionProviderService service) { + mSystemConditionProviders.add(service); + service.attachBase(mContext); + registerService(service.asInterface(), service.getComponent(), UserHandle.USER_OWNER); + } + + public Iterable<SystemConditionProviderService> getSystemProviders() { + return mSystemConditionProviders; } @Override protected Config getConfig() { - Config c = new Config(); + final Config c = new Config(); c.caption = "condition provider"; c.serviceInterface = ConditionProviderService.SERVICE_INTERFACE; c.secureSettingName = Settings.Secure.ENABLED_CONDITION_PROVIDERS; @@ -98,12 +90,6 @@ public class ConditionProviders extends ManagedServices { public void dump(PrintWriter pw, DumpFilter filter) { super.dump(pw, filter); synchronized(mMutex) { - if (filter == null) { - pw.print(" mListeners("); pw.print(mListeners.size()); pw.println("):"); - for (int i = 0; i < mListeners.size(); i++) { - pw.print(" "); pw.println(mListeners.keyAt(i)); - } - } pw.print(" mRecords("); pw.print(mRecords.size()); pw.println("):"); for (int i = 0; i < mRecords.size(); i++) { final ConditionRecord r = mRecords.get(i); @@ -115,18 +101,15 @@ public class ConditionProviders extends ManagedServices { } } } - pw.print(" mSystemConditionProviders: "); pw.println(mSystemConditionProviders); - if (mCountdown != null) { - mCountdown.dump(pw, filter); - } - if (mDowntime != null) { - mDowntime.dump(pw, filter); - } - if (mNextAlarm != null) { - mNextAlarm.dump(pw, filter); + if (filter == null) { + pw.print(" mListeners("); pw.print(mListeners.size()); pw.println("):"); + for (int i = 0; i < mListeners.size(); i++) { + pw.print(" "); pw.println(mListeners.keyAt(i)); + } } - if (mNextAlarmTracker != null) { - mNextAlarmTracker.dump(pw, filter); + pw.print(" mSystemConditionProviders: "); pw.println(mSystemConditionProviderNames); + for (int i = 0; i < mSystemConditionProviders.size(); i++) { + mSystemConditionProviders.valueAt(i).dump(pw, filter); } } @@ -138,31 +121,16 @@ public class ConditionProviders extends ManagedServices { @Override public void onBootPhaseAppsCanStart() { super.onBootPhaseAppsCanStart(); - if (mNextAlarmTracker != null) { - mNextAlarmTracker.init(); - } - if (mCountdown != null) { - mCountdown.attachBase(mContext); - registerService(mCountdown.asInterface(), CountdownConditionProvider.COMPONENT, - UserHandle.USER_OWNER); - } - if (mDowntime != null) { - mDowntime.attachBase(mContext); - registerService(mDowntime.asInterface(), DowntimeConditionProvider.COMPONENT, - UserHandle.USER_OWNER); - } - if (mNextAlarm != null) { - mNextAlarm.attachBase(mContext); - registerService(mNextAlarm.asInterface(), NextAlarmConditionProvider.COMPONENT, - UserHandle.USER_OWNER); + if (mCallback != null) { + mCallback.onBootComplete(); } } @Override public void onUserSwitched() { super.onUserSwitched(); - if (mNextAlarmTracker != null) { - mNextAlarmTracker.onUserSwitched(); + if (mCallback != null) { + mCallback.onUserSwitched(); } } @@ -174,24 +142,6 @@ public class ConditionProviders extends ManagedServices { } catch (RemoteException e) { // we tried } - synchronized (mMutex) { - if (info.component.equals(mExitConditionComponent)) { - // ensure record exists, we'll wire it up and subscribe below - final ConditionRecord manualRecord = - getRecordLocked(mExitCondition.id, mExitConditionComponent); - manualRecord.isManual = true; - } - final int N = mRecords.size(); - for(int i = 0; i < N; i++) { - final ConditionRecord r = mRecords.get(i); - if (!r.component.equals(info.component)) continue; - r.info = info; - // if automatic or manual, auto-subscribe - if (r.isAutomatic || r.isManual) { - subscribeLocked(r); - } - } - } } @Override @@ -200,15 +150,6 @@ public class ConditionProviders extends ManagedServices { for (int i = mRecords.size() - 1; i >= 0; i--) { final ConditionRecord r = mRecords.get(i); if (!r.component.equals(removed.component)) continue; - if (r.isManual) { - // removing the current manual condition, exit zen - onManualConditionClearing(); - mZenModeHelper.setZenMode(Global.ZEN_MODE_OFF, "manualServiceRemoved"); - } - if (r.isAutomatic) { - // removing an automatic condition, exit zen - mZenModeHelper.setZenMode(Global.ZEN_MODE_OFF, "automaticServiceRemoved"); - } mRecords.remove(i); } } @@ -219,9 +160,9 @@ public class ConditionProviders extends ManagedServices { } } - public void requestZenModeConditions(IConditionListener callback, int relevance) { + public void requestConditions(IConditionListener callback, int relevance) { synchronized(mMutex) { - if (DEBUG) Slog.d(TAG, "requestZenModeConditions callback=" + callback + if (DEBUG) Slog.d(TAG, "requestConditions callback=" + callback + " relevance=" + Condition.relevanceToString(relevance)); if (callback == null) return; relevance = relevance & (Condition.FLAG_RELEVANT_NOW | Condition.FLAG_RELEVANT_ALWAYS); @@ -262,7 +203,8 @@ public class ConditionProviders extends ManagedServices { return rt; } - private ConditionRecord getRecordLocked(Uri id, ComponentName component) { + private ConditionRecord getRecordLocked(Uri id, ComponentName component, boolean create) { + if (id == null || component == null) return null; final int N = mRecords.size(); for (int i = 0; i < N; i++) { final ConditionRecord r = mRecords.get(i); @@ -270,9 +212,12 @@ public class ConditionProviders extends ManagedServices { return r; } } - final ConditionRecord r = new ConditionRecord(id, component); - mRecords.add(r); - return r; + if (create) { + final ConditionRecord r = new ConditionRecord(id, component); + mRecords.add(r); + return r; + } + return null; } public void notifyConditions(String pkg, ManagedServiceInfo info, Condition[] conditions) { @@ -291,99 +236,48 @@ public class ConditionProviders extends ManagedServices { } for (int i = 0; i < N; i++) { final Condition c = conditions[i]; - final ConditionRecord r = getRecordLocked(c.id, info.component); - final Condition oldCondition = r.condition; - final boolean conditionUpdate = oldCondition != null && !oldCondition.equals(c); + final ConditionRecord r = getRecordLocked(c.id, info.component, true /*create*/); r.info = info; r.condition = c; - // if manual, exit zen if false (or failed), update if true (and changed) - if (r.isManual) { - if (c.state == Condition.STATE_FALSE || c.state == Condition.STATE_ERROR) { - final boolean failed = c.state == Condition.STATE_ERROR; - if (failed) { - Slog.w(TAG, "Exit zen: manual condition failed: " + c); - } else if (DEBUG) { - Slog.d(TAG, "Exit zen: manual condition false: " + c); - } - onManualConditionClearing(); - mZenModeHelper.setZenMode(Settings.Global.ZEN_MODE_OFF, - "manualConditionExit"); - unsubscribeLocked(r); - r.isManual = false; - } else if (c.state == Condition.STATE_TRUE && conditionUpdate) { - if (DEBUG) Slog.d(TAG, "Current condition updated, still true. old=" - + oldCondition + " new=" + c); - setZenModeCondition(c, "conditionUpdate"); - } - } - // if automatic, exit zen if false (or failed), enter zen if true - if (r.isAutomatic) { - if (c.state == Condition.STATE_FALSE || c.state == Condition.STATE_ERROR) { - final boolean failed = c.state == Condition.STATE_ERROR; - if (failed) { - Slog.w(TAG, "Exit zen: automatic condition failed: " + c); - } else if (DEBUG) { - Slog.d(TAG, "Exit zen: automatic condition false: " + c); - } - mZenModeHelper.setZenMode(Settings.Global.ZEN_MODE_OFF, - "automaticConditionExit"); - } else if (c.state == Condition.STATE_TRUE) { - Slog.d(TAG, "Enter zen: automatic condition true: " + c); - mZenModeHelper.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, - "automaticConditionEnter"); - } + if (mCallback != null) { + mCallback.onConditionChanged(c.id, c); } } } } - private void ensureRecordExists(Condition condition, IConditionProvider provider, - ComponentName component) { + public void ensureRecordExists(ComponentName component, Uri conditionId, + IConditionProvider provider) { // constructed by convention, make sure the record exists... - final ConditionRecord r = getRecordLocked(condition.id, component); + final ConditionRecord r = getRecordLocked(conditionId, component, true /*create*/); if (r.info == null) { // ... and is associated with the in-process service r.info = checkServiceTokenLocked(provider); } } - public void setZenModeCondition(Condition condition, String reason) { - if (DEBUG) Slog.d(TAG, "setZenModeCondition " + condition + " reason=" + reason); - synchronized(mMutex) { - ComponentName conditionComponent = null; - if (condition != null) { - if (mCountdown != null && ZenModeConfig.isValidCountdownConditionId(condition.id)) { - ensureRecordExists(condition, mCountdown.asInterface(), - CountdownConditionProvider.COMPONENT); - } - if (mDowntime != null && ZenModeConfig.isValidDowntimeConditionId(condition.id)) { - ensureRecordExists(condition, mDowntime.asInterface(), - DowntimeConditionProvider.COMPONENT); - } - } - final int N = mRecords.size(); - for (int i = 0; i < N; i++) { - final ConditionRecord r = mRecords.get(i); - final boolean idEqual = condition != null && r.id.equals(condition.id); - if (r.isManual && !idEqual) { - // was previous manual condition, unsubscribe - unsubscribeLocked(r); - r.isManual = false; - } else if (idEqual && !r.isManual) { - // is new manual condition, subscribe - subscribeLocked(r); - r.isManual = true; - } - if (idEqual) { - conditionComponent = r.component; - } + public boolean subscribeIfNecessary(ComponentName component, Uri conditionId) { + synchronized (mMutex) { + final ConditionRecord r = getRecordLocked(conditionId, component, false /*create*/); + if (r == null) { + Slog.w(TAG, "Unable to subscribe to " + component + " " + conditionId); + return false; } - if (!Objects.equals(mExitCondition, condition)) { - mExitCondition = condition; - mExitConditionComponent = conditionComponent; - ZenLog.traceExitCondition(mExitCondition, mExitConditionComponent, reason); - saveZenConfigLocked(); + if (r.subscribed) return true; + subscribeLocked(r); + return r.subscribed; + } + } + + public void unsubscribeIfNecessary(ComponentName component, Uri conditionId) { + synchronized (mMutex) { + final ConditionRecord r = getRecordLocked(conditionId, component, false /*create*/); + if (r == null) { + Slog.w(TAG, "Unable to unsubscribe to " + component + " " + conditionId); + return; } + if (!r.subscribed) return; + unsubscribeLocked(r);; } } @@ -393,8 +287,9 @@ public class ConditionProviders extends ManagedServices { RemoteException re = null; if (provider != null) { try { - Slog.d(TAG, "Subscribing to " + r.id + " with " + provider); + Slog.d(TAG, "Subscribing to " + r.id + " with " + r.component); provider.onSubscribe(r.id); + r.subscribed = true; } catch (RemoteException e) { Slog.w(TAG, "Error subscribing to " + r, e); re = e; @@ -417,53 +312,6 @@ public class ConditionProviders extends ManagedServices { return rt; } - public void setAutomaticZenModeConditions(Uri[] conditionIds) { - setAutomaticZenModeConditions(conditionIds, true /*save*/); - } - - private void setAutomaticZenModeConditions(Uri[] conditionIds, boolean save) { - if (DEBUG) Slog.d(TAG, "setAutomaticZenModeConditions " - + (conditionIds == null ? null : Arrays.asList(conditionIds))); - synchronized(mMutex) { - final ArraySet<Uri> newIds = safeSet(conditionIds); - final int N = mRecords.size(); - boolean changed = false; - for (int i = 0; i < N; i++) { - final ConditionRecord r = mRecords.get(i); - final boolean automatic = newIds.contains(r.id); - if (!r.isAutomatic && automatic) { - // subscribe to new automatic - subscribeLocked(r); - r.isAutomatic = true; - changed = true; - } else if (r.isAutomatic && !automatic) { - // unsubscribe from old automatic - unsubscribeLocked(r); - r.isAutomatic = false; - changed = true; - } - } - if (save && changed) { - saveZenConfigLocked(); - } - } - } - - public Condition[] getAutomaticZenModeConditions() { - synchronized(mMutex) { - final int N = mRecords.size(); - ArrayList<Condition> rt = null; - for (int i = 0; i < N; i++) { - final ConditionRecord r = mRecords.get(i); - if (r.isAutomatic && r.condition != null) { - if (rt == null) rt = new ArrayList<Condition>(); - rt.add(r.condition); - } - } - return rt == null ? NO_CONDITIONS : rt.toArray(new Condition[rt.size()]); - } - } - private void unsubscribeLocked(ConditionRecord r) { if (DEBUG) Slog.d(TAG, "unsubscribeLocked " + r); final IConditionProvider provider = provider(r); @@ -475,6 +323,7 @@ public class ConditionProviders extends ManagedServices { Slog.w(TAG, "Error unsubscribing to " + r, e); re = e; } + r.subscribed = false; } ZenLog.traceUnsubscribe(r != null ? r.id : null, provider, re); } @@ -495,7 +344,7 @@ public class ConditionProviders extends ManagedServices { for (int i = mRecords.size() - 1; i >= 0; i--) { final ConditionRecord r = mRecords.get(i); if (r.info != info) continue; - if (r.isManual || r.isAutomatic) continue; + if (r.subscribed) continue; mRecords.remove(i); } try { @@ -506,103 +355,12 @@ public class ConditionProviders extends ManagedServices { } } - private void loadZenConfig() { - final ZenModeConfig config = mZenModeHelper.getConfig(); - if (config == null) { - if (DEBUG) Slog.d(TAG, "loadZenConfig: no config"); - return; - } - synchronized (mMutex) { - final boolean changingExit = !Objects.equals(mExitCondition, config.exitCondition); - mExitCondition = config.exitCondition; - mExitConditionComponent = config.exitConditionComponent; - if (changingExit) { - ZenLog.traceExitCondition(mExitCondition, mExitConditionComponent, "config"); - } - if (mDowntime != null) { - mDowntime.setConfig(config); - } - if (config.conditionComponents == null || config.conditionIds == null - || config.conditionComponents.length != config.conditionIds.length) { - if (DEBUG) Slog.d(TAG, "loadZenConfig: no conditions"); - setAutomaticZenModeConditions(null, false /*save*/); - return; - } - final ArraySet<Uri> newIds = new ArraySet<Uri>(); - final int N = config.conditionComponents.length; - for (int i = 0; i < N; i++) { - final ComponentName component = config.conditionComponents[i]; - final Uri id = config.conditionIds[i]; - if (component != null && id != null) { - getRecordLocked(id, component); // ensure record exists - newIds.add(id); - } - } - if (DEBUG) Slog.d(TAG, "loadZenConfig: N=" + N); - setAutomaticZenModeConditions(newIds.toArray(new Uri[newIds.size()]), false /*save*/); - } - } - - private void saveZenConfigLocked() { - ZenModeConfig config = mZenModeHelper.getConfig(); - if (config == null) return; - config = config.copy(); - final ArrayList<ConditionRecord> automatic = new ArrayList<ConditionRecord>(); - final int automaticN = mRecords.size(); - for (int i = 0; i < automaticN; i++) { - final ConditionRecord r = mRecords.get(i); - if (r.isAutomatic) { - automatic.add(r); - } - } - if (automatic.isEmpty()) { - config.conditionComponents = null; - config.conditionIds = null; - } else { - final int N = automatic.size(); - config.conditionComponents = new ComponentName[N]; - config.conditionIds = new Uri[N]; - for (int i = 0; i < N; i++) { - final ConditionRecord r = automatic.get(i); - config.conditionComponents[i] = r.component; - config.conditionIds[i] = r.id; - } - } - config.exitCondition = mExitCondition; - config.exitConditionComponent = mExitConditionComponent; - if (DEBUG) Slog.d(TAG, "Setting zen config to: " + config); - mZenModeHelper.setConfig(config); - } - - private void onManualConditionClearing() { - if (mDowntime != null) { - mDowntime.onManualConditionClearing(); - } - } - - private class ZenModeHelperCallback extends ZenModeHelper.Callback { - @Override - void onConfigChanged() { - loadZenConfig(); - } - - @Override - void onZenModeChanged() { - final int mode = mZenModeHelper.getZenMode(); - if (mode == Global.ZEN_MODE_OFF) { - // ensure any manual condition is cleared - setZenModeCondition(null, "zenOff"); - } - } - } - private static class ConditionRecord { public final Uri id; public final ComponentName component; public Condition condition; public ManagedServiceInfo info; - public boolean isAutomatic; - public boolean isManual; + public boolean subscribed; private ConditionRecord(Uri id, ComponentName component) { this.id = id; @@ -612,10 +370,16 @@ public class ConditionProviders extends ManagedServices { @Override public String toString() { final StringBuilder sb = new StringBuilder("ConditionRecord[id=") - .append(id).append(",component=").append(component); - if (isAutomatic) sb.append(",automatic"); - if (isManual) sb.append(",manual"); + .append(id).append(",component=").append(component) + .append(",subscribed=").append(subscribed); return sb.append(']').toString(); } } + + public interface Callback { + void onBootComplete(); + void onConditionChanged(Uri id, Condition condition); + void onUserSwitched(); + } + } diff --git a/services/core/java/com/android/server/notification/CountdownConditionProvider.java b/services/core/java/com/android/server/notification/CountdownConditionProvider.java index 37aacaa..d223353 100644 --- a/services/core/java/com/android/server/notification/CountdownConditionProvider.java +++ b/services/core/java/com/android/server/notification/CountdownConditionProvider.java @@ -25,7 +25,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.service.notification.Condition; -import android.service.notification.ConditionProviderService; import android.service.notification.IConditionProvider; import android.service.notification.ZenModeConfig; import android.text.format.DateUtils; @@ -38,7 +37,7 @@ import java.io.PrintWriter; import java.util.Date; /** Built-in zen condition provider for simple time-based conditions */ -public class CountdownConditionProvider extends ConditionProviderService { +public class CountdownConditionProvider extends SystemConditionProviderService { private static final String TAG = "CountdownConditions"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -59,6 +58,27 @@ public class CountdownConditionProvider extends ConditionProviderService { if (DEBUG) Slog.d(TAG, "new CountdownConditionProvider()"); } + @Override + public ComponentName getComponent() { + return COMPONENT; + } + + @Override + public boolean isValidConditionid(Uri id) { + return ZenModeConfig.isValidCountdownConditionId(id); + } + + @Override + public void attachBase(Context base) { + attachBaseContext(base); + } + + @Override + public IConditionProvider asInterface() { + return (IConditionProvider) onBind(null); + } + + @Override public void dump(PrintWriter pw, DumpFilter filter) { pw.println(" CountdownConditionProvider:"); pw.print(" mConnected="); pw.println(mConnected); @@ -154,11 +174,4 @@ public class CountdownConditionProvider extends ConditionProviderService { return new Date(time) + " (" + time + ")"; } - public void attachBase(Context base) { - attachBaseContext(base); - } - - public IConditionProvider asInterface() { - return (IConditionProvider) onBind(null); - } } diff --git a/services/core/java/com/android/server/notification/DowntimeConditionProvider.java b/services/core/java/com/android/server/notification/DowntimeConditionProvider.java deleted file mode 100644 index df4ecfd..0000000 --- a/services/core/java/com/android/server/notification/DowntimeConditionProvider.java +++ /dev/null @@ -1,409 +0,0 @@ -/** - * Copyright (c) 2014, 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.server.notification; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.app.AlarmManager.AlarmClockInfo; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.Uri; -import android.provider.Settings.Global; -import android.service.notification.Condition; -import android.service.notification.ConditionProviderService; -import android.service.notification.IConditionProvider; -import android.service.notification.ZenModeConfig; -import android.service.notification.ZenModeConfig.DowntimeInfo; -import android.text.format.DateFormat; -import android.util.ArraySet; -import android.util.Log; -import android.util.Slog; -import android.util.TimeUtils; - -import com.android.internal.R; -import com.android.server.notification.NotificationManagerService.DumpFilter; - -import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.Objects; -import java.util.TimeZone; - -/** Built-in zen condition provider for managing downtime */ -public class DowntimeConditionProvider extends ConditionProviderService { - private static final String TAG = "DowntimeConditions"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - public static final ComponentName COMPONENT = - new ComponentName("android", DowntimeConditionProvider.class.getName()); - - private static final String ENTER_ACTION = TAG + ".enter"; - private static final int ENTER_CODE = 100; - private static final String EXIT_ACTION = TAG + ".exit"; - private static final int EXIT_CODE = 101; - private static final String EXTRA_TIME = "time"; - - private static final long SECONDS = 1000; - private static final long MINUTES = 60 * SECONDS; - private static final long HOURS = 60 * MINUTES; - - private final Context mContext = this; - private final DowntimeCalendar mCalendar = new DowntimeCalendar(); - private final FiredAlarms mFiredAlarms = new FiredAlarms(); - private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>(); - private final ConditionProviders mConditionProviders; - private final NextAlarmTracker mTracker; - private final ZenModeHelper mZenModeHelper; - - private boolean mConnected; - private long mLookaheadThreshold; - private ZenModeConfig mConfig; - private boolean mDowntimed; - private boolean mConditionClearing; - private boolean mRequesting; - - public DowntimeConditionProvider(ConditionProviders conditionProviders, - NextAlarmTracker tracker, ZenModeHelper zenModeHelper) { - if (DEBUG) Slog.d(TAG, "new DowntimeConditionProvider()"); - mConditionProviders = conditionProviders; - mTracker = tracker; - mZenModeHelper = zenModeHelper; - } - - public void dump(PrintWriter pw, DumpFilter filter) { - pw.println(" DowntimeConditionProvider:"); - pw.print(" mConnected="); pw.println(mConnected); - pw.print(" mSubscriptions="); pw.println(mSubscriptions); - pw.print(" mLookaheadThreshold="); pw.print(mLookaheadThreshold); - pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")"); - pw.print(" mCalendar="); pw.println(mCalendar); - pw.print(" mFiredAlarms="); pw.println(mFiredAlarms); - pw.print(" mDowntimed="); pw.println(mDowntimed); - pw.print(" mConditionClearing="); pw.println(mConditionClearing); - pw.print(" mRequesting="); pw.println(mRequesting); - } - - public void attachBase(Context base) { - attachBaseContext(base); - } - - public IConditionProvider asInterface() { - return (IConditionProvider) onBind(null); - } - - @Override - public void onConnected() { - if (DEBUG) Slog.d(TAG, "onConnected"); - mConnected = true; - mLookaheadThreshold = PropConfig.getInt(mContext, "downtime.condition.lookahead", - R.integer.config_downtime_condition_lookahead_threshold_hrs) * HOURS; - final IntentFilter filter = new IntentFilter(); - filter.addAction(ENTER_ACTION); - filter.addAction(EXIT_ACTION); - filter.addAction(Intent.ACTION_TIME_CHANGED); - filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); - mContext.registerReceiver(mReceiver, filter); - mTracker.addCallback(mTrackerCallback); - mZenModeHelper.addCallback(mZenCallback); - init(); - } - - @Override - public void onDestroy() { - if (DEBUG) Slog.d(TAG, "onDestroy"); - mTracker.removeCallback(mTrackerCallback); - mZenModeHelper.removeCallback(mZenCallback); - mConnected = false; - } - - @Override - public void onRequestConditions(int relevance) { - if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance); - if (!mConnected) return; - mRequesting = (relevance & Condition.FLAG_RELEVANT_NOW) != 0; - evaluateSubscriptions(); - } - - @Override - public void onSubscribe(Uri conditionId) { - if (DEBUG) Slog.d(TAG, "onSubscribe conditionId=" + conditionId); - final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId); - if (downtime == null) return; - mFiredAlarms.clear(); - mSubscriptions.add(conditionId); - notifyCondition(downtime); - } - - private boolean shouldShowCondition() { - final long now = System.currentTimeMillis(); - if (DEBUG) Slog.d(TAG, "shouldShowCondition now=" + mCalendar.isInDowntime(now) - + " lookahead=" - + (mCalendar.nextDowntimeStart(now) <= (now + mLookaheadThreshold))); - return mCalendar.isInDowntime(now) - || mCalendar.nextDowntimeStart(now) <= (now + mLookaheadThreshold); - } - - private void notifyCondition(DowntimeInfo downtime) { - if (mConfig == null) { - // we don't know yet - notifyCondition(createCondition(downtime, Condition.STATE_UNKNOWN)); - return; - } - if (!downtime.equals(mConfig.toDowntimeInfo())) { - // not the configured downtime, consider it false - notifyCondition(createCondition(downtime, Condition.STATE_FALSE)); - return; - } - if (!shouldShowCondition()) { - // configured downtime, but not within the time range - notifyCondition(createCondition(downtime, Condition.STATE_FALSE)); - return; - } - if (isZenNone() && mFiredAlarms.findBefore(System.currentTimeMillis())) { - // within the configured time range, but wake up if none and the next alarm is fired - notifyCondition(createCondition(downtime, Condition.STATE_FALSE)); - return; - } - // within the configured time range, condition still valid - notifyCondition(createCondition(downtime, Condition.STATE_TRUE)); - } - - private boolean isZenNone() { - return mZenModeHelper.getZenMode() == Global.ZEN_MODE_NO_INTERRUPTIONS; - } - - private boolean isZenOff() { - return mZenModeHelper.getZenMode() == Global.ZEN_MODE_OFF; - } - - private void evaluateSubscriptions() { - ArraySet<Uri> conditions = mSubscriptions; - if (mConfig != null && mRequesting && shouldShowCondition()) { - final Uri id = ZenModeConfig.toDowntimeConditionId(mConfig.toDowntimeInfo()); - if (!conditions.contains(id)) { - conditions = new ArraySet<Uri>(conditions); - conditions.add(id); - } - } - for (Uri conditionId : conditions) { - final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId); - if (downtime != null) { - notifyCondition(downtime); - } - } - } - - @Override - public void onUnsubscribe(Uri conditionId) { - final boolean current = mSubscriptions.contains(conditionId); - if (DEBUG) Slog.d(TAG, "onUnsubscribe conditionId=" + conditionId + " current=" + current); - mSubscriptions.remove(conditionId); - mFiredAlarms.clear(); - } - - public void setConfig(ZenModeConfig config) { - if (Objects.equals(mConfig, config)) return; - final boolean downtimeChanged = mConfig == null || config == null - || !mConfig.toDowntimeInfo().equals(config.toDowntimeInfo()); - mConfig = config; - if (DEBUG) Slog.d(TAG, "setConfig downtimeChanged=" + downtimeChanged); - if (mConnected && downtimeChanged) { - mDowntimed = false; - init(); - } - // when active, mark downtime as entered for today - if (mConfig != null && mConfig.exitCondition != null - && ZenModeConfig.isValidDowntimeConditionId(mConfig.exitCondition.id)) { - mDowntimed = true; - } - } - - public void onManualConditionClearing() { - mConditionClearing = true; - } - - private Condition createCondition(DowntimeInfo downtime, int state) { - if (downtime == null) return null; - final Uri id = ZenModeConfig.toDowntimeConditionId(downtime); - final String skeleton = DateFormat.is24HourFormat(mContext) ? "Hm" : "hma"; - final Locale locale = Locale.getDefault(); - final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton); - final long now = System.currentTimeMillis(); - long endTime = mCalendar.getNextTime(now, downtime.endHour, downtime.endMinute); - if (isZenNone()) { - final AlarmClockInfo nextAlarm = mTracker.getNextAlarm(); - final long nextAlarmTime = nextAlarm != null ? nextAlarm.getTriggerTime() : 0; - if (nextAlarmTime > now && nextAlarmTime < endTime) { - endTime = nextAlarmTime; - } - } - final String formatted = new SimpleDateFormat(pattern, locale).format(new Date(endTime)); - final String summary = mContext.getString(R.string.downtime_condition_summary, formatted); - final String line1 = mContext.getString(R.string.downtime_condition_line_one); - return new Condition(id, summary, line1, formatted, 0, state, Condition.FLAG_RELEVANT_NOW); - } - - private void init() { - mCalendar.setDowntimeInfo(mConfig != null ? mConfig.toDowntimeInfo() : null); - evaluateSubscriptions(); - updateAlarms(); - evaluateAutotrigger(); - } - - private void updateAlarms() { - if (mConfig == null) return; - updateAlarm(ENTER_ACTION, ENTER_CODE, mConfig.sleepStartHour, mConfig.sleepStartMinute); - updateAlarm(EXIT_ACTION, EXIT_CODE, mConfig.sleepEndHour, mConfig.sleepEndMinute); - } - - - private void updateAlarm(String action, int requestCode, int hr, int min) { - final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - final long now = System.currentTimeMillis(); - final long time = mCalendar.getNextTime(now, hr, min); - final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode, - new Intent(action) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) - .putExtra(EXTRA_TIME, time), - PendingIntent.FLAG_UPDATE_CURRENT); - alarms.cancel(pendingIntent); - if (mConfig.sleepMode != null) { - if (DEBUG) Slog.d(TAG, String.format("Scheduling %s for %s, in %s, now=%s", - action, ts(time), NextAlarmTracker.formatDuration(time - now), ts(now))); - alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); - } - } - - private static String ts(long time) { - return new Date(time) + " (" + time + ")"; - } - - private void onEvaluateNextAlarm(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { - if (!booted) return; // we don't know yet - if (DEBUG) Slog.d(TAG, "onEvaluateNextAlarm " + mTracker.formatAlarmDebug(nextAlarm)); - if (nextAlarm != null && wakeupTime > 0 && System.currentTimeMillis() > wakeupTime) { - if (DEBUG) Slog.d(TAG, "Alarm fired: " + mTracker.formatAlarmDebug(wakeupTime)); - mFiredAlarms.add(wakeupTime); - } - evaluateSubscriptions(); - } - - private void evaluateAutotrigger() { - String skipReason = null; - if (mConfig == null) { - skipReason = "no config"; - } else if (mDowntimed) { - skipReason = "already downtimed"; - } else if (mZenModeHelper.getZenMode() != Global.ZEN_MODE_OFF) { - skipReason = "already in zen"; - } else if (!mCalendar.isInDowntime(System.currentTimeMillis())) { - skipReason = "not in downtime"; - } - if (skipReason != null) { - ZenLog.traceDowntimeAutotrigger("Autotrigger skipped: " + skipReason); - return; - } - ZenLog.traceDowntimeAutotrigger("Autotrigger fired"); - mZenModeHelper.setZenMode(mConfig.sleepNone ? Global.ZEN_MODE_NO_INTERRUPTIONS - : Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, "downtime"); - final Condition condition = createCondition(mConfig.toDowntimeInfo(), Condition.STATE_TRUE); - mConditionProviders.setZenModeCondition(condition, "downtime"); - } - - private BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - final long now = System.currentTimeMillis(); - if (ENTER_ACTION.equals(action) || EXIT_ACTION.equals(action)) { - final long schTime = intent.getLongExtra(EXTRA_TIME, 0); - if (DEBUG) Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s", - action, ts(schTime), ts(now), now - schTime)); - if (ENTER_ACTION.equals(action)) { - evaluateAutotrigger(); - } else /*EXIT_ACTION*/ { - mDowntimed = false; - } - mFiredAlarms.clear(); - } else if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) { - if (DEBUG) Slog.d(TAG, "timezone changed to " + TimeZone.getDefault()); - mCalendar.setTimeZone(TimeZone.getDefault()); - mFiredAlarms.clear(); - } else if (Intent.ACTION_TIME_CHANGED.equals(action)) { - if (DEBUG) Slog.d(TAG, "time changed to " + now); - mFiredAlarms.clear(); - } else { - if (DEBUG) Slog.d(TAG, action + " fired at " + now); - } - evaluateSubscriptions(); - updateAlarms(); - } - }; - - private final NextAlarmTracker.Callback mTrackerCallback = new NextAlarmTracker.Callback() { - @Override - public void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { - DowntimeConditionProvider.this.onEvaluateNextAlarm(nextAlarm, wakeupTime, booted); - } - }; - - private final ZenModeHelper.Callback mZenCallback = new ZenModeHelper.Callback() { - @Override - void onZenModeChanged() { - if (mConditionClearing && isZenOff()) { - evaluateAutotrigger(); - } - mConditionClearing = false; - evaluateSubscriptions(); - } - }; - - private class FiredAlarms { - private final ArraySet<Long> mFiredAlarms = new ArraySet<Long>(); - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder(); - for (int i = 0; i < mFiredAlarms.size(); i++) { - if (i > 0) sb.append(','); - sb.append(mTracker.formatAlarmDebug(mFiredAlarms.valueAt(i))); - } - return sb.toString(); - } - - public void add(long firedAlarm) { - mFiredAlarms.add(firedAlarm); - } - - public void clear() { - mFiredAlarms.clear(); - } - - public boolean findBefore(long time) { - for (int i = 0; i < mFiredAlarms.size(); i++) { - if (mFiredAlarms.valueAt(i) < time) { - return true; - } - } - return false; - } - } -} diff --git a/services/core/java/com/android/server/notification/NextAlarmConditionProvider.java b/services/core/java/com/android/server/notification/NextAlarmConditionProvider.java deleted file mode 100644 index 1634c65..0000000 --- a/services/core/java/com/android/server/notification/NextAlarmConditionProvider.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright (C) 2014 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.server.notification; - -import android.app.AlarmManager; -import android.app.AlarmManager.AlarmClockInfo; -import android.content.ComponentName; -import android.content.Context; -import android.net.Uri; -import android.service.notification.Condition; -import android.service.notification.ConditionProviderService; -import android.service.notification.IConditionProvider; -import android.service.notification.ZenModeConfig; -import android.text.TextUtils; -import android.util.ArraySet; -import android.util.Log; -import android.util.Slog; -import android.util.TimeUtils; - -import com.android.internal.R; -import com.android.server.notification.NotificationManagerService.DumpFilter; - -import java.io.PrintWriter; - -/** - * Built-in zen condition provider for alarm-clock-based conditions. - * - * <p>If the user's next alarm is within a lookahead threshold (config, default 12hrs), advertise - * it as an exit condition for zen mode. - * - * <p>The next alarm is defined as {@link AlarmManager#getNextAlarmClock(int)}, which does not - * survive a reboot. Maintain the illusion of a consistent next alarm value by holding on to - * a persisted condition until we receive the first value after reboot, or timeout with no value. - */ -public class NextAlarmConditionProvider extends ConditionProviderService { - private static final String TAG = "NextAlarmConditions"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - private static final long SECONDS = 1000; - private static final long MINUTES = 60 * SECONDS; - private static final long HOURS = 60 * MINUTES; - - private static final long BAD_CONDITION = -1; - - public static final ComponentName COMPONENT = - new ComponentName("android", NextAlarmConditionProvider.class.getName()); - - private final Context mContext = this; - private final NextAlarmTracker mTracker; - private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>(); - - private boolean mConnected; - private long mLookaheadThreshold; - private boolean mRequesting; - - public NextAlarmConditionProvider(NextAlarmTracker tracker) { - if (DEBUG) Slog.d(TAG, "new NextAlarmConditionProvider()"); - mTracker = tracker; - } - - public void dump(PrintWriter pw, DumpFilter filter) { - pw.println(" NextAlarmConditionProvider:"); - pw.print(" mConnected="); pw.println(mConnected); - pw.print(" mLookaheadThreshold="); pw.print(mLookaheadThreshold); - pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")"); - pw.print(" mSubscriptions="); pw.println(mSubscriptions); - pw.print(" mRequesting="); pw.println(mRequesting); - } - - @Override - public void onConnected() { - if (DEBUG) Slog.d(TAG, "onConnected"); - mLookaheadThreshold = PropConfig.getInt(mContext, "nextalarm.condition.lookahead", - R.integer.config_next_alarm_condition_lookahead_threshold_hrs) * HOURS; - mConnected = true; - mTracker.addCallback(mTrackerCallback); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (DEBUG) Slog.d(TAG, "onDestroy"); - mTracker.removeCallback(mTrackerCallback); - mConnected = false; - } - - @Override - public void onRequestConditions(int relevance) { - if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance); - if (!mConnected) return; - mRequesting = (relevance & Condition.FLAG_RELEVANT_NOW) != 0; - mTracker.evaluate(); - } - - @Override - public void onSubscribe(Uri conditionId) { - if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId); - if (tryParseNextAlarmCondition(conditionId) == BAD_CONDITION) { - notifyCondition(conditionId, null, Condition.STATE_FALSE, "badCondition"); - return; - } - mSubscriptions.add(conditionId); - mTracker.evaluate(); - } - - @Override - public void onUnsubscribe(Uri conditionId) { - if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId); - mSubscriptions.remove(conditionId); - } - - public void attachBase(Context base) { - attachBaseContext(base); - } - - public IConditionProvider asInterface() { - return (IConditionProvider) onBind(null); - } - - private boolean isWithinLookaheadThreshold(AlarmClockInfo alarm) { - if (alarm == null) return false; - final long delta = NextAlarmTracker.getEarlyTriggerTime(alarm) - System.currentTimeMillis(); - return delta > 0 && (mLookaheadThreshold <= 0 || delta < mLookaheadThreshold); - } - - private void notifyCondition(Uri id, AlarmClockInfo alarm, int state, String reason) { - final String formattedAlarm = alarm == null ? "" : mTracker.formatAlarm(alarm); - if (DEBUG) Slog.d(TAG, "notifyCondition " + Condition.stateToString(state) - + " alarm=" + formattedAlarm + " reason=" + reason); - notifyCondition(new Condition(id, - mContext.getString(R.string.zen_mode_next_alarm_summary, formattedAlarm), - mContext.getString(R.string.zen_mode_next_alarm_line_one), - formattedAlarm, 0, state, Condition.FLAG_RELEVANT_NOW)); - } - - private Uri newConditionId(AlarmClockInfo nextAlarm) { - return new Uri.Builder().scheme(Condition.SCHEME) - .authority(ZenModeConfig.SYSTEM_AUTHORITY) - .appendPath(ZenModeConfig.NEXT_ALARM_PATH) - .appendPath(Integer.toString(mTracker.getCurrentUserId())) - .appendPath(Long.toString(nextAlarm.getTriggerTime())) - .build(); - } - - private long tryParseNextAlarmCondition(Uri conditionId) { - return conditionId != null && conditionId.getScheme().equals(Condition.SCHEME) - && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY) - && conditionId.getPathSegments().size() == 3 - && conditionId.getPathSegments().get(0).equals(ZenModeConfig.NEXT_ALARM_PATH) - && conditionId.getPathSegments().get(1) - .equals(Integer.toString(mTracker.getCurrentUserId())) - ? tryParseLong(conditionId.getPathSegments().get(2), BAD_CONDITION) - : BAD_CONDITION; - } - - private static long tryParseLong(String value, long defValue) { - if (TextUtils.isEmpty(value)) return defValue; - try { - return Long.valueOf(value); - } catch (NumberFormatException e) { - return defValue; - } - } - - private void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { - final boolean withinThreshold = isWithinLookaheadThreshold(nextAlarm); - final long nextAlarmTime = nextAlarm != null ? nextAlarm.getTriggerTime() : 0; - if (DEBUG) Slog.d(TAG, "onEvaluate mSubscriptions=" + mSubscriptions - + " nextAlarmTime=" + mTracker.formatAlarmDebug(nextAlarmTime) - + " nextAlarmWakeup=" + mTracker.formatAlarmDebug(wakeupTime) - + " withinThreshold=" + withinThreshold - + " booted=" + booted); - - ArraySet<Uri> conditions = mSubscriptions; - if (mRequesting && nextAlarm != null && withinThreshold) { - final Uri id = newConditionId(nextAlarm); - if (!conditions.contains(id)) { - conditions = new ArraySet<Uri>(conditions); - conditions.add(id); - } - } - for (Uri conditionId : conditions) { - final long time = tryParseNextAlarmCondition(conditionId); - if (time == BAD_CONDITION) { - notifyCondition(conditionId, nextAlarm, Condition.STATE_FALSE, "badCondition"); - } else if (!booted) { - // we don't know yet - if (mSubscriptions.contains(conditionId)) { - notifyCondition(conditionId, nextAlarm, Condition.STATE_UNKNOWN, "!booted"); - } - } else if (time != nextAlarmTime) { - // next alarm changed since subscription, consider obsolete - notifyCondition(conditionId, nextAlarm, Condition.STATE_FALSE, "changed"); - } else if (!withinThreshold) { - // next alarm outside threshold or in the past, condition = false - notifyCondition(conditionId, nextAlarm, Condition.STATE_FALSE, "!within"); - } else { - // next alarm within threshold and in the future, condition = true - notifyCondition(conditionId, nextAlarm, Condition.STATE_TRUE, "within"); - } - } - } - - private final NextAlarmTracker.Callback mTrackerCallback = new NextAlarmTracker.Callback() { - @Override - public void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { - NextAlarmConditionProvider.this.onEvaluate(nextAlarm, wakeupTime, booted); - } - }; -} diff --git a/services/core/java/com/android/server/notification/NextAlarmTracker.java b/services/core/java/com/android/server/notification/NextAlarmTracker.java deleted file mode 100644 index 234f545..0000000 --- a/services/core/java/com/android/server/notification/NextAlarmTracker.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright (C) 2014 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.server.notification; - -import android.app.ActivityManager; -import android.app.AlarmManager; -import android.app.AlarmManager.AlarmClockInfo; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Handler; -import android.os.Message; -import android.os.PowerManager; -import android.os.UserHandle; -import android.text.format.DateFormat; -import android.util.Log; -import android.util.Slog; -import android.util.TimeUtils; - -import com.android.server.notification.NotificationManagerService.DumpFilter; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Locale; - -/** Helper for tracking updates to the current user's next alarm. */ -public class NextAlarmTracker { - private static final String TAG = "NextAlarmTracker"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - private static final String ACTION_TRIGGER = TAG + ".trigger"; - private static final String EXTRA_TRIGGER = "trigger"; - private static final int REQUEST_CODE = 100; - - private static final long SECONDS = 1000; - private static final long MINUTES = 60 * SECONDS; - private static final long NEXT_ALARM_UPDATE_DELAY = 1 * SECONDS; // treat clear+set as update - private static final long EARLY = 5 * SECONDS; // fire early, ensure alarm stream is unmuted - private static final long WAIT_AFTER_INIT = 5 * MINUTES;// for initial alarm re-registration - private static final long WAIT_AFTER_BOOT = 20 * SECONDS; // for initial alarm re-registration - - private final Context mContext; - private final H mHandler = new H(); - private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); - - private long mInit; - private boolean mRegistered; - private AlarmManager mAlarmManager; - private int mCurrentUserId; - private long mScheduledAlarmTime; - private long mBootCompleted; - private PowerManager.WakeLock mWakeLock; - - public NextAlarmTracker(Context context) { - mContext = context; - } - - public void dump(PrintWriter pw, DumpFilter filter) { - pw.println(" NextAlarmTracker:"); - pw.print(" len(mCallbacks)="); pw.println(mCallbacks.size()); - pw.print(" mRegistered="); pw.println(mRegistered); - pw.print(" mInit="); pw.println(mInit); - pw.print(" mBootCompleted="); pw.println(mBootCompleted); - pw.print(" mCurrentUserId="); pw.println(mCurrentUserId); - pw.print(" mScheduledAlarmTime="); pw.println(formatAlarmDebug(mScheduledAlarmTime)); - pw.print(" mWakeLock="); pw.println(mWakeLock); - } - - public void addCallback(Callback callback) { - mCallbacks.add(callback); - } - - public void removeCallback(Callback callback) { - mCallbacks.remove(callback); - } - - public int getCurrentUserId() { - return mCurrentUserId; - } - - public AlarmClockInfo getNextAlarm() { - return mAlarmManager.getNextAlarmClock(mCurrentUserId); - } - - public void onUserSwitched() { - reset(); - } - - public void init() { - mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - final PowerManager p = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - mWakeLock = p.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); - mInit = System.currentTimeMillis(); - reset(); - } - - public void reset() { - if (mRegistered) { - mContext.unregisterReceiver(mReceiver); - } - mCurrentUserId = ActivityManager.getCurrentUser(); - final IntentFilter filter = new IntentFilter(); - filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); - filter.addAction(ACTION_TRIGGER); - filter.addAction(Intent.ACTION_TIME_CHANGED); - filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); - filter.addAction(Intent.ACTION_BOOT_COMPLETED); - mContext.registerReceiverAsUser(mReceiver, new UserHandle(mCurrentUserId), filter, null, - null); - mRegistered = true; - evaluate(); - } - - public void destroy() { - if (mRegistered) { - mContext.unregisterReceiver(mReceiver); - mRegistered = false; - } - } - - public void evaluate() { - mHandler.postEvaluate(0); - } - - private void fireEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { - for (Callback callback : mCallbacks) { - callback.onEvaluate(nextAlarm, wakeupTime, booted); - } - } - - private void handleEvaluate() { - final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId); - final long triggerTime = getEarlyTriggerTime(nextAlarm); - final long now = System.currentTimeMillis(); - final boolean alarmUpcoming = triggerTime > now; - final boolean booted = isDoneWaitingAfterBoot(now); - if (DEBUG) Slog.d(TAG, "handleEvaluate nextAlarm=" + formatAlarmDebug(triggerTime) - + " alarmUpcoming=" + alarmUpcoming - + " booted=" + booted); - fireEvaluate(nextAlarm, triggerTime, booted); - if (!booted) { - // recheck after boot - final long recheckTime = (mBootCompleted > 0 ? mBootCompleted : now) + WAIT_AFTER_BOOT; - rescheduleAlarm(recheckTime); - return; - } - if (alarmUpcoming) { - // wake up just before the next alarm - rescheduleAlarm(triggerTime); - } - } - - public static long getEarlyTriggerTime(AlarmClockInfo alarm) { - return alarm != null ? (alarm.getTriggerTime() - EARLY) : 0; - } - - private boolean isDoneWaitingAfterBoot(long time) { - if (mBootCompleted > 0) return (time - mBootCompleted) > WAIT_AFTER_BOOT; - if (mInit > 0) return (time - mInit) > WAIT_AFTER_INIT; - return true; - } - - public static String formatDuration(long millis) { - final StringBuilder sb = new StringBuilder(); - TimeUtils.formatDuration(millis, sb); - return sb.toString(); - } - - public String formatAlarm(AlarmClockInfo alarm) { - return alarm != null ? formatAlarm(alarm.getTriggerTime()) : null; - } - - private String formatAlarm(long time) { - return formatAlarm(time, "Hm", "hma"); - } - - private String formatAlarm(long time, String skeleton24, String skeleton12) { - final String skeleton = DateFormat.is24HourFormat(mContext) ? skeleton24 : skeleton12; - final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); - return DateFormat.format(pattern, time).toString(); - } - - public String formatAlarmDebug(AlarmClockInfo alarm) { - return formatAlarmDebug(alarm != null ? alarm.getTriggerTime() : 0); - } - - public String formatAlarmDebug(long time) { - if (time <= 0) return Long.toString(time); - return String.format("%s (%s)", time, formatAlarm(time, "Hms", "hmsa")); - } - - private void rescheduleAlarm(long time) { - if (DEBUG) Slog.d(TAG, "rescheduleAlarm " + time); - final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE, - new Intent(ACTION_TRIGGER) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) - .putExtra(EXTRA_TRIGGER, time), - PendingIntent.FLAG_UPDATE_CURRENT); - alarms.cancel(pendingIntent); - mScheduledAlarmTime = time; - if (time > 0) { - if (DEBUG) Slog.d(TAG, String.format("Scheduling alarm for %s (in %s)", - formatAlarmDebug(time), formatDuration(time - System.currentTimeMillis()))); - alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); - } - } - - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (DEBUG) Slog.d(TAG, "onReceive " + action); - long delay = 0; - if (action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) { - delay = NEXT_ALARM_UPDATE_DELAY; - if (DEBUG) Slog.d(TAG, String.format(" next alarm for user %s: %s", - mCurrentUserId, - formatAlarmDebug(mAlarmManager.getNextAlarmClock(mCurrentUserId)))); - } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { - mBootCompleted = System.currentTimeMillis(); - } - mHandler.postEvaluate(delay); - mWakeLock.acquire(delay + 5000); // stay awake during evaluate - } - }; - - private class H extends Handler { - private static final int MSG_EVALUATE = 1; - - public void postEvaluate(long delay) { - removeMessages(MSG_EVALUATE); - sendEmptyMessageDelayed(MSG_EVALUATE, delay); - } - - @Override - public void handleMessage(Message msg) { - if (msg.what == MSG_EVALUATE) { - handleEvaluate(); - } - } - } - - public interface Callback { - void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted); - } -} diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index c330046..4cf2909 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -879,7 +879,8 @@ public class NotificationManagerService extends SystemService { mRankingHelper = new RankingHelper(getContext(), new RankingWorkerHandler(mRankingThread.getLooper()), extractorNames); - mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper()); + mConditionProviders = new ConditionProviders(getContext(), mHandler, mUserProfiles); + mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), mConditionProviders); mZenModeHelper.addCallback(new ZenModeHelper.Callback() { @Override public void onConfigChanged() { @@ -900,8 +901,6 @@ public class NotificationManagerService extends SystemService { importOldBlockDb(); mListeners = new NotificationListeners(); - mConditionProviders = new ConditionProviders(getContext(), - mHandler, mUserProfiles, mZenModeHelper); mStatusBar = getLocalService(StatusBarManagerInternal.class); mStatusBar.setNotificationDelegate(mNotificationDelegate); @@ -936,7 +935,7 @@ public class NotificationManagerService extends SystemService { Settings.Global.DEVICE_PROVISIONED, 0)) { mDisableNotificationEffects = true; } - mZenModeHelper.readZenModeFromSetting(); + mZenModeHelper.initZenMode(); mInterruptionFilter = mZenModeHelper.getZenModeListenerInterruptionFilter(); mUserProfiles.updateCache(getContext()); @@ -1490,23 +1489,28 @@ public class NotificationManagerService extends SystemService { } @Override + public int getZenMode() { + return mZenModeHelper.getZenMode(); + } + + @Override public ZenModeConfig getZenModeConfig() { enforceSystemOrSystemUIOrVolume("INotificationManager.getZenModeConfig"); return mZenModeHelper.getConfig(); } @Override - public boolean setZenModeConfig(ZenModeConfig config) { + public boolean setZenModeConfig(ZenModeConfig config, String reason) { checkCallerIsSystem(); - return mZenModeHelper.setConfig(config); + return mZenModeHelper.setConfig(config, reason); } @Override - public void setZenMode(int mode) throws RemoteException { + public void setZenMode(int mode, Uri conditionId, String reason) throws RemoteException { enforceSystemOrSystemUIOrVolume("INotificationManager.setZenMode"); final long identity = Binder.clearCallingIdentity(); try { - mZenModeHelper.setZenMode(mode, "NotificationManager"); + mZenModeHelper.setManualZenMode(mode, conditionId, reason); } finally { Binder.restoreCallingIdentity(identity); } @@ -1528,30 +1532,7 @@ public class NotificationManagerService extends SystemService { @Override public void requestZenModeConditions(IConditionListener callback, int relevance) { enforceSystemOrSystemUIOrVolume("INotificationManager.requestZenModeConditions"); - mConditionProviders.requestZenModeConditions(callback, relevance); - } - - @Override - public void setZenModeCondition(Condition condition) { - enforceSystemOrSystemUIOrVolume("INotificationManager.setZenModeCondition"); - final long identity = Binder.clearCallingIdentity(); - try { - mConditionProviders.setZenModeCondition(condition, "binderCall"); - } finally { - Binder.restoreCallingIdentity(identity); - } - } - - @Override - public void setAutomaticZenModeConditions(Uri[] conditionIds) { - enforceSystemOrSystemUI("INotificationManager.setAutomaticZenModeConditions"); - mConditionProviders.setAutomaticZenModeConditions(conditionIds); - } - - @Override - public Condition[] getAutomaticZenModeConditions() { - enforceSystemOrSystemUI("INotificationManager.getAutomaticZenModeConditions"); - return mConditionProviders.getAutomaticZenModeConditions(); + mZenModeHelper.requestZenModeConditions(callback, relevance); } private void enforceSystemOrSystemUIOrVolume(String message) { @@ -1603,7 +1584,7 @@ public class NotificationManagerService extends SystemService { @Override public boolean isSystemConditionProviderEnabled(String path) { enforceSystemOrSystemUIOrVolume("INotificationManager.isSystemConditionProviderEnabled"); - return mConditionProviders.isSystemConditionProviderEnabled(path); + return mConditionProviders.isSystemProviderEnabled(path); } }; diff --git a/services/core/java/com/android/server/notification/DowntimeCalendar.java b/services/core/java/com/android/server/notification/ScheduleCalendar.java index d14fd40..cea611d 100644 --- a/services/core/java/com/android/server/notification/DowntimeCalendar.java +++ b/services/core/java/com/android/server/notification/ScheduleCalendar.java @@ -16,38 +16,36 @@ package com.android.server.notification; +import android.service.notification.ZenModeConfig.ScheduleInfo; +import android.util.ArraySet; + import java.util.Calendar; import java.util.Objects; import java.util.TimeZone; -import android.service.notification.ZenModeConfig; -import android.service.notification.ZenModeConfig.DowntimeInfo; -import android.util.ArraySet; - -public class DowntimeCalendar { - +public class ScheduleCalendar { private final ArraySet<Integer> mDays = new ArraySet<Integer>(); private final Calendar mCalendar = Calendar.getInstance(); - private DowntimeInfo mInfo; + private ScheduleInfo mSchedule; @Override public String toString() { - return "DowntimeCalendar[mDays=" + mDays + "]"; + return "ScheduleCalendar[mDays=" + mDays + "]"; } - public void setDowntimeInfo(DowntimeInfo info) { - if (Objects.equals(mInfo, info)) return; - mInfo = info; + public void setSchedule(ScheduleInfo schedule) { + if (Objects.equals(mSchedule, schedule)) return; + mSchedule = schedule; updateDays(); } - public long nextDowntimeStart(long time) { - if (mInfo == null || mDays.size() == 0) return Long.MAX_VALUE; - final long start = getTime(time, mInfo.startHour, mInfo.startMinute); + public long nextScheduleStart(long time) { + if (mSchedule == null || mDays.size() == 0) return Long.MAX_VALUE; + final long start = getTime(time, mSchedule.startHour, mSchedule.startMinute); for (int i = 0; i < Calendar.SATURDAY; i++) { final long t = addDays(start, i); - if (t > time && isInDowntime(t)) { + if (t > time && isInSchedule(t)) { return t; } } @@ -58,7 +56,14 @@ public class DowntimeCalendar { mCalendar.setTimeZone(tz); } - public long getNextTime(long now, int hr, int min) { + public long getNextChangeTime(long now) { + if (mSchedule == null) return 0; + final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute); + final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute); + return Math.min(nextStart, nextEnd); + } + + private long getNextTime(long now, int hr, int min) { final long time = getTime(now, hr, min); return time <= now ? addDays(time, 1) : time; } @@ -72,17 +77,17 @@ public class DowntimeCalendar { return mCalendar.getTimeInMillis(); } - public boolean isInDowntime(long time) { - if (mInfo == null || mDays.size() == 0) return false; - final long start = getTime(time, mInfo.startHour, mInfo.startMinute); - long end = getTime(time, mInfo.endHour, mInfo.endMinute); + public boolean isInSchedule(long time) { + if (mSchedule == null || mDays.size() == 0) return false; + final long start = getTime(time, mSchedule.startHour, mSchedule.startMinute); + long end = getTime(time, mSchedule.endHour, mSchedule.endMinute); if (end <= start) { end = addDays(end, 1); } - return isInDowntime(-1, time, start, end) || isInDowntime(0, time, start, end); + return isInSchedule(-1, time, start, end) || isInSchedule(0, time, start, end); } - private boolean isInDowntime(int daysOffset, long time, long start, long end) { + private boolean isInSchedule(int daysOffset, long time, long start, long end) { final int n = Calendar.SATURDAY; final int day = ((getDayOfWeek(time) - 1) + (daysOffset % n) + n) % n + 1; start = addDays(start, daysOffset); @@ -97,10 +102,9 @@ public class DowntimeCalendar { private void updateDays() { mDays.clear(); - if (mInfo != null) { - final int[] days = ZenModeConfig.tryParseDays(mInfo.mode); - for (int i = 0; days != null && i < days.length; i++) { - mDays.add(days[i]); + if (mSchedule != null && mSchedule.days != null) { + for (int i = 0; i < mSchedule.days.length; i++) { + mDays.add(mSchedule.days[i]); } } } @@ -110,4 +114,4 @@ public class DowntimeCalendar { mCalendar.add(Calendar.DATE, days); return mCalendar.getTimeInMillis(); } -} +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java new file mode 100644 index 0000000..c997e45 --- /dev/null +++ b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java @@ -0,0 +1,238 @@ +/* + * 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.server.notification; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.service.notification.Condition; +import android.service.notification.IConditionProvider; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ScheduleInfo; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.util.TimeUtils; + +import com.android.server.notification.NotificationManagerService.DumpFilter; + +import java.io.PrintWriter; +import java.util.Date; +import java.util.TimeZone; + +/** + * Built-in zen condition provider for daily scheduled time-based conditions. + */ +public class ScheduleConditionProvider extends SystemConditionProviderService { + private static final String TAG = "ScheduleConditions"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + public static final ComponentName COMPONENT = + new ComponentName("android", ScheduleConditionProvider.class.getName()); + private static final String NOT_SHOWN = "..."; + private static final String ACTION_EVALUATE = TAG + ".EVALUATE"; + private static final int REQUEST_CODE_EVALUATE = 1; + private static final String EXTRA_TIME = "time"; + + private final Context mContext = this; + private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>(); + + private boolean mConnected; + private boolean mRegistered; + + public ScheduleConditionProvider() { + if (DEBUG) Slog.d(TAG, "new ScheduleConditionProvider()"); + } + + @Override + public ComponentName getComponent() { + return COMPONENT; + } + + @Override + public boolean isValidConditionid(Uri id) { + return ZenModeConfig.isValidScheduleConditionId(id); + } + + @Override + public void dump(PrintWriter pw, DumpFilter filter) { + pw.println(" ScheduleConditionProvider:"); + pw.print(" mConnected="); pw.println(mConnected); + pw.print(" mRegistered="); pw.println(mRegistered); + pw.println(" mSubscriptions="); + final long now = System.currentTimeMillis(); + for (Uri conditionId : mSubscriptions) { + pw.print(" "); + pw.print(meetsSchedule(conditionId, now) ? "* " : " "); + pw.println(conditionId); + } + } + + @Override + public void onConnected() { + if (DEBUG) Slog.d(TAG, "onConnected"); + mConnected = true; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (DEBUG) Slog.d(TAG, "onDestroy"); + mConnected = false; + } + + @Override + public void onRequestConditions(int relevance) { + if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance); + // does not advertise conditions + } + + @Override + public void onSubscribe(Uri conditionId) { + if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId); + if (!ZenModeConfig.isValidScheduleConditionId(conditionId)) { + notifyCondition(conditionId, Condition.STATE_FALSE, "badCondition"); + return; + } + mSubscriptions.add(conditionId); + evaluateSubscriptions(); + } + + @Override + public void onUnsubscribe(Uri conditionId) { + if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId); + mSubscriptions.remove(conditionId); + evaluateSubscriptions(); + } + + @Override + public void attachBase(Context base) { + attachBaseContext(base); + } + + @Override + public IConditionProvider asInterface() { + return (IConditionProvider) onBind(null); + } + + private void evaluateSubscriptions() { + setRegistered(!mSubscriptions.isEmpty()); + final long now = System.currentTimeMillis(); + long nextAlarmTime = 0; + for (Uri conditionId : mSubscriptions) { + final ScheduleCalendar cal = toScheduleCalendar(conditionId); + if (cal != null && cal.isInSchedule(now)) { + notifyCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule"); + } else { + notifyCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule"); + } + if (cal != null) { + final long nextChangeTime = cal.getNextChangeTime(now); + if (nextChangeTime > 0 && nextChangeTime > now) { + if (nextAlarmTime == 0 || nextChangeTime < nextAlarmTime) { + nextAlarmTime = nextChangeTime; + } + } + } + } + updateAlarm(now, nextAlarmTime); + } + + private void updateAlarm(long now, long time) { + final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, + REQUEST_CODE_EVALUATE, + new Intent(ACTION_EVALUATE) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + .putExtra(EXTRA_TIME, time), + PendingIntent.FLAG_UPDATE_CURRENT); + alarms.cancel(pendingIntent); + if (time > now) { + if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s", + ts(time), formatDuration(time - now), ts(now))); + alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); + } else { + if (DEBUG) Slog.d(TAG, "Not scheduling evaluate"); + } + } + + private static String ts(long time) { + return new Date(time) + " (" + time + ")"; + } + + private static String formatDuration(long millis) { + final StringBuilder sb = new StringBuilder(); + TimeUtils.formatDuration(millis, sb); + return sb.toString(); + } + + private static boolean meetsSchedule(Uri conditionId, long time) { + final ScheduleCalendar cal = toScheduleCalendar(conditionId); + return cal != null && cal.isInSchedule(time); + } + + private static ScheduleCalendar toScheduleCalendar(Uri conditionId) { + final ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId(conditionId); + if (schedule == null || schedule.days == null || schedule.days.length == 0) return null; + final ScheduleCalendar sc = new ScheduleCalendar(); + sc.setSchedule(schedule); + sc.setTimeZone(TimeZone.getDefault()); + return sc; + } + + private void setRegistered(boolean registered) { + if (mRegistered == registered) return; + if (DEBUG) Slog.d(TAG, "setRegistered " + registered); + mRegistered = registered; + if (mRegistered) { + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + filter.addAction(ACTION_EVALUATE); + registerReceiver(mReceiver, filter); + } else { + unregisterReceiver(mReceiver); + } + } + + private void notifyCondition(Uri conditionId, int state, String reason) { + if (DEBUG) Slog.d(TAG, "notifyCondition " + Condition.stateToString(state) + + " reason=" + reason); + notifyCondition(createCondition(conditionId, state)); + } + + private Condition createCondition(Uri id, int state) { + final String summary = NOT_SHOWN; + final String line1 = NOT_SHOWN; + final String line2 = NOT_SHOWN; + return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS); + } + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction()); + evaluateSubscriptions(); + } + }; + +} diff --git a/services/core/java/com/android/server/notification/SystemConditionProviderService.java b/services/core/java/com/android/server/notification/SystemConditionProviderService.java new file mode 100644 index 0000000..a217623 --- /dev/null +++ b/services/core/java/com/android/server/notification/SystemConditionProviderService.java @@ -0,0 +1,36 @@ +/* + * 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.server.notification; + +import android.content.ComponentName; +import android.content.Context; +import android.net.Uri; +import android.service.notification.ConditionProviderService; +import android.service.notification.IConditionProvider; + +import com.android.server.notification.NotificationManagerService.DumpFilter; + +import java.io.PrintWriter; + +public abstract class SystemConditionProviderService extends ConditionProviderService { + + abstract public void dump(PrintWriter pw, DumpFilter filter); + abstract public void attachBase(Context context); + abstract public IConditionProvider asInterface(); + abstract public ComponentName getComponent(); + abstract public boolean isValidConditionid(Uri id); +} diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java new file mode 100644 index 0000000..67a2a54 --- /dev/null +++ b/services/core/java/com/android/server/notification/ZenModeConditions.java @@ -0,0 +1,144 @@ +/** + * 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.server.notification; + +import android.content.ComponentName; +import android.net.Uri; +import android.service.notification.Condition; +import android.service.notification.IConditionListener; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ZenRule; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; + +import java.io.PrintWriter; +import java.util.Objects; + +public class ZenModeConditions implements ConditionProviders.Callback { + private static final String TAG = ZenModeHelper.TAG; + private static final boolean DEBUG = ZenModeHelper.DEBUG; + + private final ZenModeHelper mHelper; + private final ConditionProviders mConditionProviders; + private final ArrayMap<Uri, ComponentName> mSubscriptions = new ArrayMap<>(); + + private CountdownConditionProvider mCountdown; + private ScheduleConditionProvider mSchedule; + + public ZenModeConditions(ZenModeHelper helper, ConditionProviders conditionProviders) { + mHelper = helper; + mConditionProviders = conditionProviders; + if (mConditionProviders.isSystemProviderEnabled(ZenModeConfig.COUNTDOWN_PATH)) { + mCountdown = new CountdownConditionProvider(); + mConditionProviders.addSystemProvider(mCountdown); + } + if (mConditionProviders.isSystemProviderEnabled(ZenModeConfig.SCHEDULE_PATH)) { + mSchedule = new ScheduleConditionProvider(); + mConditionProviders.addSystemProvider(mSchedule); + } + mConditionProviders.setCallback(this); + } + + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.print("mSubscriptions="); pw.println(mSubscriptions); + } + + public void requestConditions(IConditionListener callback, int relevance) { + mConditionProviders.requestConditions(callback, relevance); + } + + public void evaluateConfig(ZenModeConfig config) { + if (config == null) return; + if (config.manualRule != null && !config.manualRule.isTrueOrUnknown()) { + if (DEBUG) Log.d(TAG, "evaluateConfig: clearing manual rule"); + config.manualRule = null; + } + final ArraySet<Uri> current = new ArraySet<>(); + evaluateRule(config.manualRule, current); + for (ZenRule automaticRule : config.automaticRules.values()) { + evaluateRule(automaticRule, current); + } + final int N = mSubscriptions.size(); + for (int i = N - 1; i >= 0; i--) { + final Uri id = mSubscriptions.keyAt(i); + final ComponentName component = mSubscriptions.valueAt(i); + if (!current.contains(id)) { + mConditionProviders.unsubscribeIfNecessary(component, id); + mSubscriptions.removeAt(i); + } + } + } + + private void evaluateRule(ZenRule rule, ArraySet<Uri> current) { + if (rule == null || rule.conditionId == null) return; + final Uri id = rule.conditionId; + for (SystemConditionProviderService sp : mConditionProviders.getSystemProviders()) { + if (sp.isValidConditionid(id)) { + mConditionProviders.ensureRecordExists(sp.getComponent(), id, sp.asInterface()); + rule.component = sp.getComponent(); + } + } + current.add(id); + if (mConditionProviders.subscribeIfNecessary(rule.component, rule.conditionId)) { + mSubscriptions.put(rule.conditionId, rule.component); + } + } + + @Override + public void onBootComplete() { + // noop + } + + @Override + public void onUserSwitched() { + // noop + } + + @Override + public void onConditionChanged(Uri id, Condition condition) { + if (DEBUG) Log.d(TAG, "onConditionChanged " + id + " " + condition); + ZenModeConfig config = mHelper.getConfig(); + if (config == null) return; + config = config.copy(); + boolean updated = updateCondition(id, condition, config.manualRule); + for (ZenRule automaticRule : config.automaticRules.values()) { + updated |= updateCondition(id, condition, automaticRule); + updated |= updateSnoozing(automaticRule); + } + if (updated) { + mHelper.setConfig(config, "conditionChanged"); + } + } + + private boolean updateSnoozing(ZenRule rule) { + if (rule != null && rule.snoozing && !rule.isTrueOrUnknown()) { + rule.snoozing = false; + if (DEBUG) Log.d(TAG, "Snoozing reset for " + rule.conditionId); + return true; + } + return false; + } + + private boolean updateCondition(Uri id, Condition condition, ZenRule rule) { + if (id == null || rule == null || rule.conditionId == null) return false; + if (!rule.conditionId.equals(id)) return false; + if (Objects.equals(condition, rule.condition)) return false; + rule.condition = condition; + return true; + } +} diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java new file mode 100644 index 0000000..32fd01a --- /dev/null +++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java @@ -0,0 +1,202 @@ +/** + * 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.server.notification; + +import android.app.Notification; +import android.content.ComponentName; +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.UserHandle; +import android.provider.Settings.Global; +import android.provider.Settings.Secure; +import android.service.notification.ZenModeConfig; +import android.telecom.TelecomManager; +import android.util.Slog; + +import java.util.Objects; + +public class ZenModeFiltering { + private static final String TAG = ZenModeHelper.TAG; + private static final boolean DEBUG = ZenModeHelper.DEBUG; + + private final Context mContext; + + private ComponentName mDefaultPhoneApp; + + public ZenModeFiltering(Context context) { + mContext = context; + } + + public ComponentName getDefaultPhoneApp() { + return mDefaultPhoneApp; + } + + /** + * @param extras extras of the notification with EXTRA_PEOPLE populated + * @param contactsTimeoutMs timeout in milliseconds to wait for contacts response + * @param timeoutAffinity affinity to return when the timeout specified via + * <code>contactsTimeoutMs</code> is hit + */ + public static boolean matchesCallFilter(int zen, ZenModeConfig config, UserHandle userHandle, + Bundle extras, ValidateNotificationPeople validator, int contactsTimeoutMs, + float timeoutAffinity) { + if (zen == Global.ZEN_MODE_NO_INTERRUPTIONS) return false; // nothing gets through + if (zen == Global.ZEN_MODE_ALARMS) return false; // not an alarm + if (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) { + if (!config.allowCalls) return false; // no calls get through + if (validator != null) { + final float contactAffinity = validator.getContactAffinity(userHandle, extras, + contactsTimeoutMs, timeoutAffinity); + return audienceMatches(config, contactAffinity); + } + } + return true; + } + + public boolean shouldIntercept(int zen, ZenModeConfig config, NotificationRecord record) { + if (isSystem(record)) { + return false; + } + switch (zen) { + case Global.ZEN_MODE_NO_INTERRUPTIONS: + // #notevenalarms + ZenLog.traceIntercepted(record, "none"); + return true; + case Global.ZEN_MODE_ALARMS: + if (isAlarm(record)) { + // Alarms only + return false; + } + ZenLog.traceIntercepted(record, "alarmsOnly"); + return true; + case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + if (isAlarm(record)) { + // Alarms are always priority + return false; + } + // allow user-prioritized packages through in priority mode + if (record.getPackagePriority() == Notification.PRIORITY_MAX) { + ZenLog.traceNotIntercepted(record, "priorityApp"); + return false; + } + if (isCall(record)) { + if (!config.allowCalls) { + ZenLog.traceIntercepted(record, "!allowCalls"); + return true; + } + return shouldInterceptAudience(config, record); + } + if (isMessage(record)) { + if (!config.allowMessages) { + ZenLog.traceIntercepted(record, "!allowMessages"); + return true; + } + return shouldInterceptAudience(config, record); + } + if (isEvent(record)) { + if (!config.allowEvents) { + ZenLog.traceIntercepted(record, "!allowEvents"); + return true; + } + return false; + } + if (isReminder(record)) { + if (!config.allowReminders) { + ZenLog.traceIntercepted(record, "!allowReminders"); + return true; + } + return false; + } + ZenLog.traceIntercepted(record, "!priority"); + return true; + default: + return false; + } + } + + private static boolean shouldInterceptAudience(ZenModeConfig config, + NotificationRecord record) { + if (!audienceMatches(config, record.getContactAffinity())) { + ZenLog.traceIntercepted(record, "!audienceMatches"); + return true; + } + return false; + } + + private static boolean isSystem(NotificationRecord record) { + return record.isCategory(Notification.CATEGORY_SYSTEM); + } + + private static boolean isAlarm(NotificationRecord record) { + return record.isCategory(Notification.CATEGORY_ALARM) + || record.isAudioStream(AudioManager.STREAM_ALARM) + || record.isAudioAttributesUsage(AudioAttributes.USAGE_ALARM); + } + + private static boolean isEvent(NotificationRecord record) { + return record.isCategory(Notification.CATEGORY_EVENT); + } + + private static boolean isReminder(NotificationRecord record) { + return record.isCategory(Notification.CATEGORY_REMINDER); + } + + public boolean isCall(NotificationRecord record) { + return record != null && (isDefaultPhoneApp(record.sbn.getPackageName()) + || record.isCategory(Notification.CATEGORY_CALL)); + } + + private boolean isDefaultPhoneApp(String pkg) { + if (mDefaultPhoneApp == null) { + final TelecomManager telecomm = + (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE); + mDefaultPhoneApp = telecomm != null ? telecomm.getDefaultPhoneApp() : null; + if (DEBUG) Slog.d(TAG, "Default phone app: " + mDefaultPhoneApp); + } + return pkg != null && mDefaultPhoneApp != null + && pkg.equals(mDefaultPhoneApp.getPackageName()); + } + + @SuppressWarnings("deprecation") + private boolean isDefaultMessagingApp(NotificationRecord record) { + final int userId = record.getUserId(); + if (userId == UserHandle.USER_NULL || userId == UserHandle.USER_ALL) return false; + final String defaultApp = Secure.getStringForUser(mContext.getContentResolver(), + Secure.SMS_DEFAULT_APPLICATION, userId); + return Objects.equals(defaultApp, record.sbn.getPackageName()); + } + + private boolean isMessage(NotificationRecord record) { + return record.isCategory(Notification.CATEGORY_MESSAGE) || isDefaultMessagingApp(record); + } + + private static boolean audienceMatches(ZenModeConfig config, float contactAffinity) { + switch (config.allowFrom) { + case ZenModeConfig.SOURCE_ANYONE: + return true; + case ZenModeConfig.SOURCE_CONTACT: + return contactAffinity >= ValidateNotificationPeople.VALID_CONTACT; + case ZenModeConfig.SOURCE_STAR: + return contactAffinity >= ValidateNotificationPeople.STARRED_CONTACT; + default: + Slog.w(TAG, "Encountered unknown source: " + config.allowFrom); + return true; + } + } +} diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 1775df2..77f78fe 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -21,14 +21,12 @@ import static android.media.AudioAttributes.USAGE_NOTIFICATION; import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; import android.app.AppOpsManager; -import android.app.Notification; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.database.ContentObserver; -import android.media.AudioAttributes; import android.media.AudioManager; import android.media.AudioManagerInternal; import android.media.VolumePolicy; @@ -39,12 +37,13 @@ import android.os.Looper; import android.os.Message; import android.os.UserHandle; import android.provider.Settings.Global; -import android.provider.Settings.Secure; +import android.service.notification.IConditionListener; import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; -import android.telecom.TelecomManager; +import android.service.notification.ZenModeConfig.ScheduleInfo; +import android.service.notification.ZenModeConfig.ZenRule; +import android.util.ArraySet; import android.util.Log; -import android.util.Slog; import com.android.internal.R; import com.android.server.LocalServices; @@ -58,14 +57,13 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Objects; /** * NotificationManagerService helper for functionality related to zen mode. */ -public class ZenModeHelper implements AudioManagerInternal.RingerModeDelegate { - private static final String TAG = "ZenModeHelper"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); +public class ZenModeHelper { + static final String TAG = "ZenModeHelper"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final Context mContext; private final H mHandler; @@ -73,38 +71,46 @@ public class ZenModeHelper implements AudioManagerInternal.RingerModeDelegate { private final AppOpsManager mAppOps; private final ZenModeConfig mDefaultConfig; private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); + private final ZenModeFiltering mFiltering; + private final RingerModeDelegate mRingerModeDelegate = new RingerModeDelegate(); + private final ZenModeConditions mConditions; - private ComponentName mDefaultPhoneApp; private int mZenMode; private ZenModeConfig mConfig; private AudioManagerInternal mAudioManager; private int mPreviousRingerMode = -1; private boolean mEffectsSuppressed; - public ZenModeHelper(Context context, Looper looper) { + public ZenModeHelper(Context context, Looper looper, ConditionProviders conditionProviders) { mContext = context; mHandler = new H(looper); mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); mDefaultConfig = readDefaultConfig(context.getResources()); + appendDefaultScheduleRules(mDefaultConfig); mConfig = mDefaultConfig; mSettingsObserver = new SettingsObserver(mHandler); mSettingsObserver.observe(); + mFiltering = new ZenModeFiltering(mContext); + mConditions = new ZenModeConditions(this, conditionProviders); } - public static ZenModeConfig readDefaultConfig(Resources resources) { - XmlResourceParser parser = null; - try { - parser = resources.getXml(R.xml.default_zen_mode_config); - while (parser.next() != XmlPullParser.END_DOCUMENT) { - final ZenModeConfig config = ZenModeConfig.readXml(parser); - if (config != null) return config; - } - } catch (Exception e) { - Slog.w(TAG, "Error reading default zen mode config from resource", e); - } finally { - IoUtils.closeQuietly(parser); - } - return new ZenModeConfig(); + @Override + public String toString() { + return TAG; + } + + public boolean matchesCallFilter(UserHandle userHandle, Bundle extras, + ValidateNotificationPeople validator, int contactsTimeoutMs, float timeoutAffinity) { + return ZenModeFiltering.matchesCallFilter(mZenMode, mConfig, userHandle, extras, validator, + contactsTimeoutMs, timeoutAffinity); + } + + public boolean isCall(NotificationRecord record) { + return mFiltering.isCall(record); + } + + public boolean shouldIntercept(NotificationRecord record) { + return mFiltering.shouldIntercept(mZenMode, mConfig, record); } public void addCallback(Callback callback) { @@ -115,48 +121,32 @@ public class ZenModeHelper implements AudioManagerInternal.RingerModeDelegate { mCallbacks.remove(callback); } + public void initZenMode() { + if (DEBUG) Log.d(TAG, "initZenMode"); + evaluateZenMode("init", true /*setRingerMode*/); + } + public void onSystemReady() { + if (DEBUG) Log.d(TAG, "onSystemReady"); mAudioManager = LocalServices.getService(AudioManagerInternal.class); if (mAudioManager != null) { - mAudioManager.setRingerModeDelegate(this); + mAudioManager.setRingerModeDelegate(mRingerModeDelegate); } } - public int getZenModeListenerInterruptionFilter() { - switch (mZenMode) { - case Global.ZEN_MODE_OFF: - return NotificationListenerService.INTERRUPTION_FILTER_ALL; - case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: - return NotificationListenerService.INTERRUPTION_FILTER_PRIORITY; - case Global.ZEN_MODE_ALARMS: - return NotificationListenerService.INTERRUPTION_FILTER_ALARMS; - case Global.ZEN_MODE_NO_INTERRUPTIONS: - return NotificationListenerService.INTERRUPTION_FILTER_NONE; - default: - return 0; - } + public void requestZenModeConditions(IConditionListener callback, int relevance) { + mConditions.requestConditions(callback, relevance); } - private static int zenModeFromListenerInterruptionFilter(int listenerInterruptionFilter, - int defValue) { - switch (listenerInterruptionFilter) { - case NotificationListenerService.INTERRUPTION_FILTER_ALL: - return Global.ZEN_MODE_OFF; - case NotificationListenerService.INTERRUPTION_FILTER_PRIORITY: - return Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; - case NotificationListenerService.INTERRUPTION_FILTER_ALARMS: - return Global.ZEN_MODE_ALARMS; - case NotificationListenerService.INTERRUPTION_FILTER_NONE: - return Global.ZEN_MODE_NO_INTERRUPTIONS; - default: - return defValue; - } + public int getZenModeListenerInterruptionFilter() { + return getZenModeListenerInterruptionFilter(mZenMode); } public void requestFromListener(ComponentName name, int interruptionFilter) { final int newZen = zenModeFromListenerInterruptionFilter(interruptionFilter, -1); if (newZen != -1) { - setZenMode(newZen, "listener:" + (name != null ? name.flattenToShortString() : null)); + setManualZenMode(newZen, null, + "listener:" + (name != null ? name.flattenToShortString() : null)); } } @@ -166,100 +156,144 @@ public class ZenModeHelper implements AudioManagerInternal.RingerModeDelegate { applyRestrictions(); } - public boolean shouldIntercept(NotificationRecord record) { - if (isSystem(record)) { - return false; - } - switch (mZenMode) { - case Global.ZEN_MODE_NO_INTERRUPTIONS: - // #notevenalarms - ZenLog.traceIntercepted(record, "none"); - return true; - case Global.ZEN_MODE_ALARMS: - if (isAlarm(record)) { - // Alarms only - return false; - } - ZenLog.traceIntercepted(record, "alarmsOnly"); - return true; - case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: - if (isAlarm(record)) { - // Alarms are always priority - return false; - } - // allow user-prioritized packages through in priority mode - if (record.getPackagePriority() == Notification.PRIORITY_MAX) { - ZenLog.traceNotIntercepted(record, "priorityApp"); - return false; - } - if (isCall(record)) { - if (!mConfig.allowCalls) { - ZenLog.traceIntercepted(record, "!allowCalls"); - return true; - } - return shouldInterceptAudience(record); - } - if (isMessage(record)) { - if (!mConfig.allowMessages) { - ZenLog.traceIntercepted(record, "!allowMessages"); - return true; - } - return shouldInterceptAudience(record); - } - if (isEvent(record)) { - if (!mConfig.allowEvents) { - ZenLog.traceIntercepted(record, "!allowEvents"); - return true; - } - return false; - } - if (isReminder(record)) { - if (!mConfig.allowReminders) { - ZenLog.traceIntercepted(record, "!allowReminders"); - return true; - } - return false; + public int getZenMode() { + return mZenMode; + } + + public void setManualZenMode(int zenMode, Uri conditionId, String reason) { + setManualZenMode(zenMode, conditionId, reason, true /*setRingerMode*/); + } + + private void setManualZenMode(int zenMode, Uri conditionId, String reason, + boolean setRingerMode) { + if (mConfig == null) return; + if (!Global.isValidZenMode(zenMode)) return; + if (DEBUG) Log.d(TAG, "setManualZenMode " + Global.zenModeToString(zenMode) + + " conditionId=" + conditionId + " reason=" + reason + + " setRingerMode=" + setRingerMode); + final ZenModeConfig newConfig = mConfig.copy(); + if (zenMode == Global.ZEN_MODE_OFF) { + newConfig.manualRule = null; + for (ZenRule automaticRule : newConfig.automaticRules.values()) { + if (automaticRule.isTrueOrUnknown()) { + automaticRule.snoozing = true; } - ZenLog.traceIntercepted(record, "!priority"); - return true; - default: - return false; + } + } else { + final ZenRule newRule = new ZenRule(); + newRule.enabled = true; + newRule.zenMode = zenMode; + newRule.conditionId = conditionId; + newConfig.manualRule = newRule; } + setConfig(newConfig, reason, setRingerMode); + } + + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.print("mZenMode="); + pw.println(Global.zenModeToString(mZenMode)); + dump(pw, prefix, "mConfig", mConfig); + dump(pw, prefix, "mDefaultConfig", mDefaultConfig); + pw.print(prefix); pw.print("mPreviousRingerMode="); pw.println(mPreviousRingerMode); + pw.print(prefix); pw.print("DefaultPhoneApp="); pw.println(mFiltering.getDefaultPhoneApp()); + pw.print(prefix); pw.print("mEffectsSuppressed="); pw.println(mEffectsSuppressed); + mConditions.dump(pw, prefix); } - private boolean shouldInterceptAudience(NotificationRecord record) { - if (!audienceMatches(record.getContactAffinity())) { - ZenLog.traceIntercepted(record, "!audienceMatches"); - return true; + private static void dump(PrintWriter pw, String prefix, String var, ZenModeConfig config) { + pw.print(prefix); pw.print(var); pw.print('='); + if (config == null) { + pw.println(config); + return; + } + pw.printf("allow(calls=%s,events=%s,from=%s,messages=%s,reminders=%s)\n", + config.allowCalls, config.allowEvents, config.allowFrom, config.allowMessages, + config.allowReminders); + pw.print(prefix); pw.print(" manualRule="); pw.println(config.manualRule); + if (config.automaticRules.isEmpty()) return; + final int N = config.automaticRules.size(); + for (int i = 0; i < N; i++) { + pw.print(prefix); pw.print(i == 0 ? " automaticRules=" : " "); + pw.println(config.automaticRules.valueAt(i)); } - return false; } - public int getZenMode() { - return mZenMode; + public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException { + final ZenModeConfig config = ZenModeConfig.readXml(parser, mConfigMigration); + if (config != null) { + if (DEBUG) Log.d(TAG, "readXml"); + setConfig(config, "readXml"); + } } - public void setZenMode(int zenMode, String reason) { - setZenMode(zenMode, reason, true); + public void writeXml(XmlSerializer out) throws IOException { + mConfig.writeXml(out); + } + + public ZenModeConfig getConfig() { + return mConfig; + } + + public boolean setConfig(ZenModeConfig config, String reason) { + return setConfig(config, reason, true /*setRingerMode*/); + } + + private boolean setConfig(ZenModeConfig config, String reason, boolean setRingerMode) { + if (config == null || !config.isValid()) { + Log.w(TAG, "Invalid config in setConfig; " + config); + return false; + } + mConditions.evaluateConfig(config); // may modify config + if (config.equals(mConfig)) return true; + if (DEBUG) Log.d(TAG, "setConfig reason=" + reason); + ZenLog.traceConfig(mConfig, config); + mConfig = config; + dispatchOnConfigChanged(); + final String val = Integer.toString(mConfig.hashCode()); + Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val); + if (!evaluateZenMode(reason, setRingerMode)) { + applyRestrictions(); // evaluateZenMode will also apply restrictions if changed + } + return true; + } + + private int getZenModeSetting() { + return Global.getInt(mContext.getContentResolver(), Global.ZEN_MODE, Global.ZEN_MODE_OFF); + } + + private void setZenModeSetting(int zen) { + Global.putInt(mContext.getContentResolver(), Global.ZEN_MODE, zen); } - private void setZenMode(int zenMode, String reason, boolean setRingerMode) { - ZenLog.traceSetZenMode(zenMode, reason); - if (mZenMode == zenMode) return; - ZenLog.traceUpdateZenMode(mZenMode, zenMode); - mZenMode = zenMode; - Global.putInt(mContext.getContentResolver(), Global.ZEN_MODE, mZenMode); + private boolean evaluateZenMode(String reason, boolean setRingerMode) { + if (DEBUG) Log.d(TAG, "evaluateZenMode"); + final ArraySet<ZenRule> automaticRules = new ArraySet<ZenRule>(); + final int zen = computeZenMode(automaticRules); + if (zen == mZenMode) return false; + ZenLog.traceSetZenMode(zen, reason); + mZenMode = zen; + setZenModeSetting(mZenMode); if (setRingerMode) { applyZenToRingerMode(); } applyRestrictions(); mHandler.postDispatchOnZenModeChanged(); + return true; } - public void readZenModeFromSetting() { - final int newMode = Global.getInt(mContext.getContentResolver(), - Global.ZEN_MODE, Global.ZEN_MODE_OFF); - setZenMode(newMode, "setting"); + private int computeZenMode(ArraySet<ZenRule> automaticRulesOut) { + if (mConfig == null) return Global.ZEN_MODE_OFF; + if (mConfig.manualRule != null) return mConfig.manualRule.zenMode; + int zen = Global.ZEN_MODE_OFF; + for (ZenRule automaticRule : mConfig.automaticRules.values()) { + if (automaticRule.enabled && !automaticRule.snoozing + && automaticRule.isTrueOrUnknown()) { + if (zenSeverity(automaticRule.zenMode) > zenSeverity(zen)) { + zen = automaticRule.zenMode; + } + } + } + return zen; } private void applyRestrictions() { @@ -288,43 +322,6 @@ public class ZenModeHelper implements AudioManagerInternal.RingerModeDelegate { exceptionPackages); } - public void dump(PrintWriter pw, String prefix) { - pw.print(prefix); pw.print("mZenMode="); - pw.println(Global.zenModeToString(mZenMode)); - pw.print(prefix); pw.print("mConfig="); pw.println(mConfig); - pw.print(prefix); pw.print("mDefaultConfig="); pw.println(mDefaultConfig); - pw.print(prefix); pw.print("mPreviousRingerMode="); pw.println(mPreviousRingerMode); - pw.print(prefix); pw.print("mDefaultPhoneApp="); pw.println(mDefaultPhoneApp); - pw.print(prefix); pw.print("mEffectsSuppressed="); pw.println(mEffectsSuppressed); - } - - public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException { - final ZenModeConfig config = ZenModeConfig.readXml(parser); - if (config != null) { - setConfig(config); - } - } - - public void writeXml(XmlSerializer out) throws IOException { - mConfig.writeXml(out); - } - - public ZenModeConfig getConfig() { - return mConfig; - } - - public boolean setConfig(ZenModeConfig config) { - if (config == null || !config.isValid()) return false; - if (config.equals(mConfig)) return true; - ZenLog.traceConfig(mConfig, config); - mConfig = config; - dispatchOnConfigChanged(); - final String val = Integer.toString(mConfig.hashCode()); - Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val); - applyRestrictions(); - return true; - } - private void applyZenToRingerMode() { if (mAudioManager == null) return; // force the ringer mode into compliance @@ -352,81 +349,6 @@ public class ZenModeHelper implements AudioManagerInternal.RingerModeDelegate { } } - @Override // RingerModeDelegate - public int onSetRingerModeInternal(int ringerModeOld, int ringerModeNew, String caller, - int ringerModeExternal, VolumePolicy policy) { - final boolean isChange = ringerModeOld != ringerModeNew; - - int ringerModeExternalOut = ringerModeNew; - - int newZen = -1; - switch (ringerModeNew) { - case AudioManager.RINGER_MODE_SILENT: - if (isChange && policy.doNotDisturbWhenSilent) { - if (mZenMode != Global.ZEN_MODE_NO_INTERRUPTIONS - && mZenMode != Global.ZEN_MODE_ALARMS) { - newZen = Global.ZEN_MODE_NO_INTERRUPTIONS; - } - } - break; - case AudioManager.RINGER_MODE_VIBRATE: - case AudioManager.RINGER_MODE_NORMAL: - if (isChange && ringerModeOld == AudioManager.RINGER_MODE_SILENT - && (mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS - || mZenMode == Global.ZEN_MODE_ALARMS)) { - newZen = Global.ZEN_MODE_OFF; - } else if (mZenMode != Global.ZEN_MODE_OFF) { - ringerModeExternalOut = AudioManager.RINGER_MODE_SILENT; - } - break; - } - if (newZen != -1) { - setZenMode(newZen, "ringerModeInternal", false /*setRingerMode*/); - } - - if (isChange || newZen != -1 || ringerModeExternal != ringerModeExternalOut) { - ZenLog.traceSetRingerModeInternal(ringerModeOld, ringerModeNew, caller, - ringerModeExternal, ringerModeExternalOut); - } - return ringerModeExternalOut; - } - - @Override // RingerModeDelegate - public int onSetRingerModeExternal(int ringerModeOld, int ringerModeNew, String caller, - int ringerModeInternal, VolumePolicy policy) { - int ringerModeInternalOut = ringerModeNew; - final boolean isChange = ringerModeOld != ringerModeNew; - final boolean isVibrate = ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE; - - int newZen = -1; - switch (ringerModeNew) { - case AudioManager.RINGER_MODE_SILENT: - if (isChange) { - if (mZenMode == Global.ZEN_MODE_OFF) { - newZen = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; - } - ringerModeInternalOut = isVibrate ? AudioManager.RINGER_MODE_VIBRATE - : AudioManager.RINGER_MODE_NORMAL; - } else { - ringerModeInternalOut = ringerModeInternal; - } - break; - case AudioManager.RINGER_MODE_VIBRATE: - case AudioManager.RINGER_MODE_NORMAL: - if (mZenMode != Global.ZEN_MODE_OFF) { - newZen = Global.ZEN_MODE_OFF; - } - break; - } - if (newZen != -1) { - setZenMode(newZen, "ringerModeExternal", false /*setRingerMode*/); - } - - ZenLog.traceSetRingerModeExternal(ringerModeOld, ringerModeNew, caller, ringerModeInternal, - ringerModeInternalOut); - return ringerModeInternalOut; - } - private void dispatchOnConfigChanged() { for (Callback callback : mCallbacks) { callback.onConfigChanged(); @@ -439,94 +361,210 @@ public class ZenModeHelper implements AudioManagerInternal.RingerModeDelegate { } } - private static boolean isSystem(NotificationRecord record) { - return record.isCategory(Notification.CATEGORY_SYSTEM); - } - - private static boolean isAlarm(NotificationRecord record) { - return record.isCategory(Notification.CATEGORY_ALARM) - || record.isAudioStream(AudioManager.STREAM_ALARM) - || record.isAudioAttributesUsage(AudioAttributes.USAGE_ALARM); + private static int getZenModeListenerInterruptionFilter(int zen) { + switch (zen) { + case Global.ZEN_MODE_OFF: + return NotificationListenerService.INTERRUPTION_FILTER_ALL; + case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + return NotificationListenerService.INTERRUPTION_FILTER_PRIORITY; + case Global.ZEN_MODE_ALARMS: + return NotificationListenerService.INTERRUPTION_FILTER_ALARMS; + case Global.ZEN_MODE_NO_INTERRUPTIONS: + return NotificationListenerService.INTERRUPTION_FILTER_NONE; + default: + return 0; + } } - private static boolean isEvent(NotificationRecord record) { - return record.isCategory(Notification.CATEGORY_EVENT); + private static int zenModeFromListenerInterruptionFilter(int listenerInterruptionFilter, + int defValue) { + switch (listenerInterruptionFilter) { + case NotificationListenerService.INTERRUPTION_FILTER_ALL: + return Global.ZEN_MODE_OFF; + case NotificationListenerService.INTERRUPTION_FILTER_PRIORITY: + return Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + case NotificationListenerService.INTERRUPTION_FILTER_ALARMS: + return Global.ZEN_MODE_ALARMS; + case NotificationListenerService.INTERRUPTION_FILTER_NONE: + return Global.ZEN_MODE_NO_INTERRUPTIONS; + default: + return defValue; + } } - private static boolean isReminder(NotificationRecord record) { - return record.isCategory(Notification.CATEGORY_REMINDER); + private ZenModeConfig readDefaultConfig(Resources resources) { + XmlResourceParser parser = null; + try { + parser = resources.getXml(R.xml.default_zen_mode_config); + while (parser.next() != XmlPullParser.END_DOCUMENT) { + final ZenModeConfig config = ZenModeConfig.readXml(parser, mConfigMigration); + if (config != null) return config; + } + } catch (Exception e) { + Log.w(TAG, "Error reading default zen mode config from resource", e); + } finally { + IoUtils.closeQuietly(parser); + } + return new ZenModeConfig(); } - public boolean isCall(NotificationRecord record) { - return record != null && (isDefaultPhoneApp(record.sbn.getPackageName()) - || record.isCategory(Notification.CATEGORY_CALL)); + private void appendDefaultScheduleRules(ZenModeConfig config) { + if (config == null) return; + + final ScheduleInfo weeknights = new ScheduleInfo(); + weeknights.days = ZenModeConfig.WEEKNIGHT_DAYS; + weeknights.startHour = 22; + weeknights.endHour = 7; + final ZenRule rule1 = new ZenRule(); + rule1.enabled = false; + rule1.name = mContext.getResources() + .getString(R.string.zen_mode_default_weeknights_name); + rule1.conditionId = ZenModeConfig.toScheduleConditionId(weeknights); + rule1.zenMode = Global.ZEN_MODE_ALARMS; + config.automaticRules.put(config.newRuleId(), rule1); + + final ScheduleInfo weekends = new ScheduleInfo(); + weekends.days = ZenModeConfig.WEEKEND_DAYS; + weekends.startHour = 23; + weekends.startMinute = 30; + weekends.endHour = 10; + final ZenRule rule2 = new ZenRule(); + rule2.enabled = false; + rule2.name = mContext.getResources() + .getString(R.string.zen_mode_default_weekends_name); + rule2.conditionId = ZenModeConfig.toScheduleConditionId(weekends); + rule2.zenMode = Global.ZEN_MODE_ALARMS; + config.automaticRules.put(config.newRuleId(), rule2); + } + + private static int zenSeverity(int zen) { + switch (zen) { + case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: return 1; + case Global.ZEN_MODE_ALARMS: return 2; + case Global.ZEN_MODE_NO_INTERRUPTIONS: return 3; + default: return 0; + } } - private boolean isDefaultPhoneApp(String pkg) { - if (mDefaultPhoneApp == null) { - final TelecomManager telecomm = - (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE); - mDefaultPhoneApp = telecomm != null ? telecomm.getDefaultPhoneApp() : null; - if (DEBUG) Slog.d(TAG, "Default phone app: " + mDefaultPhoneApp); + private final ZenModeConfig.Migration mConfigMigration = new ZenModeConfig.Migration() { + @Override + public ZenModeConfig migrate(ZenModeConfig.XmlV1 v1) { + if (v1 == null) return null; + final ZenModeConfig rt = new ZenModeConfig(); + rt.allowCalls = v1.allowCalls; + rt.allowEvents = v1.allowEvents; + rt.allowFrom = v1.allowFrom; + rt.allowMessages = v1.allowMessages; + rt.allowReminders = v1.allowReminders; + // don't migrate current exit condition + final int[] days = ZenModeConfig.XmlV1.tryParseDays(v1.sleepMode); + if (days != null && days.length > 0) { + Log.i(TAG, "Migrating existing V1 downtime to single schedule"); + final ScheduleInfo schedule = new ScheduleInfo(); + schedule.days = days; + schedule.startHour = v1.sleepStartHour; + schedule.startMinute = v1.sleepStartMinute; + schedule.endHour = v1.sleepEndHour; + schedule.endMinute = v1.sleepEndMinute; + final ZenRule rule = new ZenRule(); + rule.enabled = true; + rule.name = mContext.getResources() + .getString(R.string.zen_mode_downtime_feature_name); + rule.conditionId = ZenModeConfig.toScheduleConditionId(schedule); + rule.zenMode = v1.sleepNone ? Global.ZEN_MODE_NO_INTERRUPTIONS + : Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + rt.automaticRules.put(rt.newRuleId(), rule); + } else { + Log.i(TAG, "No existing V1 downtime found, generating default schedules"); + appendDefaultScheduleRules(rt); + } + return rt; } - return pkg != null && mDefaultPhoneApp != null - && pkg.equals(mDefaultPhoneApp.getPackageName()); - } + }; - private boolean isDefaultMessagingApp(NotificationRecord record) { - final int userId = record.getUserId(); - if (userId == UserHandle.USER_NULL || userId == UserHandle.USER_ALL) return false; - final String defaultApp = Secure.getStringForUser(mContext.getContentResolver(), - Secure.SMS_DEFAULT_APPLICATION, userId); - return Objects.equals(defaultApp, record.sbn.getPackageName()); - } + private final class RingerModeDelegate implements AudioManagerInternal.RingerModeDelegate { + @Override + public String toString() { + return TAG; + } - private boolean isMessage(NotificationRecord record) { - return record.isCategory(Notification.CATEGORY_MESSAGE) || isDefaultMessagingApp(record); - } + @Override + public int onSetRingerModeInternal(int ringerModeOld, int ringerModeNew, String caller, + int ringerModeExternal, VolumePolicy policy) { + final boolean isChange = ringerModeOld != ringerModeNew; + + int ringerModeExternalOut = ringerModeNew; + + int newZen = -1; + switch (ringerModeNew) { + case AudioManager.RINGER_MODE_SILENT: + if (isChange && policy.doNotDisturbWhenSilent) { + if (mZenMode != Global.ZEN_MODE_NO_INTERRUPTIONS + && mZenMode != Global.ZEN_MODE_ALARMS) { + newZen = Global.ZEN_MODE_NO_INTERRUPTIONS; + } + } + break; + case AudioManager.RINGER_MODE_VIBRATE: + case AudioManager.RINGER_MODE_NORMAL: + if (isChange && ringerModeOld == AudioManager.RINGER_MODE_SILENT + && (mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS + || mZenMode == Global.ZEN_MODE_ALARMS)) { + newZen = Global.ZEN_MODE_OFF; + } else if (mZenMode != Global.ZEN_MODE_OFF) { + ringerModeExternalOut = AudioManager.RINGER_MODE_SILENT; + } + break; + } + if (newZen != -1) { + setManualZenMode(newZen, null, "ringerModeInternal", false /*setRingerMode*/); + } - /** - * @param extras extras of the notification with EXTRA_PEOPLE populated - * @param contactsTimeoutMs timeout in milliseconds to wait for contacts response - * @param timeoutAffinity affinity to return when the timeout specified via - * <code>contactsTimeoutMs</code> is hit - */ - public boolean matchesCallFilter(UserHandle userHandle, Bundle extras, - ValidateNotificationPeople validator, int contactsTimeoutMs, float timeoutAffinity) { - final int zen = mZenMode; - if (zen == Global.ZEN_MODE_NO_INTERRUPTIONS) return false; // nothing gets through - if (zen == Global.ZEN_MODE_ALARMS) return false; // not an alarm - if (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) { - if (!mConfig.allowCalls) return false; // no calls get through - if (validator != null) { - final float contactAffinity = validator.getContactAffinity(userHandle, extras, - contactsTimeoutMs, timeoutAffinity); - return audienceMatches(contactAffinity); + if (isChange || newZen != -1 || ringerModeExternal != ringerModeExternalOut) { + ZenLog.traceSetRingerModeInternal(ringerModeOld, ringerModeNew, caller, + ringerModeExternal, ringerModeExternalOut); } + return ringerModeExternalOut; } - return true; - } - @Override - public String toString() { - return TAG; - } + @Override + public int onSetRingerModeExternal(int ringerModeOld, int ringerModeNew, String caller, + int ringerModeInternal, VolumePolicy policy) { + int ringerModeInternalOut = ringerModeNew; + final boolean isChange = ringerModeOld != ringerModeNew; + final boolean isVibrate = ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE; + + int newZen = -1; + switch (ringerModeNew) { + case AudioManager.RINGER_MODE_SILENT: + if (isChange) { + if (mZenMode == Global.ZEN_MODE_OFF) { + newZen = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + } + ringerModeInternalOut = isVibrate ? AudioManager.RINGER_MODE_VIBRATE + : AudioManager.RINGER_MODE_NORMAL; + } else { + ringerModeInternalOut = ringerModeInternal; + } + break; + case AudioManager.RINGER_MODE_VIBRATE: + case AudioManager.RINGER_MODE_NORMAL: + if (mZenMode != Global.ZEN_MODE_OFF) { + newZen = Global.ZEN_MODE_OFF; + } + break; + } + if (newZen != -1) { + setManualZenMode(newZen, null, "ringerModeExternal", false /*setRingerMode*/); + } - private boolean audienceMatches(float contactAffinity) { - switch (mConfig.allowFrom) { - case ZenModeConfig.SOURCE_ANYONE: - return true; - case ZenModeConfig.SOURCE_CONTACT: - return contactAffinity >= ValidateNotificationPeople.VALID_CONTACT; - case ZenModeConfig.SOURCE_STAR: - return contactAffinity >= ValidateNotificationPeople.STARRED_CONTACT; - default: - Slog.w(TAG, "Encountered unknown source: " + mConfig.allowFrom); - return true; + ZenLog.traceSetRingerModeExternal(ringerModeOld, ringerModeNew, caller, + ringerModeInternal, ringerModeInternalOut); + return ringerModeInternalOut; } } - private class SettingsObserver extends ContentObserver { + private final class SettingsObserver extends ContentObserver { private final Uri ZEN_MODE = Global.getUriFor(Global.ZEN_MODE); public SettingsObserver(Handler handler) { @@ -546,12 +584,15 @@ public class ZenModeHelper implements AudioManagerInternal.RingerModeDelegate { public void update(Uri uri) { if (ZEN_MODE.equals(uri)) { - readZenModeFromSetting(); + if (mZenMode != getZenModeSetting()) { + if (DEBUG) Log.d(TAG, "Fixing zen mode setting"); + setZenModeSetting(mZenMode); + } } } } - private class H extends Handler { + private final class H extends Handler { private static final int MSG_DISPATCH = 1; private H(Looper looper) { @@ -577,4 +618,5 @@ public class ZenModeHelper implements AudioManagerInternal.RingerModeDelegate { void onConfigChanged() {} void onZenModeChanged() {} } + } diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java index b4a44a6..ce31f98 100644 --- a/services/core/java/com/android/server/pm/Installer.java +++ b/services/core/java/com/android/server/pm/Installer.java @@ -20,7 +20,9 @@ import android.annotation.Nullable; import android.content.Context; import android.content.pm.PackageStats; import android.os.Build; +import android.text.TextUtils; import android.util.Slog; + import dalvik.system.VMRuntime; import com.android.internal.os.InstallerConnection; @@ -42,9 +44,24 @@ public final class Installer extends SystemService { ping(); } + private static String escapeNull(String arg) { + if (TextUtils.isEmpty(arg)) { + return "!"; + } else { + return arg; + } + } + + @Deprecated public int install(String name, int uid, int gid, String seinfo) { + return install(null, name, uid, gid, seinfo); + } + + public int install(String uuid, String name, int uid, int gid, String seinfo) { StringBuilder builder = new StringBuilder("install"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(name); builder.append(' '); builder.append(uid); @@ -55,43 +72,25 @@ public final class Installer extends SystemService { return mInstaller.execute(builder.toString()); } - public int patchoat(String apkPath, int uid, boolean isPublic, String pkgName, - String instructionSet) { - if (!isValidInstructionSet(instructionSet)) { - Slog.e(TAG, "Invalid instruction set: " + instructionSet); - return -1; - } - - return mInstaller.patchoat(apkPath, uid, isPublic, pkgName, instructionSet); - } - - public int patchoat(String apkPath, int uid, boolean isPublic, String instructionSet) { + public int dexopt(String apkPath, int uid, boolean isPublic, + String instructionSet, int dexoptNeeded) { if (!isValidInstructionSet(instructionSet)) { Slog.e(TAG, "Invalid instruction set: " + instructionSet); return -1; } - return mInstaller.patchoat(apkPath, uid, isPublic, instructionSet); - } - - public int dexopt(String apkPath, int uid, boolean isPublic, String instructionSet) { - if (!isValidInstructionSet(instructionSet)) { - Slog.e(TAG, "Invalid instruction set: " + instructionSet); - return -1; - } - - return mInstaller.dexopt(apkPath, uid, isPublic, instructionSet); + return mInstaller.dexopt(apkPath, uid, isPublic, instructionSet, dexoptNeeded); } public int dexopt(String apkPath, int uid, boolean isPublic, String pkgName, - String instructionSet, boolean vmSafeMode, boolean debuggable, - @Nullable String outputPath) { + String instructionSet, int dexoptNeeded, boolean vmSafeMode, + boolean debuggable, @Nullable String outputPath) { if (!isValidInstructionSet(instructionSet)) { Slog.e(TAG, "Invalid instruction set: " + instructionSet); return -1; } - - return mInstaller.dexopt(apkPath, uid, isPublic, pkgName, instructionSet, vmSafeMode, + return mInstaller.dexopt(apkPath, uid, isPublic, pkgName, + instructionSet, dexoptNeeded, vmSafeMode, debuggable, outputPath); } @@ -146,9 +145,16 @@ public final class Installer extends SystemService { return mInstaller.execute(builder.toString()); } + @Deprecated public int remove(String name, int userId) { + return remove(null, name, userId); + } + + public int remove(String uuid, String name, int userId) { StringBuilder builder = new StringBuilder("remove"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(name); builder.append(' '); builder.append(userId); @@ -164,9 +170,16 @@ public final class Installer extends SystemService { return mInstaller.execute(builder.toString()); } + @Deprecated public int fixUid(String name, int uid, int gid) { + return fixUid(null, name, uid, gid); + } + + public int fixUid(String uuid, String name, int uid, int gid) { StringBuilder builder = new StringBuilder("fixuid"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(name); builder.append(' '); builder.append(uid); @@ -175,27 +188,48 @@ public final class Installer extends SystemService { return mInstaller.execute(builder.toString()); } + @Deprecated public int deleteCacheFiles(String name, int userId) { + return deleteCacheFiles(null, name, userId); + } + + public int deleteCacheFiles(String uuid, String name, int userId) { StringBuilder builder = new StringBuilder("rmcache"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(name); builder.append(' '); builder.append(userId); return mInstaller.execute(builder.toString()); } + @Deprecated public int deleteCodeCacheFiles(String name, int userId) { + return deleteCodeCacheFiles(null, name, userId); + } + + public int deleteCodeCacheFiles(String uuid, String name, int userId) { StringBuilder builder = new StringBuilder("rmcodecache"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(name); builder.append(' '); builder.append(userId); return mInstaller.execute(builder.toString()); } + @Deprecated public int createUserData(String name, int uid, int userId, String seinfo) { + return createUserData(null, name, uid, userId, seinfo); + } + + public int createUserData(String uuid, String name, int uid, int userId, String seinfo) { StringBuilder builder = new StringBuilder("mkuserdata"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(name); builder.append(' '); builder.append(uid); @@ -213,16 +247,30 @@ public final class Installer extends SystemService { return mInstaller.execute(builder.toString()); } + @Deprecated public int removeUserDataDirs(int userId) { + return removeUserDataDirs(null, userId); + } + + public int removeUserDataDirs(String uuid, int userId) { StringBuilder builder = new StringBuilder("rmuser"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(userId); return mInstaller.execute(builder.toString()); } + @Deprecated public int clearUserData(String name, int userId) { + return clearUserData(null, name, userId); + } + + public int clearUserData(String uuid, String name, int userId) { StringBuilder builder = new StringBuilder("rmuserdata"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(name); builder.append(' '); builder.append(userId); @@ -249,15 +297,30 @@ public final class Installer extends SystemService { } } + @Deprecated public int freeCache(long freeStorageSize) { + return freeCache(null, freeStorageSize); + } + + public int freeCache(String uuid, long freeStorageSize) { StringBuilder builder = new StringBuilder("freecache"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(String.valueOf(freeStorageSize)); return mInstaller.execute(builder.toString()); } + @Deprecated public int getSizeInfo(String pkgName, int persona, String apkPath, String libDirPath, String fwdLockApkPath, String asecPath, String[] instructionSets, PackageStats pStats) { + return getSizeInfo(null, pkgName, persona, apkPath, libDirPath, fwdLockApkPath, asecPath, + instructionSets, pStats); + } + + public int getSizeInfo(String uuid, String pkgName, int persona, String apkPath, + String libDirPath, String fwdLockApkPath, String asecPath, String[] instructionSets, + PackageStats pStats) { for (String instructionSet : instructionSets) { if (!isValidInstructionSet(instructionSet)) { Slog.e(TAG, "Invalid instruction set: " + instructionSet); @@ -267,6 +330,8 @@ public final class Installer extends SystemService { StringBuilder builder = new StringBuilder("getsize"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(pkgName); builder.append(' '); builder.append(persona); @@ -306,6 +371,11 @@ public final class Installer extends SystemService { return mInstaller.execute("movefiles"); } + @Deprecated + public int linkNativeLibraryDirectory(String dataPath, String nativeLibPath32, int userId) { + return linkNativeLibraryDirectory(null, dataPath, nativeLibPath32, userId); + } + /** * Links the 32 bit native library directory in an application's data directory to the * real location for backward compatibility. Note that no such symlink is created for @@ -313,7 +383,8 @@ public final class Installer extends SystemService { * * @return -1 on error */ - public int linkNativeLibraryDirectory(String dataPath, String nativeLibPath32, int userId) { + public int linkNativeLibraryDirectory(String uuid, String dataPath, String nativeLibPath32, + int userId) { if (dataPath == null) { Slog.e(TAG, "linkNativeLibraryDirectory dataPath is null"); return -1; @@ -322,7 +393,10 @@ public final class Installer extends SystemService { return -1; } - StringBuilder builder = new StringBuilder("linklib "); + StringBuilder builder = new StringBuilder("linklib"); + builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(dataPath); builder.append(' '); builder.append(nativeLibPath32); @@ -332,9 +406,16 @@ public final class Installer extends SystemService { return mInstaller.execute(builder.toString()); } + @Deprecated public boolean restoreconData(String pkgName, String seinfo, int uid) { + return restoreconData(null, pkgName, seinfo, uid); + } + + public boolean restoreconData(String uuid, String pkgName, String seinfo, int uid) { StringBuilder builder = new StringBuilder("restorecondata"); builder.append(' '); + builder.append(escapeNull(uuid)); + builder.append(' '); builder.append(pkgName); builder.append(' '); builder.append(seinfo != null ? seinfo : "!"); diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java index 680ec4b..4c36fa6 100644 --- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java +++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java @@ -113,64 +113,48 @@ final class PackageDexOptimizer { for (String path : paths) { try { - // This will return DEXOPT_NEEDED if we either cannot find any odex file for this - // package or the one we find does not match the image checksum (i.e. it was - // compiled against an old image). It will return PATCHOAT_NEEDED if we can find a - // odex file and it matches the checksum of the image but not its base address, - // meaning we need to move it. - final byte isDexOptNeeded = DexFile.isDexOptNeededInternal(path, - pkg.packageName, dexCodeInstructionSet, defer); - if (forceDex || (!defer && isDexOptNeeded == DexFile.DEXOPT_NEEDED)) { - File oatDir = createOatDirIfSupported(pkg, dexCodeInstructionSet); - Log.i(TAG, "Running dexopt on: " + path + " pkg=" - + pkg.applicationInfo.packageName + " isa=" + dexCodeInstructionSet - + " vmSafeMode=" + vmSafeMode + " debuggable=" + debuggable - + " oatDir = " + oatDir); - final int sharedGid = UserHandle.getSharedAppGid(pkg.applicationInfo.uid); + final int dexoptNeeded; + if (forceDex) { + dexoptNeeded = DexFile.DEX2OAT_NEEDED; + } else { + dexoptNeeded = DexFile.getDexOptNeeded(path, + pkg.packageName, dexCodeInstructionSet, defer); + } - if (oatDir != null) { - int ret = mPackageManagerService.mInstaller.dexopt( - path, sharedGid, !pkg.isForwardLocked(), pkg.packageName, - dexCodeInstructionSet, vmSafeMode, debuggable, - oatDir.getAbsolutePath()); - if (ret < 0) { - return DEX_OPT_FAILED; - } + if (!forceDex && defer && dexoptNeeded != DexFile.NO_DEXOPT_NEEDED) { + // We're deciding to defer a needed dexopt. Don't bother dexopting for other + // paths and instruction sets. We'll deal with them all together when we process + // our list of deferred dexopts. + addPackageForDeferredDexopt(pkg); + return DEX_OPT_DEFERRED; + } + + if (dexoptNeeded != DexFile.NO_DEXOPT_NEEDED) { + final String dexoptType; + String oatDir = null; + if (dexoptNeeded == DexFile.DEX2OAT_NEEDED) { + dexoptType = "dex2oat"; + oatDir = createOatDirIfSupported(pkg, dexCodeInstructionSet); + } else if (dexoptNeeded == DexFile.PATCHOAT_NEEDED) { + dexoptType = "patchoat"; + } else if (dexoptNeeded == DexFile.SELF_PATCHOAT_NEEDED) { + dexoptType = "self patchoat"; } else { - final int ret = mPackageManagerService.mInstaller - .dexopt(path, sharedGid, - !pkg.isForwardLocked(), pkg.packageName, - dexCodeInstructionSet, - vmSafeMode, debuggable, null); - if (ret < 0) { - return DEX_OPT_FAILED; - } + throw new IllegalStateException("Invalid dexopt needed: " + dexoptNeeded); } - - performedDexOpt = true; - } else if (!defer && isDexOptNeeded == DexFile.PATCHOAT_NEEDED) { - Log.i(TAG, "Running patchoat on: " + pkg.applicationInfo.packageName); + Log.i(TAG, "Running dexopt (" + dexoptType + ") on: " + path + " pkg=" + + pkg.applicationInfo.packageName + " isa=" + dexCodeInstructionSet + + " vmSafeMode=" + vmSafeMode + " debuggable=" + debuggable + + " oatDir = " + oatDir); final int sharedGid = UserHandle.getSharedAppGid(pkg.applicationInfo.uid); - final int ret = mPackageManagerService.mInstaller.patchoat(path, sharedGid, - !pkg.isForwardLocked(), pkg.packageName, dexCodeInstructionSet); - + final int ret = mPackageManagerService.mInstaller.dexopt(path, sharedGid, + !pkg.isForwardLocked(), pkg.packageName, dexCodeInstructionSet, + dexoptNeeded, vmSafeMode, debuggable, oatDir); if (ret < 0) { - // Don't bother running patchoat again if we failed, it will probably - // just result in an error again. Also, don't bother dexopting for other - // paths & ISAs. return DEX_OPT_FAILED; } - performedDexOpt = true; } - - // We're deciding to defer a needed dexopt. Don't bother dexopting for other - // paths and instruction sets. We'll deal with them all together when we process - // our list of deferred dexopts. - if (defer && isDexOptNeeded != DexFile.UP_TO_DATE) { - addPackageForDeferredDexopt(pkg); - return DEX_OPT_DEFERRED; - } } catch (FileNotFoundException e) { Slog.w(TAG, "Apk not found for dexopt: " + path); return DEX_OPT_FAILED; @@ -187,7 +171,7 @@ final class PackageDexOptimizer { } // At this point we haven't failed dexopt and we haven't deferred dexopt. We must - // either have either succeeded dexopt, or have had isDexOptNeededInternal tell us + // either have either succeeded dexopt, or have had getDexOptNeeded tell us // it isn't required. We therefore mark that this package doesn't need dexopt unless // it's forced. performedDexOpt will tell us whether we performed dex-opt or skipped // it. @@ -209,10 +193,11 @@ final class PackageDexOptimizer { * <li>Package location is not a directory, i.e. monolithic install.</li> * </ul> * - * @return oat directory or null, if oat directory cannot be created. + * @return Absolute path to the oat directory or null, if oat directory + * cannot be created. */ @Nullable - private File createOatDirIfSupported(PackageParser.Package pkg, String dexInstructionSet) + private String createOatDirIfSupported(PackageParser.Package pkg, String dexInstructionSet) throws IOException { if (pkg.isSystemApp() && !pkg.isUpdatedSystemApp()) { return null; @@ -222,7 +207,7 @@ final class PackageDexOptimizer { File oatDir = getOatDir(codePath); mPackageManagerService.mInstaller.createOatDir(oatDir.getAbsolutePath(), dexInstructionSet); - return oatDir; + return oatDir.getAbsolutePath(); } return null; } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 80a4351..f5042ed 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -560,7 +560,6 @@ public class PackageManagerService extends IPackageManager.Stub { mIntentFilterVerificationStates.get(verificationId); String packageName = ivs.getPackageName(); - boolean modified = false; ArrayList<PackageParser.ActivityIntentInfo> filters = ivs.getFilters(); final int filterCount = filters.size(); @@ -571,9 +570,8 @@ public class PackageManagerService extends IPackageManager.Stub { } ArrayList<String> domainsList = new ArrayList<>(domainsSet); synchronized (mPackages) { - modified = mSettings.createIntentFilterVerificationIfNeededLPw( - packageName, domainsList); - if (modified) { + if (mSettings.createIntentFilterVerificationIfNeededLPw( + packageName, domainsList) != null) { scheduleWriteSettingsLocked(); } } @@ -634,7 +632,7 @@ public class PackageManagerService extends IPackageManager.Stub { + verificationId + " packageName:" + packageName); return; } - Slog.d(TAG, "Updating IntentFilterVerificationInfo for verificationId: " + Slog.d(TAG, "Updating IntentFilterVerificationInfo for verificationId:" + verificationId); synchronized (mPackages) { @@ -698,8 +696,7 @@ public class PackageManagerService extends IPackageManager.Stub { ivs = createDomainVerificationState(verifierId, userId, verificationId, packageName); } - ArrayList<String> hosts = filter.getHostsList(); - if (!hasValidHosts(hosts)) { + if (!hasValidDomains(filter)) { return false; } ivs.addFilter(filter); @@ -719,17 +716,35 @@ public class PackageManagerService extends IPackageManager.Stub { } } - private static boolean hasValidHosts(ArrayList<String> hosts) { - if (hosts.size() == 0) { - Slog.d(TAG, "IntentFilter does not contain any data hosts"); + private static boolean hasValidDomains(ActivityIntentInfo filter) { + return hasValidDomains(filter, true); + } + + private static boolean hasValidDomains(ActivityIntentInfo filter, boolean logging) { + boolean hasHTTPorHTTPS = filter.hasDataScheme(IntentFilter.SCHEME_HTTP) || + filter.hasDataScheme(IntentFilter.SCHEME_HTTPS); + if (!hasHTTPorHTTPS) { + if (logging) { + Slog.d(TAG, "IntentFilter does not contain any HTTP or HTTPS data scheme"); + } return false; } + ArrayList<String> hosts = filter.getHostsList(); + if (hosts.size() == 0) { + if (logging) { + Slog.d(TAG, "IntentFilter does not contain any data hosts"); + } + // We still return true as this is the case of any Browser + return true; + } String hostEndBase = null; for (String host : hosts) { String[] hostParts = host.split("\\."); // Should be at minimum a host like "example.com" if (hostParts.length < 2) { - Slog.d(TAG, "IntentFilter does not contain a valid data host name: " + host); + if (logging) { + Slog.d(TAG, "IntentFilter does not contain a valid data host name: " + host); + } return false; } // Verify that we have the same ending domain @@ -739,7 +754,9 @@ public class PackageManagerService extends IPackageManager.Stub { hostEndBase = hostEnd; } if (!hostEnd.equalsIgnoreCase(hostEndBase)) { - Slog.d(TAG, "IntentFilter does not contain the same data domains"); + if (logging) { + Slog.d(TAG, "IntentFilter does not contain the same data domains"); + } return false; } } @@ -1830,18 +1847,10 @@ public class PackageManagerService extends IPackageManager.Stub { } try { - byte dexoptRequired = DexFile.isDexOptNeededInternal(lib, null, - dexCodeInstructionSet, - false); - if (dexoptRequired != DexFile.UP_TO_DATE) { + int dexoptNeeded = DexFile.getDexOptNeeded(lib, null, dexCodeInstructionSet, false); + if (dexoptNeeded != DexFile.NO_DEXOPT_NEEDED) { alreadyDexOpted.add(lib); - - // The list of "shared libraries" we have at this point is - if (dexoptRequired == DexFile.DEXOPT_NEEDED) { - mInstaller.dexopt(lib, Process.SYSTEM_UID, true, dexCodeInstructionSet); - } else { - mInstaller.patchoat(lib, Process.SYSTEM_UID, true, dexCodeInstructionSet); - } + mInstaller.dexopt(lib, Process.SYSTEM_UID, true, dexCodeInstructionSet, dexoptNeeded); } } catch (FileNotFoundException e) { Slog.w(TAG, "Library not found: " + lib); @@ -1887,13 +1896,9 @@ public class PackageManagerService extends IPackageManager.Stub { continue; } try { - byte dexoptRequired = DexFile.isDexOptNeededInternal(path, null, - dexCodeInstructionSet, - false); - if (dexoptRequired == DexFile.DEXOPT_NEEDED) { - mInstaller.dexopt(path, Process.SYSTEM_UID, true, dexCodeInstructionSet); - } else if (dexoptRequired == DexFile.PATCHOAT_NEEDED) { - mInstaller.patchoat(path, Process.SYSTEM_UID, true, dexCodeInstructionSet); + int dexoptNeeded = DexFile.getDexOptNeeded(path, null, dexCodeInstructionSet, false); + if (dexoptNeeded != DexFile.NO_DEXOPT_NEEDED) { + mInstaller.dexopt(path, Process.SYSTEM_UID, true, dexCodeInstructionSet, dexoptNeeded); } } catch (FileNotFoundException e) { Slog.w(TAG, "Jar not found: " + path); @@ -2176,6 +2181,8 @@ public class PackageManagerService extends IPackageManager.Stub { mIntentFilterVerifier = new IntentVerifierProxy(mContext, mIntentFilterVerifierComponent); + primeDomainVerificationsLPw(false); + } // synchronized (mPackages) } // synchronized (mInstallLock) @@ -2272,6 +2279,50 @@ public class PackageManagerService extends IPackageManager.Stub { return verifierComponentName; } + private void primeDomainVerificationsLPw(boolean logging) { + Slog.d(TAG, "Start priming domain verification"); + boolean updated = false; + ArrayList<String> allHosts = new ArrayList<>(); + for (PackageParser.Package pkg : mPackages.values()) { + final String packageName = pkg.packageName; + if (!hasDomainURLs(pkg)) { + if (logging) { + Slog.d(TAG, "No priming domain verifications for " + + "package with no domain URLs: " + packageName); + } + continue; + } + for (PackageParser.Activity a : pkg.activities) { + for (ActivityIntentInfo filter : a.intents) { + if (hasValidDomains(filter, false)) { + allHosts.addAll(filter.getHostsList()); + } + } + } + if (allHosts.size() > 0) { + allHosts.add("*"); + } + IntentFilterVerificationInfo ivi = + mSettings.createIntentFilterVerificationIfNeededLPw(packageName, allHosts); + if (ivi != null) { + // We will always log this + Slog.d(TAG, "Priming domain verifications for package: " + packageName); + ivi.setStatus(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS); + updated = true; + } + else { + if (logging) { + Slog.d(TAG, "No priming domain verifications for package: " + packageName); + } + } + allHosts.clear(); + } + if (updated) { + scheduleWriteSettingsLocked(); + } + Slog.d(TAG, "End priming domain verification"); + } + @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { @@ -3904,10 +3955,9 @@ public class PackageManagerService extends IPackageManager.Stub { Collections.sort(result, mResolvePrioritySorter); } result = filterIfNotPrimaryUser(result, userId); - if (result.size() > 1) { + if (result.size() > 1 && hasWebURI(intent)) { return filterCandidatesWithDomainPreferedActivitiesLPr(result); } - return result; } final PackageParser.Package pkg = mPackages.get(pkgName); @@ -3939,14 +3989,30 @@ public class PackageManagerService extends IPackageManager.Stub { return resolveInfos; } + private static boolean hasWebURI(Intent intent) { + if (intent.getData() == null) { + return false; + } + final String scheme = intent.getScheme(); + if (TextUtils.isEmpty(scheme)) { + return false; + } + return scheme.equals(IntentFilter.SCHEME_HTTP) || scheme.equals(IntentFilter.SCHEME_HTTPS); + } + private List<ResolveInfo> filterCandidatesWithDomainPreferedActivitiesLPr( List<ResolveInfo> candidates) { if (DEBUG_PREFERRED) { Slog.v("TAG", "Filtering results with prefered activities. Candidates count: " + candidates.size()); } + final int userId = UserHandle.getCallingUserId(); ArrayList<ResolveInfo> result = new ArrayList<ResolveInfo>(); + ArrayList<ResolveInfo> undefinedList = new ArrayList<ResolveInfo>(); + ArrayList<ResolveInfo> neverList = new ArrayList<ResolveInfo>(); + ArrayList<ResolveInfo> matchAllList = new ArrayList<ResolveInfo>(); + synchronized (mPackages) { final int count = candidates.size(); // First, try to use the domain prefered App @@ -3959,13 +4025,31 @@ public class PackageManagerService extends IPackageManager.Stub { int status = getDomainVerificationStatusLPr(ps, userId); if (status == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS) { result.add(info); + } else if (status == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER) { + neverList.add(info); + } else if (status == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED) { + undefinedList.add(info); + } + // Add to the special match all list (Browser use case) + if (info.handleAllWebDataURI) { + matchAllList.add(info); } } } - // There is not much we can do, add all candidates + // If there is nothing selected, add all candidates and remove the ones that the User + // has explicitely put into the INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER state and + // also remove any Browser Apps ones. + // If there is still none after this pass, add all undefined one and Browser Apps and + // let the User decide with the Disambiguation dialog if there are several ones. if (result.size() == 0) { result.addAll(candidates); } + result.removeAll(neverList); + result.removeAll(matchAllList); + if (result.size() == 0) { + result.addAll(undefinedList); + result.addAll(matchAllList); + } } if (DEBUG_PREFERRED) { Slog.v("TAG", "Filtered results with prefered activities. New candidates count: " + @@ -7843,7 +7927,7 @@ public class PackageManagerService extends IPackageManager.Stub { res.filter = info; } if (info != null) { - res.filterNeedsVerification = info.needsVerification(); + res.handleAllWebDataURI = info.handleAllWebDataURI(); } res.priority = info.getPriority(); res.preferredOrder = activity.owner.mPreferredOrder; @@ -11215,6 +11299,12 @@ public class PackageManagerService extends IPackageManager.Stub { return; } + final boolean hasDomainURLs = hasDomainURLs(pkg); + if (!hasDomainURLs) { + Slog.d(TAG, "No domain URLs, so no need to verify any IntentFilter!"); + return; + } + Slog.d(TAG, "Checking for userId:" + userId + " if any IntentFilter from the " + size + " Activities needs verification ..."); @@ -11222,22 +11312,25 @@ public class PackageManagerService extends IPackageManager.Stub { int count = 0; final String packageName = pkg.packageName; ArrayList<String> allHosts = new ArrayList<>(); + synchronized (mPackages) { for (PackageParser.Activity a : pkg.activities) { for (ActivityIntentInfo filter : a.intents) { - boolean needFilterVerification = filter.needsVerification() && - !filter.isVerified(); - if (needFilterVerification && needNetworkVerificationLPr(filter)) { + boolean needsFilterVerification = filter.needsVerification(); + if (needsFilterVerification && needsNetworkVerificationLPr(filter)) { Slog.d(TAG, "Verification needed for IntentFilter:" + filter.toString()); mIntentFilterVerifier.addOneIntentFilterVerification( verifierUid, userId, verificationId, filter, packageName); count++; - } else { - Slog.d(TAG, "No verification needed for IntentFilter:" + filter.toString()); - ArrayList<String> list = filter.getHostsList(); - if (hasValidHosts(list)) { - allHosts.addAll(list); + } else if (!needsFilterVerification) { + Slog.d(TAG, "No verification needed for IntentFilter:" + + filter.toString()); + if (hasValidDomains(filter)) { + allHosts.addAll(filter.getHostsList()); } + } else { + Slog.d(TAG, "Verification already done for IntentFilter:" + + filter.toString()); } } } @@ -11249,15 +11342,14 @@ public class PackageManagerService extends IPackageManager.Stub { + (count > 1 ? "s" : "") + " for userId:" + userId + "!"); } else { Slog.d(TAG, "No need to start any IntentFilter verification!"); - if (allHosts.size() > 0 && hasDomainURLs(pkg) && - mSettings.createIntentFilterVerificationIfNeededLPw( - packageName, allHosts)) { + if (allHosts.size() > 0 && mSettings.createIntentFilterVerificationIfNeededLPw( + packageName, allHosts) != null) { scheduleWriteSettingsLocked(); } } } - private boolean needNetworkVerificationLPr(ActivityIntentInfo filter) { + private boolean needsNetworkVerificationLPr(ActivityIntentInfo filter) { final ComponentName cn = filter.activity.getComponentName(); final String packageName = cn.getPackageName(); diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 2e2053d..bfcc3db 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -966,19 +966,19 @@ final class Settings { } /* package protected */ - boolean createIntentFilterVerificationIfNeededLPw(String packageName, + IntentFilterVerificationInfo createIntentFilterVerificationIfNeededLPw(String packageName, ArrayList<String> domains) { PackageSetting ps = mPackages.get(packageName); if (ps == null) { Slog.w(PackageManagerService.TAG, "No package known for name: " + packageName); - return false; + return null; } - if (ps.getIntentFilterVerificationInfo() == null) { - IntentFilterVerificationInfo ivi = new IntentFilterVerificationInfo(packageName, domains); + IntentFilterVerificationInfo ivi = ps.getIntentFilterVerificationInfo(); + if (ivi == null) { + ivi = new IntentFilterVerificationInfo(packageName, domains); ps.setIntentFilterVerificationInfo(ivi); - return true; } - return false; + return ivi; } int getIntentFilterVerificationStatusLPr(String packageName, int userId) { diff --git a/services/core/java/com/android/server/power/DeviceIdleController.java b/services/core/java/com/android/server/power/DeviceIdleController.java index dd00446..a23a87b 100644 --- a/services/core/java/com/android/server/power/DeviceIdleController.java +++ b/services/core/java/com/android/server/power/DeviceIdleController.java @@ -377,7 +377,7 @@ public class DeviceIdleController extends SystemService { } } - void scheduleAlarmLocked(long delay, boolean wakeup) { + void scheduleAlarmLocked(long delay, boolean idleUntil) { if (mSigMotionSensor == null) { // If there is no significant motion sensor on this device, then we won't schedule // alarms, because we can't determine if the device is not moving. This effectively @@ -386,8 +386,13 @@ public class DeviceIdleController extends SystemService { return; } mNextAlarmTime = SystemClock.elapsedRealtime() + delay; - mAlarmManager.set(wakeup ? AlarmManager.ELAPSED_REALTIME_WAKEUP - : AlarmManager.ELAPSED_REALTIME, mNextAlarmTime, mAlarmIntent); + if (idleUntil) { + mAlarmManager.setIdleUntil(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mNextAlarmTime, mAlarmIntent); + } else { + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mNextAlarmTime, mAlarmIntent); + } } private void dumpHelp(PrintWriter pw) { diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java index 1d2180e..c1c5c56 100644 --- a/services/midi/java/com/android/server/midi/MidiService.java +++ b/services/midi/java/com/android/server/midi/MidiService.java @@ -372,8 +372,8 @@ public class MidiService extends IMidiManager.Stub { int numOutputPorts, String[] inputPortNames, String[] outputPortNames, Bundle properties, int type) { int uid = Binder.getCallingUid(); - if (type != MidiDeviceInfo.TYPE_VIRTUAL && uid != Process.SYSTEM_UID) { - throw new SecurityException("only system can create non-virtual devices"); + if (type == MidiDeviceInfo.TYPE_USB && uid != Process.SYSTEM_UID) { + throw new SecurityException("only system can create USB devices"); } synchronized (mDevicesByInfo) { diff --git a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java index 7383478..7ea5aa7 100644 --- a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java @@ -39,6 +39,7 @@ import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static android.text.format.DateUtils.WEEK_IN_MILLIS; import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL; +import static org.easymock.EasyMock.anyInt; import static org.easymock.EasyMock.anyLong; import static org.easymock.EasyMock.capture; import static org.easymock.EasyMock.createMock; @@ -879,7 +880,7 @@ public class NetworkStatsServiceTest extends AndroidTestCase { expectLastCall().anyTimes(); mAlarmManager.set(eq(AlarmManager.ELAPSED_REALTIME), anyLong(), anyLong(), anyLong(), - isA(PendingIntent.class), isA(WorkSource.class), + anyInt(), isA(PendingIntent.class), isA(WorkSource.class), isA(AlarmManager.AlarmClockInfo.class)); expectLastCall().atLeastOnce(); diff --git a/services/usb/java/com/android/server/usb/UsbMidiDevice.java b/services/usb/java/com/android/server/usb/UsbMidiDevice.java index 6ece888..671cf01 100644 --- a/services/usb/java/com/android/server/usb/UsbMidiDevice.java +++ b/services/usb/java/com/android/server/usb/UsbMidiDevice.java @@ -45,7 +45,8 @@ public final class UsbMidiDevice implements Closeable { private MidiDeviceServer mServer; - private final MidiEventScheduler mEventScheduler; + // event schedulers for each output port + private final MidiEventScheduler[] mEventSchedulers; private static final int BUFFER_SIZE = 512; @@ -99,10 +100,11 @@ public final class UsbMidiDevice implements Closeable { } mOutputStreams = new FileOutputStream[outputCount]; + mEventSchedulers = new MidiEventScheduler[outputCount]; for (int i = 0; i < outputCount; i++) { mOutputStreams[i] = new FileOutputStream(fileDescriptors[i]); + mEventSchedulers[i] = new MidiEventScheduler(); } - mEventScheduler = new MidiEventScheduler(inputCount); } private boolean register(Context context, Bundle properties) { @@ -116,7 +118,7 @@ public final class UsbMidiDevice implements Closeable { int outputCount = mOutputStreams.length; MidiReceiver[] inputPortReceivers = new MidiReceiver[inputCount]; for (int port = 0; port < inputCount; port++) { - inputPortReceivers[port] = mEventScheduler.getReceiver(port); + inputPortReceivers[port] = mEventSchedulers[port].getReceiver(); } mServer = midiManager.createDeviceServer(inputPortReceivers, outputCount, @@ -126,7 +128,7 @@ public final class UsbMidiDevice implements Closeable { } final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers(); - // Create input thread + // Create input thread which will read from all input ports new Thread("UsbMidiDevice input thread") { @Override public void run() { @@ -161,38 +163,46 @@ public final class UsbMidiDevice implements Closeable { } }.start(); - // Create output thread - new Thread("UsbMidiDevice output thread") { - @Override - public void run() { - while (true) { - MidiEvent event; - try { - event = (MidiEvent)mEventScheduler.waitNextEvent(); - } catch (InterruptedException e) { - // try again - continue; - } - if (event == null) { - break; - } - try { - mOutputStreams[event.portNumber].write(event.data, 0, event.count); - } catch (IOException e) { - Log.e(TAG, "write failed for port " + event.portNumber); + // Create output thread for each output port + for (int port = 0; port < outputCount; port++) { + final MidiEventScheduler eventSchedulerF = mEventSchedulers[port]; + final FileOutputStream outputStreamF = mOutputStreams[port]; + final int portF = port; + + new Thread("UsbMidiDevice output thread " + port) { + @Override + public void run() { + while (true) { + MidiEvent event; + try { + event = (MidiEvent)eventSchedulerF.waitNextEvent(); + } catch (InterruptedException e) { + // try again + continue; + } + if (event == null) { + break; + } + try { + outputStreamF.write(event.data, 0, event.count); + } catch (IOException e) { + Log.e(TAG, "write failed for port " + portF); + } + eventSchedulerF.addEventToPool(event); } - mEventScheduler.addEventToPool(event); + Log.d(TAG, "output thread exit"); } - Log.d(TAG, "output thread exit"); - } - }.start(); + }.start(); + } return true; } @Override public void close() throws IOException { - mEventScheduler.close(); + for (int i = 0; i < mEventSchedulers.length; i++) { + mEventSchedulers[i].close(); + } if (mServer != null) { mServer.close(); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java index 607df2d..fb83956 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java @@ -162,8 +162,8 @@ final class VoiceInteractionSessionConnection implements ServiceConnection { mBindIntent = new Intent(VoiceInteractionService.SERVICE_INTERFACE); mBindIntent.setComponent(mSessionComponentName); mBound = mContext.bindServiceAsUser(mBindIntent, this, - Context.BIND_AUTO_CREATE|Context.BIND_WAIVE_PRIORITY - |Context.BIND_ALLOW_OOM_MANAGEMENT, new UserHandle(mUser)); + Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY + | Context.BIND_ALLOW_OOM_MANAGEMENT, new UserHandle(mUser)); if (mBound) { try { mIWindowManager.addWindowToken(mToken, diff --git a/test-runner/src/android/test/mock/MockCursor.java b/test-runner/src/android/test/mock/MockCursor.java index a37c6eb..28fa0f8 100644 --- a/test-runner/src/android/test/mock/MockCursor.java +++ b/test-runner/src/android/test/mock/MockCursor.java @@ -35,162 +35,209 @@ import android.os.Bundle; * </P> */ public class MockCursor implements Cursor { + @Override public int getColumnCount() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public int getColumnIndex(String columnName) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public int getColumnIndexOrThrow(String columnName) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public String getColumnName(int columnIndex) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public String[] getColumnNames() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public int getCount() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean isNull(int columnIndex) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public int getInt(int columnIndex) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public long getLong(int columnIndex) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public short getShort(int columnIndex) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public float getFloat(int columnIndex) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public double getDouble(int columnIndex) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public byte[] getBlob(int columnIndex) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public String getString(int columnIndex) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override + public void setExtras(Bundle extras) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override public Bundle getExtras() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public int getPosition() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean isAfterLast() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean isBeforeFirst() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean isFirst() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean isLast() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean move(int offset) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean moveToFirst() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean moveToLast() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean moveToNext() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean moveToPrevious() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean moveToPosition(int position) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override + @Deprecated public void deactivate() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public void close() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean isClosed() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override + @Deprecated public boolean requery() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public void registerContentObserver(ContentObserver observer) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public void registerDataSetObserver(DataSetObserver observer) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public Bundle respond(Bundle extras) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public boolean getWantsAllOnMoveCalls() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public void setNotificationUri(ContentResolver cr, Uri uri) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public Uri getNotificationUri() { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public void unregisterContentObserver(ContentObserver observer) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public void unregisterDataSetObserver(DataSetObserver observer) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override public int getType(int columnIndex) { throw new UnsupportedOperationException("unimplemented mock method"); } diff --git a/tests/Compatibility/Android.mk b/tests/Compatibility/Android.mk index 0ec4d9d..c2f89dd 100644 --- a/tests/Compatibility/Android.mk +++ b/tests/Compatibility/Android.mk @@ -25,7 +25,7 @@ LOCAL_SRC_FILES := \ LOCAL_PACKAGE_NAME := AppCompatibilityTest - +LOCAL_CERTIFICATE := platform include $(BUILD_PACKAGE) include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tests/Compatibility/AndroidManifest.xml b/tests/Compatibility/AndroidManifest.xml index 2884532..8ae5bc5 100644 --- a/tests/Compatibility/AndroidManifest.xml +++ b/tests/Compatibility/AndroidManifest.xml @@ -19,7 +19,7 @@ <application > <uses-library android:name="android.test.runner" /> </application> - + <uses-permission android:name="android.permission.REAL_GET_TASKS" /> <instrumentation android:name=".AppCompatibilityRunner" android:targetPackage="com.android.compatibilitytest" diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk index 9cea176..14f558e 100644 --- a/tools/aapt2/Android.mk +++ b/tools/aapt2/Android.mk @@ -29,12 +29,14 @@ sources := \ BinaryResourceParser.cpp \ ConfigDescription.cpp \ Files.cpp \ + Flag.cpp \ JavaClassGenerator.cpp \ Linker.cpp \ Locale.cpp \ Logger.cpp \ ManifestParser.cpp \ ManifestValidator.cpp \ + Png.cpp \ ResChunkPullParser.cpp \ Resolver.cpp \ Resource.cpp \ @@ -69,7 +71,10 @@ testSources := \ XliffXmlPullParser_test.cpp \ XmlFlattener_test.cpp -cIncludes := +cIncludes := \ + external/libpng \ + external/libz + hostLdLibs := hostStaticLibs := \ @@ -78,7 +83,8 @@ hostStaticLibs := \ liblog \ libcutils \ libexpat \ - libziparchive-host + libziparchive-host \ + libpng ifneq ($(strip $(USE_MINGW)),) hostStaticLibs += libz diff --git a/tools/aapt2/Flag.cpp b/tools/aapt2/Flag.cpp new file mode 100644 index 0000000..b1ee8e7 --- /dev/null +++ b/tools/aapt2/Flag.cpp @@ -0,0 +1,109 @@ +#include "Flag.h" +#include "StringPiece.h" + +#include <functional> +#include <iomanip> +#include <iostream> +#include <string> +#include <vector> + +namespace aapt { +namespace flag { + +struct Flag { + std::string name; + std::string description; + std::function<void(const StringPiece&)> action; + bool required; + bool* flagResult; + bool parsed; +}; + +static std::vector<Flag> sFlags; +static std::vector<std::string> sArgs; + +void optionalFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action) { + sFlags.push_back( + Flag{ name.toString(), description.toString(), action, false, nullptr, false }); +} + +void requiredFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action) { + sFlags.push_back( + Flag{ name.toString(), description.toString(), action, true, nullptr, false }); +} + +void optionalSwitch(const StringPiece& name, const StringPiece& description, bool* result) { + sFlags.push_back( + Flag{ name.toString(), description.toString(), {}, false, result, false }); +} + +static void usageAndDie(const StringPiece& command) { + std::cerr << command << " [options]"; + for (const Flag& flag : sFlags) { + if (flag.required) { + std::cerr << " " << flag.name << " arg"; + } + } + std::cerr << " files..." << std::endl << std::endl << "Options:" << std::endl; + + for (const Flag& flag : sFlags) { + std::string command = flag.name; + if (!flag.flagResult) { + command += " arg "; + } + std::cerr << " " << std::setw(30) << std::left << command + << flag.description << std::endl; + } + exit(1); +} + +void parse(int argc, char** argv, const StringPiece& command) { + for (int i = 0; i < argc; i++) { + const StringPiece arg(argv[i]); + if (*arg.data() != '-') { + sArgs.emplace_back(arg.toString()); + continue; + } + + bool match = false; + for (Flag& flag : sFlags) { + if (arg == flag.name) { + match = true; + flag.parsed = true; + if (flag.flagResult) { + *flag.flagResult = true; + } else { + i++; + if (i >= argc) { + std::cerr << flag.name << " missing argument." << std::endl + << std::endl; + usageAndDie(command); + } + flag.action(argv[i]); + } + break; + } + } + + if (!match) { + std::cerr << "unknown option '" << arg << "'." << std::endl << std::endl; + usageAndDie(command); + } + } + + for (const Flag& flag : sFlags) { + if (flag.required && !flag.parsed) { + std::cerr << "missing required flag " << flag.name << std::endl << std::endl; + usageAndDie(command); + } + } +} + +const std::vector<std::string>& getArgs() { + return sArgs; +} + +} // namespace flag +} // namespace aapt diff --git a/tools/aapt2/Flag.h b/tools/aapt2/Flag.h new file mode 100644 index 0000000..32f5f2c --- /dev/null +++ b/tools/aapt2/Flag.h @@ -0,0 +1,28 @@ +#ifndef AAPT_FLAG_H +#define AAPT_FLAG_H + +#include "StringPiece.h" + +#include <functional> +#include <string> +#include <vector> + +namespace aapt { +namespace flag { + +void requiredFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action); + +void optionalFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action); + +void optionalSwitch(const StringPiece& name, const StringPiece& description, bool* result); + +void parse(int argc, char** argv, const StringPiece& command); + +const std::vector<std::string>& getArgs(); + +} // namespace flag +} // namespace aapt + +#endif // AAPT_FLAG_H diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp index cfc5874..3a4b444 100644 --- a/tools/aapt2/Main.cpp +++ b/tools/aapt2/Main.cpp @@ -18,10 +18,12 @@ #include "BigBuffer.h" #include "BinaryResourceParser.h" #include "Files.h" +#include "Flag.h" #include "JavaClassGenerator.h" #include "Linker.h" #include "ManifestParser.h" #include "ManifestValidator.h" +#include "Png.h" #include "ResourceParser.h" #include "ResourceTable.h" #include "ResourceValues.h" @@ -41,6 +43,7 @@ #include <iostream> #include <sstream> #include <sys/stat.h> +#include <utils/Errors.h> using namespace aapt; @@ -107,12 +110,12 @@ std::unique_ptr<FileReference> makeFileReference(StringPool& pool, const StringP * Collect files from 'root', filtering out any files that do not * match the FileFilter 'filter'. */ -bool walkTree(const StringPiece& root, const FileFilter& filter, - std::vector<Source>& outEntries) { +bool walkTree(const Source& root, const FileFilter& filter, + std::vector<Source>* outEntries) { bool error = false; - for (const std::string& dirName : listFiles(root)) { - std::string dir(root.toString()); + for (const std::string& dirName : listFiles(root.path)) { + std::string dir = root.path; appendPath(&dir, dirName); FileType ft = getFileType(dir); @@ -134,13 +137,11 @@ bool walkTree(const StringPiece& root, const FileFilter& filter, } if (ft != FileType::kRegular) { - Logger::error(Source{ file }) - << "not a regular file." - << std::endl; + Logger::error(Source{ file }) << "not a regular file." << std::endl; error = true; continue; } - outEntries.emplace_back(Source{ file }); + outEntries->push_back(Source{ file }); } } return !error; @@ -171,9 +172,6 @@ bool loadBinaryResourceTable(std::shared_ptr<ResourceTable> table, const Source& } bool loadResTable(android::ResTable* table, const Source& source) { - // For NO_ERROR (which on Windows is a MACRO). - using namespace android; - std::ifstream ifs(source.path, std::ifstream::in | std::ifstream::binary); if (!ifs) { Logger::error(source) << strerror(errno) << std::endl; @@ -190,7 +188,7 @@ bool loadResTable(android::ResTable* table, const Source& source) { char* buf = new char[dataSize]; ifs.read(buf, dataSize); - bool result = table->add(buf, dataSize, -1, true) == NO_ERROR; + bool result = table->add(buf, dataSize, -1, true) == android::NO_ERROR; delete [] buf; return result; @@ -323,13 +321,6 @@ bool collectXml(std::shared_ptr<ResourceTable> table, const Source& source, } } - std::unique_ptr<FileReference> fileResource = makeFileReference( - table->getValueStringPool(), - util::utf16ToUtf8(name.entry) + ".xml", - name.type, - config); - table->addResource(name, config, source.line(0), std::move(fileResource)); - for (size_t level : sdkLevels) { Logger::note(source) << "creating v" << level << " versioned file." @@ -347,14 +338,15 @@ bool collectXml(std::shared_ptr<ResourceTable> table, const Source& source, return true; } -struct CompileXml { +struct CompileItem { Source source; ResourceName name; ConfigDescription config; + std::string extension; }; -bool compileXml(std::shared_ptr<Resolver> resolver, const CompileXml& item, - const Source& outputSource, std::queue<CompileXml>* queue) { +bool compileXml(std::shared_ptr<Resolver> resolver, const CompileItem& item, + const Source& outputSource, std::queue<CompileItem>* queue) { std::ifstream in(item.source.path, std::ifstream::binary); if (!in) { Logger::error(item.source) << strerror(errno) << std::endl; @@ -376,7 +368,7 @@ bool compileXml(std::shared_ptr<Resolver> resolver, const CompileXml& item, if (minStrippedSdk.value() > 0) { // Something was stripped, so let's generate a new file // with the version of the smallest SDK version stripped. - CompileXml newWork = item; + CompileItem newWork = item; newWork.config.sdkVersion = minStrippedSdk.value(); queue->push(newWork); } @@ -394,9 +386,51 @@ bool compileXml(std::shared_ptr<Resolver> resolver, const CompileXml& item, return true; } +bool compilePng(const Source& source, const Source& output) { + std::ifstream in(source.path, std::ifstream::binary); + if (!in) { + Logger::error(source) << strerror(errno) << std::endl; + return false; + } + + std::ofstream out(output.path, std::ofstream::binary); + if (!out) { + Logger::error(output) << strerror(errno) << std::endl; + return false; + } + + std::string err; + Png png; + if (!png.process(source, in, out, {}, &err)) { + Logger::error(source) << err << std::endl; + return false; + } + return true; +} + +bool copyFile(const Source& source, const Source& output) { + std::ifstream in(source.path, std::ifstream::binary); + if (!in) { + Logger::error(source) << strerror(errno) << std::endl; + return false; + } + + std::ofstream out(output.path, std::ofstream::binary); + if (!out) { + Logger::error(output) << strerror(errno) << std::endl; + return false; + } + + if (out << in.rdbuf()) { + Logger::error(output) << strerror(errno) << std::endl; + return true; + } + return false; +} + struct AaptOptions { enum class Phase { - LegacyFull, + Full, Collect, Link, Compile, @@ -411,16 +445,26 @@ struct AaptOptions { // The location of the manifest file. Source manifest; - // The files to process. - std::vector<Source> sources; + // The source directories to walk and find resource files. + std::vector<Source> sourceDirs; + + // The resource files to process and collect. + std::vector<Source> collectFiles; + + // The binary table files to link. + std::vector<Source> linkFiles; + + // The resource files to compile. + std::vector<Source> compileFiles; // The libraries these files may reference. std::vector<Source> libraries; - // Output directory. + // Output path. This can be a directory or file + // depending on the phase. Source output; - // Whether to generate a Java Class. + // Directory to in which to generate R.java. Maybe<Source> generateJavaClass; // Whether to output verbose details about @@ -428,9 +472,8 @@ struct AaptOptions { bool verbose = false; }; -bool compileAndroidManifest(std::shared_ptr<Resolver> resolver, const AaptOptions& options) { - using namespace android; - +bool compileAndroidManifest(const std::shared_ptr<Resolver>& resolver, + const AaptOptions& options) { Source outSource = options.output; appendPath(&outSource.path, "AndroidManifest.xml"); @@ -461,8 +504,8 @@ bool compileAndroidManifest(std::shared_ptr<Resolver> resolver, const AaptOption p += b.size; } - ResXMLTree tree; - if (tree.setTo(data.get(), outBuffer.size()) != NO_ERROR) { + android::ResXMLTree tree; + if (tree.setTo(data.get(), outBuffer.size()) != android::NO_ERROR) { return false; } @@ -496,246 +539,117 @@ bool loadAppInfo(const Source& source, AppInfo* outInfo) { return parser.parse(source, pullParser, outInfo); } -/** - * Parses legacy options and walks the source directories collecting - * files to process. - */ -bool prepareLegacy(std::vector<StringPiece>::const_iterator argsIter, - const std::vector<StringPiece>::const_iterator argsEndIter, - AaptOptions &options) { - options.phase = AaptOptions::Phase::LegacyFull; - - std::vector<StringPiece> sourceDirs; - while (argsIter != argsEndIter) { - if (*argsIter == "-S") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-S missing argument." << std::endl; - return false; - } - sourceDirs.push_back(*argsIter); - } else if (*argsIter == "-I") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-I missing argument." << std::endl; - return false; - } - options.libraries.push_back(Source{ argsIter->toString() }); - } else if (*argsIter == "-M") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-M missing argument." << std::endl; - return false; - } +static AaptOptions prepareArgs(int argc, char** argv) { + if (argc < 2) { + std::cerr << "no command specified." << std::endl; + exit(1); + } - if (!options.manifest.path.empty()) { - Logger::error() << "multiple -M flags are not allowed." << std::endl; - return false; - } - options.manifest.path = argsIter->toString(); - } else if (*argsIter == "-o") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-o missing argument." << std::endl; - return false; - } - options.output = Source{ argsIter->toString() }; - } else if (*argsIter == "-J") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-J missing argument." << std::endl; - return false; - } - options.generateJavaClass = make_value<Source>(Source{ argsIter->toString() }); - } else if (*argsIter == "-v") { - options.verbose = true; - } else { - Logger::error() << "unrecognized option '" << *argsIter << "'." << std::endl; - return false; - } + const StringPiece command(argv[1]); + argc -= 2; + argv += 2; - ++argsIter; - } + AaptOptions options; - if (options.manifest.path.empty()) { - Logger::error() << "must specify manifest file with -M." << std::endl; - return false; + StringPiece outputDescription = "place output in file"; + if (command == "package") { + options.phase = AaptOptions::Phase::Full; + outputDescription = "place output in directory"; + } else if (command == "collect") { + options.phase = AaptOptions::Phase::Collect; + } else if (command == "link") { + options.phase = AaptOptions::Phase::Link; + } else if (command == "compile") { + options.phase = AaptOptions::Phase::Compile; + outputDescription = "place output in directory"; + } else { + std::cerr << "invalid command '" << command << "'." << std::endl; + exit(1); } - // Load the App's package name, etc. - if (!loadAppInfo(options.manifest, &options.appInfo)) { - return false; - } + if (options.phase == AaptOptions::Phase::Full) { + flag::requiredFlag("-S", "add a directory in which to find resources", + [&options](const StringPiece& arg) { + options.sourceDirs.push_back(Source{ arg.toString() }); + }); + + flag::requiredFlag("-M", "path to AndroidManifest.xml", + [&options](const StringPiece& arg) { + options.manifest = Source{ arg.toString() }; + }); + + flag::optionalFlag("-I", "add an Android APK to link against", + [&options](const StringPiece& arg) { + options.libraries.push_back(Source{ arg.toString() }); + }); + + flag::optionalFlag("--java", "directory in which to generate R.java", + [&options](const StringPiece& arg) { + options.generateJavaClass = Source{ arg.toString() }; + }); - /** - * Set up the file filter to ignore certain files. - */ - const char* customIgnore = getenv("ANDROID_AAPT_IGNORE"); - FileFilter fileFilter; - if (customIgnore && customIgnore[0]) { - fileFilter.setPattern(customIgnore); } else { - fileFilter.setPattern( - "!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"); - } + flag::requiredFlag("--package", "Android package name", + [&options](const StringPiece& arg) { + options.appInfo.package = util::utf8ToUtf16(arg); + }); - /* - * Enumerate the files in each source directory. - */ - for (const StringPiece& source : sourceDirs) { - if (!walkTree(source, fileFilter, options.sources)) { - return false; + if (options.phase != AaptOptions::Phase::Collect) { + flag::optionalFlag("-I", "add an Android APK to link against", + [&options](const StringPiece& arg) { + options.libraries.push_back(Source{ arg.toString() }); + }); } - } - return true; -} - -bool prepareCollect(std::vector<StringPiece>::const_iterator argsIter, - const std::vector<StringPiece>::const_iterator argsEndIter, - AaptOptions& options) { - options.phase = AaptOptions::Phase::Collect; - while (argsIter != argsEndIter) { - if (*argsIter == "--package") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "--package missing argument." << std::endl; - return false; - } - options.appInfo.package = util::utf8ToUtf16(*argsIter); - } else if (*argsIter == "-o") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-o missing argument." << std::endl; - return false; - } - options.output = Source{ argsIter->toString() }; - } else if (*argsIter == "-v") { - options.verbose = true; - } else if (argsIter->data()[0] != '-') { - options.sources.push_back(Source{ argsIter->toString() }); - } else { - Logger::error() - << "unknown option '" - << *argsIter - << "'." - << std::endl; - return false; + if (options.phase == AaptOptions::Phase::Link) { + flag::optionalFlag("--java", "directory in which to generate R.java", + [&options](const StringPiece& arg) { + options.generateJavaClass = Source{ arg.toString() }; + }); } - ++argsIter; } - return true; -} -bool prepareLink(std::vector<StringPiece>::const_iterator argsIter, - const std::vector<StringPiece>::const_iterator argsEndIter, - AaptOptions& options) { - options.phase = AaptOptions::Phase::Link; + // Common flags for all steps. + flag::requiredFlag("-o", outputDescription, [&options](const StringPiece& arg) { + options.output = Source{ arg.toString() }; + }); + flag::optionalSwitch("-v", "enables verbose logging", &options.verbose); - while (argsIter != argsEndIter) { - if (*argsIter == "--package") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "--package missing argument." << std::endl; - return false; - } - options.appInfo.package = util::utf8ToUtf16(*argsIter); - } else if (*argsIter == "-o") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-o missing argument." << std::endl; - return false; - } - options.output = Source{ argsIter->toString() }; - } else if (*argsIter == "-I") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-I missing argument." << std::endl; - return false; - } - options.libraries.push_back(Source{ argsIter->toString() }); - } else if (*argsIter == "--java") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "--java missing argument." << std::endl; - return false; - } - options.generateJavaClass = make_value<Source>(Source{ argsIter->toString() }); - } else if (*argsIter == "-v") { - options.verbose = true; - } else if (argsIter->data()[0] != '-') { - options.sources.push_back(Source{ argsIter->toString() }); - } else { - Logger::error() - << "unknown option '" - << *argsIter - << "'." - << std::endl; - return false; - } - ++argsIter; - } - return true; -} + // Build the command string for output (eg. "aapt2 compile"). + std::string fullCommand = "aapt2"; + fullCommand += " "; + fullCommand += command.toString(); -bool prepareCompile(std::vector<StringPiece>::const_iterator argsIter, - const std::vector<StringPiece>::const_iterator argsEndIter, - AaptOptions& options) { - options.phase = AaptOptions::Phase::Compile; + // Actually read the command line flags. + flag::parse(argc, argv, fullCommand); - while (argsIter != argsEndIter) { - if (*argsIter == "--package") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "--package missing argument." << std::endl; - return false; - } - options.appInfo.package = util::utf8ToUtf16(*argsIter); - } else if (*argsIter == "-o") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-o missing argument." << std::endl; - return false; - } - options.output = Source{ argsIter->toString() }; - } else if (*argsIter == "-I") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-I missing argument." << std::endl; - return false; - } - options.libraries.push_back(Source{ argsIter->toString() }); - } else if (*argsIter == "-v") { - options.verbose = true; - } else if (argsIter->data()[0] != '-') { - options.sources.push_back(Source{ argsIter->toString() }); - } else { - Logger::error() - << "unknown option '" - << *argsIter - << "'." - << std::endl; - return false; + // Copy all the remaining arguments. + if (options.phase == AaptOptions::Phase::Collect) { + for (const std::string& arg : flag::getArgs()) { + options.collectFiles.push_back(Source{ arg }); + } + } else if (options.phase == AaptOptions::Phase::Compile) { + for (const std::string& arg : flag::getArgs()) { + options.compileFiles.push_back(Source{ arg }); + } + } else if (options.phase == AaptOptions::Phase::Link) { + for (const std::string& arg : flag::getArgs()) { + options.linkFiles.push_back(Source{ arg }); } - ++argsIter; } - return true; + return options; } -struct CollectValuesItem { - Source source; - ConfigDescription config; -}; - -bool collectValues(std::shared_ptr<ResourceTable> table, const CollectValuesItem& item) { - std::ifstream in(item.source.path, std::ifstream::binary); +static bool collectValues(const std::shared_ptr<ResourceTable>& table, const Source& source, + const ConfigDescription& config) { + std::ifstream in(source.path, std::ifstream::binary); if (!in) { - Logger::error(item.source) << strerror(errno) << std::endl; + Logger::error(source) << strerror(errno) << std::endl; return false; } std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(in); - ResourceParser parser(table, item.source, item.config, xmlParser); + ResourceParser parser(table, source, config, xmlParser); return parser.parse(); } @@ -750,7 +664,7 @@ struct ResourcePathData { * Resource file paths are expected to look like: * [--/res/]type[-config]/name */ -Maybe<ResourcePathData> extractResourcePathData(const Source& source) { +static Maybe<ResourcePathData> extractResourcePathData(const Source& source) { std::vector<std::string> parts = util::splitAndLowercase(source.path, '/'); if (parts.size() < 2) { Logger::error(source) << "bad resource path." << std::endl; @@ -792,16 +706,58 @@ Maybe<ResourcePathData> extractResourcePathData(const Source& source) { }; } -static bool doLegacy(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, - const AaptOptions& options) { +bool doAll(AaptOptions* options, const std::shared_ptr<ResourceTable>& table, + const std::shared_ptr<Resolver>& resolver) { + const bool versionStyles = (options->phase == AaptOptions::Phase::Full || + options->phase == AaptOptions::Phase::Link); + const bool verifyNoMissingSymbols = (options->phase == AaptOptions::Phase::Full || + options->phase == AaptOptions::Phase::Link); + const bool compileFiles = (options->phase == AaptOptions::Phase::Full || + options->phase == AaptOptions::Phase::Compile); + const bool flattenTable = (options->phase == AaptOptions::Phase::Full || + options->phase == AaptOptions::Phase::Collect || + options->phase == AaptOptions::Phase::Link); + const bool useExtendedChunks = options->phase == AaptOptions::Phase::Collect; + + // Build the output table path. + Source outputTable = options->output; + if (options->phase == AaptOptions::Phase::Full) { + appendPath(&outputTable.path, "resources.arsc"); + } + bool error = false; - std::queue<CompileXml> xmlCompileQueue; + std::queue<CompileItem> compileQueue; + + // If source directories were specified, walk them looking for resource files. + if (!options->sourceDirs.empty()) { + const char* customIgnore = getenv("ANDROID_AAPT_IGNORE"); + FileFilter fileFilter; + if (customIgnore && customIgnore[0]) { + fileFilter.setPattern(customIgnore); + } else { + fileFilter.setPattern( + "!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"); + } + + for (const Source& source : options->sourceDirs) { + if (!walkTree(source, fileFilter, &options->collectFiles)) { + return false; + } + } + } + + // Load all binary resource tables. + for (const Source& source : options->linkFiles) { + error |= !loadBinaryResourceTable(table, source); + } + + if (error) { + return false; + } - // - // Read values XML files and XML/PNG files. + // Collect all the resource files. // Need to parse the resource type/config/filename. - // - for (const Source& source : options.sources) { + for (const Source& source : options->collectFiles) { Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); if (!maybePathData) { return false; @@ -809,351 +765,154 @@ static bool doLegacy(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resol const ResourcePathData& pathData = maybePathData.value(); if (pathData.resourceDir == u"values") { - if (options.verbose) { + if (options->verbose) { Logger::note(source) << "collecting values..." << std::endl; } - error |= !collectValues(table, CollectValuesItem{ source, pathData.config }); + error |= !collectValues(table, source, pathData.config); continue; } const ResourceType* type = parseResourceType(pathData.resourceDir); if (!type) { - Logger::error(source) - << "invalid resource type '" - << pathData.resourceDir - << "'." - << std::endl; + Logger::error(source) << "invalid resource type '" << pathData.resourceDir << "'." + << std::endl; return false; } ResourceName resourceName = { table->getPackage(), *type, pathData.name }; - if (pathData.extension == "xml") { - if (options.verbose) { - Logger::note(source) << "collecting XML..." << std::endl; - } - error |= !collectXml(table, source, resourceName, pathData.config); - xmlCompileQueue.push(CompileXml{ - source, - resourceName, - pathData.config - }); - } else { - std::unique_ptr<FileReference> fileReference = makeFileReference( - table->getValueStringPool(), - util::utf16ToUtf8(pathData.name) + "." + pathData.extension, - *type, pathData.config); + // Add the file name to the resource table. + std::unique_ptr<FileReference> fileReference = makeFileReference( + table->getValueStringPool(), + util::utf16ToUtf8(pathData.name) + "." + pathData.extension, + *type, pathData.config); + error |= !table->addResource(resourceName, pathData.config, source.line(0), + std::move(fileReference)); - error |= !table->addResource(resourceName, pathData.config, source.line(0), - std::move(fileReference)); + if (pathData.extension == "xml") { + error |= !collectXml(table, source, resourceName, pathData.config); } + + compileQueue.push( + CompileItem{ source, resourceName, pathData.config, pathData.extension }); } if (error) { return false; } - versionStylesForCompat(table); + // Version all styles referencing attributes outside of their specified SDK version. + if (versionStyles) { + versionStylesForCompat(table); + } - // - // Verify all references and data types. - // + // Verify that all references are valid. Linker linker(table, resolver); if (!linker.linkAndValidate()) { - Logger::error() - << "linking failed." - << std::endl; return false; } - const auto& unresolvedRefs = linker.getUnresolvedReferences(); - if (!unresolvedRefs.empty()) { - for (const auto& entry : unresolvedRefs) { - for (const auto& source : entry.second) { - Logger::error(source) - << "unresolved symbol '" - << entry.first - << "'." - << std::endl; + // Verify that all symbols exist. + if (verifyNoMissingSymbols) { + const auto& unresolvedRefs = linker.getUnresolvedReferences(); + if (!unresolvedRefs.empty()) { + for (const auto& entry : unresolvedRefs) { + for (const auto& source : entry.second) { + Logger::error(source) << "unresolved symbol '" << entry.first << "'." + << std::endl; + } } - } - return false; - } - - // - // Compile the XML files. - // - while (!xmlCompileQueue.empty()) { - const CompileXml& item = xmlCompileQueue.front(); - - // Create the output path from the resource name. - std::stringstream outputPath; - outputPath << item.name.type; - if (item.config != ConfigDescription{}) { - outputPath << "-" << item.config.toString(); - } - - Source outSource = options.output; - appendPath(&outSource.path, "res"); - appendPath(&outSource.path, outputPath.str()); - - if (!mkdirs(outSource.path)) { - Logger::error(outSource) << strerror(errno) << std::endl; return false; } - - appendPath(&outSource.path, util::utf16ToUtf8(item.name.entry) + ".xml"); - - if (options.verbose) { - Logger::note(outSource) << "compiling XML file." << std::endl; - } - - error |= !compileXml(resolver, item, outSource, &xmlCompileQueue); - xmlCompileQueue.pop(); - } - - if (error) { - return false; - } - - // - // Compile the AndroidManifest.xml file. - // - if (!compileAndroidManifest(resolver, options)) { - return false; } - // - // Generate the Java R class. - // - if (options.generateJavaClass) { - Source outPath = options.generateJavaClass.value(); - if (options.verbose) { - Logger::note() - << "writing symbols to " - << outPath - << "." - << std::endl; - } - - for (std::string& part : util::split(util::utf16ToUtf8(table->getPackage()), '.')) { - appendPath(&outPath.path, part); - } - - if (!mkdirs(outPath.path)) { - Logger::error(outPath) << strerror(errno) << std::endl; - return false; - } - - appendPath(&outPath.path, "R.java"); - - std::ofstream fout(outPath.path); - if (!fout) { - Logger::error(outPath) << strerror(errno) << std::endl; - return false; - } - - JavaClassGenerator generator(table, JavaClassGenerator::Options{}); - if (!generator.generate(fout)) { - Logger::error(outPath) - << generator.getError() - << "." - << std::endl; - return false; - } - } - - // - // Flatten resource table. - // - if (table->begin() != table->end()) { - BigBuffer buffer(1024); - TableFlattener::Options tableOptions; - tableOptions.useExtendedChunks = false; - TableFlattener flattener(tableOptions); - if (!flattener.flatten(&buffer, *table)) { - Logger::error() - << "failed to flatten resource table->" - << std::endl; - return false; - } - - if (options.verbose) { - Logger::note() - << "Final resource table size=" - << util::formatSize(buffer.size()) - << std::endl; - } + // Compile files. + if (compileFiles) { + // First process any input compile files. + for (const Source& source : options->compileFiles) { + Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); + if (!maybePathData) { + return false; + } - std::string outTable(options.output.path); - appendPath(&outTable, "resources.arsc"); + const ResourcePathData& pathData = maybePathData.value(); + const ResourceType* type = parseResourceType(pathData.resourceDir); + if (!type) { + Logger::error(source) << "invalid resource type '" << pathData.resourceDir + << "'." << std::endl; + return false; + } - std::ofstream fout(outTable, std::ofstream::binary); - if (!fout) { - Logger::error(Source{outTable}) - << strerror(errno) - << "." - << std::endl; - return false; + ResourceName resourceName = { table->getPackage(), *type, pathData.name }; + compileQueue.push( + CompileItem{ source, resourceName, pathData.config, pathData.extension }); } - if (!util::writeAll(fout, buffer)) { - Logger::error(Source{outTable}) - << strerror(errno) - << "." - << std::endl; - return false; - } - fout.flush(); - } - return true; -} - -static bool doCollect(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, - const AaptOptions& options) { - bool error = false; - - // - // Read values XML files and XML/PNG files. - // Need to parse the resource type/config/filename. - // - for (const Source& source : options.sources) { - Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); - if (!maybePathData) { - return false; - } + // Now process the actual compile queue. + for (; !compileQueue.empty(); compileQueue.pop()) { + const CompileItem& item = compileQueue.front(); - const ResourcePathData& pathData = maybePathData.value(); - if (pathData.resourceDir == u"values") { - if (options.verbose) { - Logger::note(source) << "collecting values..." << std::endl; + // Create the output directory path from the resource type and config. + std::stringstream outputPath; + outputPath << item.name.type; + if (item.config != ConfigDescription{}) { + outputPath << "-" << item.config.toString(); } - error |= !collectValues(table, CollectValuesItem{ source, pathData.config }); - continue; - } + Source outSource = options->output; + appendPath(&outSource.path, "res"); + appendPath(&outSource.path, outputPath.str()); - const ResourceType* type = parseResourceType(pathData.resourceDir); - if (!type) { - Logger::error(source) - << "invalid resource type '" - << pathData.resourceDir - << "'." - << std::endl; - return false; - } - - ResourceName resourceName = { table->getPackage(), *type, pathData.name }; - if (pathData.extension == "xml") { - if (options.verbose) { - Logger::note(source) << "collecting XML..." << std::endl; + // Make the directory. + if (!mkdirs(outSource.path)) { + Logger::error(outSource) << strerror(errno) << std::endl; + return false; } - error |= !collectXml(table, source, resourceName, pathData.config); - } else { - std::unique_ptr<FileReference> fileReference = makeFileReference( - table->getValueStringPool(), - util::utf16ToUtf8(pathData.name) + "." + pathData.extension, - *type, - pathData.config); - error |= !table->addResource(resourceName, pathData.config, source.line(0), - std::move(fileReference)); - } - } - - if (error) { - return false; - } + // Add the file name to the directory path. + appendPath(&outSource.path, util::utf16ToUtf8(item.name.entry) + "." + item.extension); - Linker linker(table, resolver); - if (!linker.linkAndValidate()) { - return false; - } + if (item.extension == "xml") { + if (options->verbose) { + Logger::note(outSource) << "compiling XML file." << std::endl; + } - // - // Flatten resource table-> - // - if (table->begin() != table->end()) { - BigBuffer buffer(1024); - TableFlattener::Options tableOptions; - tableOptions.useExtendedChunks = true; - TableFlattener flattener(tableOptions); - if (!flattener.flatten(&buffer, *table)) { - Logger::error() - << "failed to flatten resource table->" - << std::endl; - return false; - } + error |= !compileXml(resolver, item, outSource, &compileQueue); + } else if (item.extension == "png" || item.extension == "9.png") { + if (options->verbose) { + Logger::note(outSource) << "compiling png file." << std::endl; + } - std::ofstream fout(options.output.path, std::ofstream::binary); - if (!fout) { - Logger::error(options.output) - << strerror(errno) - << "." - << std::endl; - return false; + error |= !compilePng(item.source, outSource); + } else { + error |= !copyFile(item.source, outSource); + } } - if (!util::writeAll(fout, buffer)) { - Logger::error(options.output) - << strerror(errno) - << "." - << std::endl; + if (error) { return false; } - fout.flush(); - } - return true; -} - -static bool doLink(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, - const AaptOptions& options) { - bool error = false; - - for (const Source& source : options.sources) { - error |= !loadBinaryResourceTable(table, source); - } - - if (error) { - return false; } - versionStylesForCompat(table); - - Linker linker(table, resolver); - if (!linker.linkAndValidate()) { - return false; - } - - const auto& unresolvedRefs = linker.getUnresolvedReferences(); - if (!unresolvedRefs.empty()) { - for (const auto& entry : unresolvedRefs) { - for (const auto& source : entry.second) { - Logger::error(source) - << "unresolved symbol '" - << entry.first - << "'." - << std::endl; - } + // Compile and validate the AndroidManifest.xml. + if (!options->manifest.path.empty()) { + if (!compileAndroidManifest(resolver, *options)) { + return false; } - return false; } - // - // Generate the Java R class. - // - if (options.generateJavaClass) { - Source outPath = options.generateJavaClass.value(); - if (options.verbose) { - Logger::note() - << "writing symbols to " - << outPath - << "." - << std::endl; + // Generate the Java class file. + if (options->generateJavaClass) { + Source outPath = options->generateJavaClass.value(); + if (options->verbose) { + Logger::note() << "writing symbols to " << outPath << "." << std::endl; } - for (std::string& part : util::split(util::utf16ToUtf8(table->getPackage()), '.')) { + // Build the output directory from the package name. + // Eg. com.android.app -> com/android/app + const std::string packageUtf8 = util::utf16ToUtf8(table->getPackage()); + for (StringPiece part : util::tokenize<char>(packageUtf8, '.')) { appendPath(&outPath.path, part); } @@ -1170,52 +929,37 @@ static bool doLink(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolve return false; } - JavaClassGenerator generator(table, JavaClassGenerator::Options{}); + JavaClassGenerator generator(table, {}); if (!generator.generate(fout)) { - Logger::error(outPath) - << generator.getError() - << "." - << std::endl; + Logger::error(outPath) << generator.getError() << "." << std::endl; return false; } } - // - // Flatten resource table. - // - if (table->begin() != table->end()) { + // Flatten the resource table. + if (flattenTable && table->begin() != table->end()) { BigBuffer buffer(1024); TableFlattener::Options tableOptions; - tableOptions.useExtendedChunks = false; + tableOptions.useExtendedChunks = useExtendedChunks; TableFlattener flattener(tableOptions); if (!flattener.flatten(&buffer, *table)) { - Logger::error() - << "failed to flatten resource table->" - << std::endl; + Logger::error() << "failed to flatten resource table." << std::endl; return false; } - if (options.verbose) { - Logger::note() - << "Final resource table size=" - << util::formatSize(buffer.size()) - << std::endl; + if (options->verbose) { + Logger::note() << "Final resource table size=" << util::formatSize(buffer.size()) + << std::endl; } - std::ofstream fout(options.output.path, std::ofstream::binary); + std::ofstream fout(outputTable.path, std::ofstream::binary); if (!fout) { - Logger::error(options.output) - << strerror(errno) - << "." - << std::endl; + Logger::error(outputTable) << strerror(errno) << "." << std::endl; return false; } if (!util::writeAll(fout, buffer)) { - Logger::error(options.output) - << strerror(errno) - << "." - << std::endl; + Logger::error(outputTable) << strerror(errno) << "." << std::endl; return false; } fout.flush(); @@ -1223,134 +967,24 @@ static bool doLink(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolve return true; } -static bool doCompile(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, - const AaptOptions& options) { - std::queue<CompileXml> xmlCompileQueue; - - for (const Source& source : options.sources) { - Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); - if (!maybePathData) { - return false; - } - - ResourcePathData& pathData = maybePathData.value(); - const ResourceType* type = parseResourceType(pathData.resourceDir); - if (!type) { - Logger::error(source) - << "invalid resource type '" - << pathData.resourceDir - << "'." - << std::endl; - return false; - } - - ResourceName resourceName = { table->getPackage(), *type, pathData.name }; - if (pathData.extension == "xml") { - xmlCompileQueue.push(CompileXml{ - source, - resourceName, - pathData.config - }); - } else { - // TODO(adamlesinski): Handle images here. - } - } - - bool error = false; - while (!xmlCompileQueue.empty()) { - const CompileXml& item = xmlCompileQueue.front(); - - // Create the output path from the resource name. - std::stringstream outputPath; - outputPath << item.name.type; - if (item.config != ConfigDescription{}) { - outputPath << "-" << item.config.toString(); - } - - Source outSource = options.output; - appendPath(&outSource.path, "res"); - appendPath(&outSource.path, outputPath.str()); - - if (!mkdirs(outSource.path)) { - Logger::error(outSource) << strerror(errno) << std::endl; - return false; - } - - appendPath(&outSource.path, util::utf16ToUtf8(item.name.entry) + ".xml"); - - if (options.verbose) { - Logger::note(outSource) << "compiling XML file." << std::endl; - } - - error |= !compileXml(resolver, item, outSource, &xmlCompileQueue); - xmlCompileQueue.pop(); - } - return !error; -} - int main(int argc, char** argv) { Logger::setLog(std::make_shared<Log>(std::cerr, std::cerr)); + AaptOptions options = prepareArgs(argc, argv); - std::vector<StringPiece> args; - args.reserve(argc - 1); - for (int i = 1; i < argc; i++) { - args.emplace_back(argv[i], strlen(argv[i])); - } - - if (args.empty()) { - Logger::error() << "no command specified." << std::endl; - return 1; - } - - AaptOptions options; - - // Check the command we're running. - const StringPiece& command = args.front(); - if (command == "package") { - if (!prepareLegacy(std::begin(args) + 1, std::end(args), options)) { - return 1; - } - } else if (command == "collect") { - if (!prepareCollect(std::begin(args) + 1, std::end(args), options)) { - return 1; - } - } else if (command == "link") { - if (!prepareLink(std::begin(args) + 1, std::end(args), options)) { - return 1; - } - } else if (command == "compile") { - if (!prepareCompile(std::begin(args) + 1, std::end(args), options)) { - return 1; + // If we specified a manifest, go ahead and load the package name from the manifest. + if (!options.manifest.path.empty()) { + if (!loadAppInfo(options.manifest, &options.appInfo)) { + return false; } - } else { - Logger::error() << "unknown command '" << command << "'." << std::endl; - return 1; } - // // Verify we have some common options set. - // - - if (options.sources.empty()) { - Logger::error() << "no sources specified." << std::endl; - return false; - } - - if (options.output.path.empty()) { - Logger::error() << "no output directory specified." << std::endl; - return false; - } - if (options.appInfo.package.empty()) { Logger::error() << "no package name specified." << std::endl; return false; } - - // - // Every phase needs a resource table and a resolver/linker. - // - + // Every phase needs a resource table. std::shared_ptr<ResourceTable> table = std::make_shared<ResourceTable>(); table->setPackage(options.appInfo.package); if (options.appInfo.package == u"android") { @@ -1359,9 +993,7 @@ int main(int argc, char** argv) { table->setPackageId(0x7f); } - // // Load the included libraries. - // std::shared_ptr<android::AssetManager> libraries = std::make_shared<android::AssetManager>(); for (const Source& source : options.libraries) { if (util::stringEndsWith(source.path, ".arsc")) { @@ -1393,33 +1025,9 @@ int main(int argc, char** argv) { // Make the resolver that will cache IDs for us. std::shared_ptr<Resolver> resolver = std::make_shared<Resolver>(table, libraries); - // - // Dispatch to the real phase here. - // - - bool result = true; - switch (options.phase) { - case AaptOptions::Phase::LegacyFull: - result = doLegacy(table, resolver, options); - break; - - case AaptOptions::Phase::Collect: - result = doCollect(table, resolver, options); - break; - - case AaptOptions::Phase::Link: - result = doLink(table, resolver, options); - break; - - case AaptOptions::Phase::Compile: - result = doCompile(table, resolver, options); - break; - } - - if (!result) { - Logger::error() - << "aapt exiting with failures." - << std::endl; + // Do the work. + if (!doAll(&options, table, resolver)) { + Logger::error() << "aapt exiting with failures." << std::endl; return 1; } return 0; diff --git a/tools/aapt2/Png.cpp b/tools/aapt2/Png.cpp new file mode 100644 index 0000000..dd753f1 --- /dev/null +++ b/tools/aapt2/Png.cpp @@ -0,0 +1,1284 @@ +/* + * 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. + */ + +#include "Logger.h" +#include "Png.h" +#include "Source.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> +#include <iostream> +#include <png.h> +#include <sstream> +#include <string> +#include <vector> +#include <zlib.h> + +namespace aapt { + +constexpr bool kDebug = false; +constexpr size_t kPngSignatureSize = 8u; + +struct PngInfo { + ~PngInfo() { + for (png_bytep row : rows) { + if (row != nullptr) { + delete[] row; + } + } + + delete[] xDivs; + delete[] yDivs; + } + + void* serialize9Patch() { + void* serialized = android::Res_png_9patch::serialize(info9Patch, xDivs, yDivs, + colors.data()); + reinterpret_cast<android::Res_png_9patch*>(serialized)->deviceToFile(); + return serialized; + } + + uint32_t width = 0; + uint32_t height = 0; + std::vector<png_bytep> rows; + + bool is9Patch = false; + android::Res_png_9patch info9Patch; + int32_t* xDivs = nullptr; + int32_t* yDivs = nullptr; + std::vector<uint32_t> colors; + + // Layout padding. + bool haveLayoutBounds = false; + int32_t layoutBoundsLeft; + int32_t layoutBoundsTop; + int32_t layoutBoundsRight; + int32_t layoutBoundsBottom; + + // Round rect outline description. + int32_t outlineInsetsLeft; + int32_t outlineInsetsTop; + int32_t outlineInsetsRight; + int32_t outlineInsetsBottom; + float outlineRadius; + uint8_t outlineAlpha; +}; + +static void readDataFromStream(png_structp readPtr, png_bytep data, png_size_t length) { + std::istream* input = reinterpret_cast<std::istream*>(png_get_io_ptr(readPtr)); + if (!input->read(reinterpret_cast<char*>(data), length)) { + png_error(readPtr, strerror(errno)); + } +} + +static void writeDataToStream(png_structp writePtr, png_bytep data, png_size_t length) { + std::ostream* output = reinterpret_cast<std::ostream*>(png_get_io_ptr(writePtr)); + if (!output->write(reinterpret_cast<const char*>(data), length)) { + png_error(writePtr, strerror(errno)); + } +} + +static void flushDataToStream(png_structp writePtr) { + std::ostream* output = reinterpret_cast<std::ostream*>(png_get_io_ptr(writePtr)); + if (!output->flush()) { + png_error(writePtr, strerror(errno)); + } +} + +static void logWarning(png_structp readPtr, png_const_charp warningMessage) { + SourceLogger* logger = reinterpret_cast<SourceLogger*>(png_get_error_ptr(readPtr)); + logger->warn() << warningMessage << "." << std::endl; +} + + +static bool readPng(png_structp readPtr, png_infop infoPtr, PngInfo* outInfo, + std::string* outError) { + if (setjmp(png_jmpbuf(readPtr))) { + *outError = "failed reading png"; + return false; + } + + png_set_sig_bytes(readPtr, kPngSignatureSize); + png_read_info(readPtr, infoPtr); + + int colorType, bitDepth, interlaceType, compressionType; + png_get_IHDR(readPtr, infoPtr, &outInfo->width, &outInfo->height, &bitDepth, &colorType, + &interlaceType, &compressionType, nullptr); + + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(readPtr); + } + + if (colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8) { + png_set_expand_gray_1_2_4_to_8(readPtr); + } + + if (png_get_valid(readPtr, infoPtr, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(readPtr); + } + + if (bitDepth == 16) { + png_set_strip_16(readPtr); + } + + if (!(colorType & PNG_COLOR_MASK_ALPHA)) { + png_set_add_alpha(readPtr, 0xFF, PNG_FILLER_AFTER); + } + + if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + png_set_gray_to_rgb(readPtr); + } + + png_set_interlace_handling(readPtr); + png_read_update_info(readPtr, infoPtr); + + const uint32_t rowBytes = png_get_rowbytes(readPtr, infoPtr); + outInfo->rows.resize(outInfo->height); + for (size_t i = 0; i < outInfo->height; i++) { + outInfo->rows[i] = new png_byte[rowBytes]; + } + + png_read_image(readPtr, outInfo->rows.data()); + png_read_end(readPtr, infoPtr); + return true; +} + +static void checkNinePatchSerialization(android::Res_png_9patch* inPatch, void* data) { + size_t patchSize = inPatch->serializedSize(); + void* newData = malloc(patchSize); + memcpy(newData, data, patchSize); + android::Res_png_9patch* outPatch = inPatch->deserialize(newData); + outPatch->fileToDevice(); + // deserialization is done in place, so outPatch == newData + assert(outPatch == newData); + assert(outPatch->numXDivs == inPatch->numXDivs); + assert(outPatch->numYDivs == inPatch->numYDivs); + assert(outPatch->paddingLeft == inPatch->paddingLeft); + assert(outPatch->paddingRight == inPatch->paddingRight); + assert(outPatch->paddingTop == inPatch->paddingTop); + assert(outPatch->paddingBottom == inPatch->paddingBottom); +/* for (int i = 0; i < outPatch->numXDivs; i++) { + assert(outPatch->getXDivs()[i] == inPatch->getXDivs()[i]); + } + for (int i = 0; i < outPatch->numYDivs; i++) { + assert(outPatch->getYDivs()[i] == inPatch->getYDivs()[i]); + } + for (int i = 0; i < outPatch->numColors; i++) { + assert(outPatch->getColors()[i] == inPatch->getColors()[i]); + }*/ + free(newData); +} + +/*static void dump_image(int w, int h, const png_byte* const* rows, int color_type) { + int i, j, rr, gg, bb, aa; + + int bpp; + if (color_type == PNG_COLOR_TYPE_PALETTE || color_type == PNG_COLOR_TYPE_GRAY) { + bpp = 1; + } else if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + bpp = 2; + } else if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA) { + // We use a padding byte even when there is no alpha + bpp = 4; + } else { + printf("Unknown color type %d.\n", color_type); + } + + for (j = 0; j < h; j++) { + const png_byte* row = rows[j]; + for (i = 0; i < w; i++) { + rr = row[0]; + gg = row[1]; + bb = row[2]; + aa = row[3]; + row += bpp; + + if (i == 0) { + printf("Row %d:", j); + } + switch (bpp) { + case 1: + printf(" (%d)", rr); + break; + case 2: + printf(" (%d %d", rr, gg); + break; + case 3: + printf(" (%d %d %d)", rr, gg, bb); + break; + case 4: + printf(" (%d %d %d %d)", rr, gg, bb, aa); + break; + } + if (i == (w - 1)) { + printf("\n"); + } + } + } +}*/ + +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define ABS(a) ((a)<0?-(a):(a)) + +static void analyze_image(SourceLogger* logger, const PngInfo& imageInfo, int grayscaleTolerance, + png_colorp rgbPalette, png_bytep alphaPalette, + int *paletteEntries, bool *hasTransparency, int *colorType, + png_bytepp outRows) { + int w = imageInfo.width; + int h = imageInfo.height; + int i, j, rr, gg, bb, aa, idx; + uint32_t colors[256], col; + int num_colors = 0; + int maxGrayDeviation = 0; + + bool isOpaque = true; + bool isPalette = true; + bool isGrayscale = true; + + // Scan the entire image and determine if: + // 1. Every pixel has R == G == B (grayscale) + // 2. Every pixel has A == 255 (opaque) + // 3. There are no more than 256 distinct RGBA colors + + if (kDebug) { + printf("Initial image data:\n"); + //dump_image(w, h, imageInfo.rows.data(), PNG_COLOR_TYPE_RGB_ALPHA); + } + + for (j = 0; j < h; j++) { + const png_byte* row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + int odev = maxGrayDeviation; + maxGrayDeviation = MAX(ABS(rr - gg), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(gg - bb), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(bb - rr), maxGrayDeviation); + if (maxGrayDeviation > odev) { + if (kDebug) { + printf("New max dev. = %d at pixel (%d, %d) = (%d %d %d %d)\n", + maxGrayDeviation, i, j, rr, gg, bb, aa); + } + } + + // Check if image is really grayscale + if (isGrayscale) { + if (rr != gg || rr != bb) { + if (kDebug) { + printf("Found a non-gray pixel at %d, %d = (%d %d %d %d)\n", + i, j, rr, gg, bb, aa); + } + isGrayscale = false; + } + } + + // Check if image is really opaque + if (isOpaque) { + if (aa != 0xff) { + if (kDebug) { + printf("Found a non-opaque pixel at %d, %d = (%d %d %d %d)\n", + i, j, rr, gg, bb, aa); + } + isOpaque = false; + } + } + + // Check if image is really <= 256 colors + if (isPalette) { + col = (uint32_t) ((rr << 24) | (gg << 16) | (bb << 8) | aa); + bool match = false; + for (idx = 0; idx < num_colors; idx++) { + if (colors[idx] == col) { + match = true; + break; + } + } + + // Write the palette index for the pixel to outRows optimistically + // We might overwrite it later if we decide to encode as gray or + // gray + alpha + *out++ = idx; + if (!match) { + if (num_colors == 256) { + if (kDebug) { + printf("Found 257th color at %d, %d\n", i, j); + } + isPalette = false; + } else { + colors[num_colors++] = col; + } + } + } + } + } + + *paletteEntries = 0; + *hasTransparency = !isOpaque; + int bpp = isOpaque ? 3 : 4; + int paletteSize = w * h + bpp * num_colors; + + if (kDebug) { + printf("isGrayscale = %s\n", isGrayscale ? "true" : "false"); + printf("isOpaque = %s\n", isOpaque ? "true" : "false"); + printf("isPalette = %s\n", isPalette ? "true" : "false"); + printf("Size w/ palette = %d, gray+alpha = %d, rgb(a) = %d\n", + paletteSize, 2 * w * h, bpp * w * h); + printf("Max gray deviation = %d, tolerance = %d\n", maxGrayDeviation, grayscaleTolerance); + } + + // Choose the best color type for the image. + // 1. Opaque gray - use COLOR_TYPE_GRAY at 1 byte/pixel + // 2. Gray + alpha - use COLOR_TYPE_PALETTE if the number of distinct combinations + // is sufficiently small, otherwise use COLOR_TYPE_GRAY_ALPHA + // 3. RGB(A) - use COLOR_TYPE_PALETTE if the number of distinct colors is sufficiently + // small, otherwise use COLOR_TYPE_RGB{_ALPHA} + if (isGrayscale) { + if (isOpaque) { + *colorType = PNG_COLOR_TYPE_GRAY; // 1 byte/pixel + } else { + // Use a simple heuristic to determine whether using a palette will + // save space versus using gray + alpha for each pixel. + // This doesn't take into account chunk overhead, filtering, LZ + // compression, etc. + if (isPalette && (paletteSize < 2 * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color + } else { + *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel + } + } + } else if (isPalette && (paletteSize < bpp * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; + } else { + if (maxGrayDeviation <= grayscaleTolerance) { + logger->note() << "forcing image to gray (max deviation = " << maxGrayDeviation + << ")." + << std::endl; + *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA; + } else { + *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; + } + } + + // Perform postprocessing of the image or palette data based on the final + // color type chosen + + if (*colorType == PNG_COLOR_TYPE_PALETTE) { + // Create separate RGB and Alpha palettes and set the number of colors + *paletteEntries = num_colors; + + // Create the RGB and alpha palettes + for (int idx = 0; idx < num_colors; idx++) { + col = colors[idx]; + rgbPalette[idx].red = (png_byte) ((col >> 24) & 0xff); + rgbPalette[idx].green = (png_byte) ((col >> 16) & 0xff); + rgbPalette[idx].blue = (png_byte) ((col >> 8) & 0xff); + alphaPalette[idx] = (png_byte) (col & 0xff); + } + } else if (*colorType == PNG_COLOR_TYPE_GRAY || *colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + // If the image is gray or gray + alpha, compact the pixels into outRows + for (j = 0; j < h; j++) { + const png_byte* row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + if (isGrayscale) { + *out++ = rr; + } else { + *out++ = (png_byte) (rr * 0.2126f + gg * 0.7152f + bb * 0.0722f); + } + if (!isOpaque) { + *out++ = aa; + } + } + } + } +} + +static bool writePng(png_structp writePtr, png_infop infoPtr, PngInfo* info, + int grayScaleTolerance, SourceLogger* logger, std::string* outError) { + if (setjmp(png_jmpbuf(writePtr))) { + *outError = "failed to write png"; + return false; + } + + uint32_t width, height; + int colorType, bitDepth, interlaceType, compressionType; + + png_unknown_chunk unknowns[3]; + unknowns[0].data = nullptr; + unknowns[1].data = nullptr; + unknowns[2].data = nullptr; + + png_bytepp outRows = (png_bytepp) malloc((int) info->height * sizeof(png_bytep)); + if (outRows == (png_bytepp) 0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + for (uint32_t i = 0; i < info->height; i++) { + outRows[i] = (png_bytep) malloc(2 * (int) info->width); + if (outRows[i] == (png_bytep) 0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + } + + png_set_compression_level(writePtr, Z_BEST_COMPRESSION); + + if (kDebug) { + logger->note() << "writing image: w = " << info->width + << ", h = " << info->height + << std::endl; + } + + png_color rgbPalette[256]; + png_byte alphaPalette[256]; + bool hasTransparency; + int paletteEntries; + + analyze_image(logger, *info, grayScaleTolerance, rgbPalette, alphaPalette, + &paletteEntries, &hasTransparency, &colorType, outRows); + + // If the image is a 9-patch, we need to preserve it as a ARGB file to make + // sure the pixels will not be pre-dithered/clamped until we decide they are + if (info->is9Patch && (colorType == PNG_COLOR_TYPE_RGB || + colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_PALETTE)) { + colorType = PNG_COLOR_TYPE_RGB_ALPHA; + } + + if (kDebug) { + switch (colorType) { + case PNG_COLOR_TYPE_PALETTE: + logger->note() << "has " << paletteEntries + << " colors" << (hasTransparency ? " (with alpha)" : "") + << ", using PNG_COLOR_TYPE_PALLETTE." + << std::endl; + break; + case PNG_COLOR_TYPE_GRAY: + logger->note() << "is opaque gray, using PNG_COLOR_TYPE_GRAY." << std::endl; + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + logger->note() << "is gray + alpha, using PNG_COLOR_TYPE_GRAY_ALPHA." << std::endl; + break; + case PNG_COLOR_TYPE_RGB: + logger->note() << "is opaque RGB, using PNG_COLOR_TYPE_RGB." << std::endl; + break; + case PNG_COLOR_TYPE_RGB_ALPHA: + logger->note() << "is RGB + alpha, using PNG_COLOR_TYPE_RGB_ALPHA." << std::endl; + break; + } + } + + png_set_IHDR(writePtr, infoPtr, info->width, info->height, 8, colorType, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_PLTE(writePtr, infoPtr, rgbPalette, paletteEntries); + if (hasTransparency) { + png_set_tRNS(writePtr, infoPtr, alphaPalette, paletteEntries, (png_color_16p) 0); + } + png_set_filter(writePtr, 0, PNG_NO_FILTERS); + } else { + png_set_filter(writePtr, 0, PNG_ALL_FILTERS); + } + + if (info->is9Patch) { + int chunkCount = 2 + (info->haveLayoutBounds ? 1 : 0); + int pIndex = info->haveLayoutBounds ? 2 : 1; + int bIndex = 1; + int oIndex = 0; + + // Chunks ordered thusly because older platforms depend on the base 9 patch data being last + png_bytep chunkNames = info->haveLayoutBounds + ? (png_bytep)"npOl\0npLb\0npTc\0" + : (png_bytep)"npOl\0npTc"; + + // base 9 patch data + if (kDebug) { + logger->note() << "adding 9-patch info..." << std::endl; + } + strcpy((char*)unknowns[pIndex].name, "npTc"); + unknowns[pIndex].data = (png_byte*) info->serialize9Patch(); + unknowns[pIndex].size = info->info9Patch.serializedSize(); + // TODO: remove the check below when everything works + checkNinePatchSerialization(&info->info9Patch, unknowns[pIndex].data); + + // automatically generated 9 patch outline data + int chunkSize = sizeof(png_uint_32) * 6; + strcpy((char*)unknowns[oIndex].name, "npOl"); + unknowns[oIndex].data = (png_byte*) calloc(chunkSize, 1); + png_byte outputData[chunkSize]; + memcpy(&outputData, &info->outlineInsetsLeft, 4 * sizeof(png_uint_32)); + ((float*) outputData)[4] = info->outlineRadius; + ((png_uint_32*) outputData)[5] = info->outlineAlpha; + memcpy(unknowns[oIndex].data, &outputData, chunkSize); + unknowns[oIndex].size = chunkSize; + + // optional optical inset / layout bounds data + if (info->haveLayoutBounds) { + int chunkSize = sizeof(png_uint_32) * 4; + strcpy((char*)unknowns[bIndex].name, "npLb"); + unknowns[bIndex].data = (png_byte*) calloc(chunkSize, 1); + memcpy(unknowns[bIndex].data, &info->layoutBoundsLeft, chunkSize); + unknowns[bIndex].size = chunkSize; + } + + for (int i = 0; i < chunkCount; i++) { + unknowns[i].location = PNG_HAVE_PLTE; + } + png_set_keep_unknown_chunks(writePtr, PNG_HANDLE_CHUNK_ALWAYS, + chunkNames, chunkCount); + png_set_unknown_chunks(writePtr, infoPtr, unknowns, chunkCount); + +#if PNG_LIBPNG_VER < 10600 + // Deal with unknown chunk location bug in 1.5.x and earlier. + png_set_unknown_chunk_location(writePtr, infoPtr, 0, PNG_HAVE_PLTE); + if (info->haveLayoutBounds) { + png_set_unknown_chunk_location(writePtr, infoPtr, 1, PNG_HAVE_PLTE); + } +#endif + } + + png_write_info(writePtr, infoPtr); + + png_bytepp rows; + if (colorType == PNG_COLOR_TYPE_RGB || colorType == PNG_COLOR_TYPE_RGB_ALPHA) { + if (colorType == PNG_COLOR_TYPE_RGB) { + png_set_filler(writePtr, 0, PNG_FILLER_AFTER); + } + rows = info->rows.data(); + } else { + rows = outRows; + } + png_write_image(writePtr, rows); + + if (kDebug) { + printf("Final image data:\n"); + //dump_image(info->width, info->height, rows, colorType); + } + + png_write_end(writePtr, infoPtr); + + for (uint32_t i = 0; i < info->height; i++) { + free(outRows[i]); + } + free(outRows); + free(unknowns[0].data); + free(unknowns[1].data); + free(unknowns[2].data); + + png_get_IHDR(writePtr, infoPtr, &width, &height, &bitDepth, &colorType, &interlaceType, + &compressionType, nullptr); + + if (kDebug) { + logger->note() << "image written: w = " << width << ", h = " << height + << ", d = " << bitDepth << ", colors = " << colorType + << ", inter = " << interlaceType << ", comp = " << compressionType + << std::endl; + } + return true; +} + +constexpr uint32_t kColorWhite = 0xffffffffu; +constexpr uint32_t kColorTick = 0xff000000u; +constexpr uint32_t kColorLayoutBoundsTick = 0xff0000ffu; + +enum class TickType { + kNone, + kTick, + kLayoutBounds, + kBoth +}; + +static TickType tickType(png_bytep p, bool transparent, const char** outError) { + png_uint_32 color = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); + + if (transparent) { + if (p[3] == 0) { + return TickType::kNone; + } + if (color == kColorLayoutBoundsTick) { + return TickType::kLayoutBounds; + } + if (color == kColorTick) { + return TickType::kTick; + } + + // Error cases + if (p[3] != 0xff) { + *outError = "Frame pixels must be either solid or transparent " + "(not intermediate alphas)"; + return TickType::kNone; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in transparent frame must be black or red"; + } + return TickType::kTick; + } + + if (p[3] != 0xFF) { + *outError = "White frame must be a solid color (no alpha)"; + } + if (color == kColorWhite) { + return TickType::kNone; + } + if (color == kColorTick) { + return TickType::kTick; + } + if (color == kColorLayoutBoundsTick) { + return TickType::kLayoutBounds; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in white frame must be black or red"; + return TickType::kNone; + } + return TickType::kTick; +} + +enum class TickState { + kStart, + kInside1, + kOutside1 +}; + +static bool getHorizontalTicks(png_bytep row, int width, bool transparent, bool required, + int32_t* outLeft, int32_t* outRight, const char** outError, + uint8_t* outDivs, bool multipleAllowed) { + *outLeft = *outRight = -1; + TickState state = TickState::kStart; + bool found = false; + + for (int i = 1; i < width - 1; i++) { + if (tickType(row+i*4, transparent, outError) == TickType::kTick) { + if (state == TickState::kStart || + (state == TickState::kOutside1 && multipleAllowed)) { + *outLeft = i-1; + *outRight = width-2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TickState::kInside1; + } else if (state == TickState::kOutside1) { + *outError = "Can't have more than one marked region along edge"; + *outLeft = i; + return false; + } + } else if (!*outError) { + if (state == TickState::kInside1) { + // We're done with this div. Move on to the next. + *outRight = i-1; + outRight += 2; + outLeft += 2; + state = TickState::kOutside1; + } + } else { + *outLeft = i; + return false; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outLeft = -1; + return false; + } + return true; +} + +static bool getVerticalTicks(png_bytepp rows, int offset, int height, bool transparent, + bool required, int32_t* outTop, int32_t* outBottom, + const char** outError, uint8_t* outDivs, bool multipleAllowed) { + *outTop = *outBottom = -1; + TickState state = TickState::kStart; + bool found = false; + + for (int i = 1; i < height - 1; i++) { + if (tickType(rows[i]+offset, transparent, outError) == TickType::kTick) { + if (state == TickState::kStart || + (state == TickState::kOutside1 && multipleAllowed)) { + *outTop = i-1; + *outBottom = height-2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TickState::kInside1; + } else if (state == TickState::kOutside1) { + *outError = "Can't have more than one marked region along edge"; + *outTop = i; + return false; + } + } else if (!*outError) { + if (state == TickState::kInside1) { + // We're done with this div. Move on to the next. + *outBottom = i-1; + outTop += 2; + outBottom += 2; + state = TickState::kOutside1; + } + } else { + *outTop = i; + return false; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outTop = -1; + return false; + } + return true; +} + +static bool getHorizontalLayoutBoundsTicks(png_bytep row, int width, bool transparent, + bool /* required */, int32_t* outLeft, + int32_t* outRight, const char** outError) { + *outLeft = *outRight = 0; + + // Look for left tick + if (tickType(row + 4, transparent, outError) == TickType::kLayoutBounds) { + // Starting with a layout padding tick + int i = 1; + while (i < width - 1) { + (*outLeft)++; + i++; + if (tickType(row + i * 4, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + + // Look for right tick + if (tickType(row + (width - 2) * 4, transparent, outError) == TickType::kLayoutBounds) { + // Ending with a layout padding tick + int i = width - 2; + while (i > 1) { + (*outRight)++; + i--; + if (tickType(row+i*4, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + return true; +} + +static bool getVerticalLayoutBoundsTicks(png_bytepp rows, int offset, int height, bool transparent, + bool /* required */, int32_t* outTop, int32_t* outBottom, + const char** outError) { + *outTop = *outBottom = 0; + + // Look for top tick + if (tickType(rows[1] + offset, transparent, outError) == TickType::kLayoutBounds) { + // Starting with a layout padding tick + int i = 1; + while (i < height - 1) { + (*outTop)++; + i++; + if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + + // Look for bottom tick + if (tickType(rows[height - 2] + offset, transparent, outError) == TickType::kLayoutBounds) { + // Ending with a layout padding tick + int i = height - 2; + while (i > 1) { + (*outBottom)++; + i--; + if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + return true; +} + +static void findMaxOpacity(png_bytepp rows, int startX, int startY, int endX, int endY, + int dX, int dY, int* outInset) { + uint8_t maxOpacity = 0; + int inset = 0; + *outInset = 0; + for (int x = startX, y = startY; x != endX && y != endY; x += dX, y += dY, inset++) { + png_byte* color = rows[y] + x * 4; + uint8_t opacity = color[3]; + if (opacity > maxOpacity) { + maxOpacity = opacity; + *outInset = inset; + } + if (opacity == 0xff) return; + } +} + +static uint8_t maxAlphaOverRow(png_bytep row, int startX, int endX) { + uint8_t maxAlpha = 0; + for (int x = startX; x < endX; x++) { + uint8_t alpha = (row + x * 4)[3]; + if (alpha > maxAlpha) maxAlpha = alpha; + } + return maxAlpha; +} + +static uint8_t maxAlphaOverCol(png_bytepp rows, int offsetX, int startY, int endY) { + uint8_t maxAlpha = 0; + for (int y = startY; y < endY; y++) { + uint8_t alpha = (rows[y] + offsetX * 4)[3]; + if (alpha > maxAlpha) maxAlpha = alpha; + } + return maxAlpha; +} + +static void getOutline(PngInfo* image) { + int midX = image->width / 2; + int midY = image->height / 2; + int endX = image->width - 2; + int endY = image->height - 2; + + // find left and right extent of nine patch content on center row + if (image->width > 4) { + findMaxOpacity(image->rows.data(), 1, midY, midX, -1, 1, 0, &image->outlineInsetsLeft); + findMaxOpacity(image->rows.data(), endX, midY, midX, -1, -1, 0, + &image->outlineInsetsRight); + } else { + image->outlineInsetsLeft = 0; + image->outlineInsetsRight = 0; + } + + // find top and bottom extent of nine patch content on center column + if (image->height > 4) { + findMaxOpacity(image->rows.data(), midX, 1, -1, midY, 0, 1, &image->outlineInsetsTop); + findMaxOpacity(image->rows.data(), midX, endY, -1, midY, 0, -1, + &image->outlineInsetsBottom); + } else { + image->outlineInsetsTop = 0; + image->outlineInsetsBottom = 0; + } + + int innerStartX = 1 + image->outlineInsetsLeft; + int innerStartY = 1 + image->outlineInsetsTop; + int innerEndX = endX - image->outlineInsetsRight; + int innerEndY = endY - image->outlineInsetsBottom; + int innerMidX = (innerEndX + innerStartX) / 2; + int innerMidY = (innerEndY + innerStartY) / 2; + + // assuming the image is a round rect, compute the radius by marching + // diagonally from the top left corner towards the center + image->outlineAlpha = std::max( + maxAlphaOverRow(image->rows[innerMidY], innerStartX, innerEndX), + maxAlphaOverCol(image->rows.data(), innerMidX, innerStartY, innerStartY)); + + int diagonalInset = 0; + findMaxOpacity(image->rows.data(), innerStartX, innerStartY, innerMidX, innerMidY, 1, 1, + &diagonalInset); + + /* Determine source radius based upon inset: + * sqrt(r^2 + r^2) = sqrt(i^2 + i^2) + r + * sqrt(2) * r = sqrt(2) * i + r + * (sqrt(2) - 1) * r = sqrt(2) * i + * r = sqrt(2) / (sqrt(2) - 1) * i + */ + image->outlineRadius = 3.4142f * diagonalInset; + + if (kDebug) { + printf("outline insets %d %d %d %d, rad %f, alpha %x\n", + image->outlineInsetsLeft, + image->outlineInsetsTop, + image->outlineInsetsRight, + image->outlineInsetsBottom, + image->outlineRadius, + image->outlineAlpha); + } +} + +static uint32_t getColor(png_bytepp rows, int left, int top, int right, int bottom) { + png_bytep color = rows[top] + left*4; + + if (left > right || top > bottom) { + return android::Res_png_9patch::TRANSPARENT_COLOR; + } + + while (top <= bottom) { + for (int i = left; i <= right; i++) { + png_bytep p = rows[top]+i*4; + if (color[3] == 0) { + if (p[3] != 0) { + return android::Res_png_9patch::NO_COLOR; + } + } else if (p[0] != color[0] || p[1] != color[1] || + p[2] != color[2] || p[3] != color[3]) { + return android::Res_png_9patch::NO_COLOR; + } + } + top++; + } + + if (color[3] == 0) { + return android::Res_png_9patch::TRANSPARENT_COLOR; + } + return (color[3]<<24) | (color[0]<<16) | (color[1]<<8) | color[2]; +} + +static bool do9Patch(PngInfo* image, std::string* outError) { + image->is9Patch = true; + + int W = image->width; + int H = image->height; + int i, j; + + const int maxSizeXDivs = W * sizeof(int32_t); + const int maxSizeYDivs = H * sizeof(int32_t); + int32_t* xDivs = image->xDivs = new int32_t[W]; + int32_t* yDivs = image->yDivs = new int32_t[H]; + uint8_t numXDivs = 0; + uint8_t numYDivs = 0; + + int8_t numColors; + int numRows; + int numCols; + int top; + int left; + int right; + int bottom; + memset(xDivs, -1, maxSizeXDivs); + memset(yDivs, -1, maxSizeYDivs); + image->info9Patch.paddingLeft = image->info9Patch.paddingRight = -1; + image->info9Patch.paddingTop = image->info9Patch.paddingBottom = -1; + image->layoutBoundsLeft = image->layoutBoundsRight = 0; + image->layoutBoundsTop = image->layoutBoundsBottom = 0; + + png_bytep p = image->rows[0]; + bool transparent = p[3] == 0; + bool hasColor = false; + + const char* errorMsg = nullptr; + int errorPixel = -1; + const char* errorEdge = nullptr; + + int colorIndex = 0; + std::vector<png_bytep> newRows; + + // Validate size... + if (W < 3 || H < 3) { + errorMsg = "Image must be at least 3x3 (1x1 without frame) pixels"; + goto getout; + } + + // Validate frame... + if (!transparent && + (p[0] != 0xFF || p[1] != 0xFF || p[2] != 0xFF || p[3] != 0xFF)) { + errorMsg = "Must have one-pixel frame that is either transparent or white"; + goto getout; + } + + // Find left and right of sizing areas... + if (!getHorizontalTicks(p, W, transparent, true, &xDivs[0], &xDivs[1], &errorMsg, &numXDivs, + true)) { + errorPixel = xDivs[0]; + errorEdge = "top"; + goto getout; + } + + // Find top and bottom of sizing areas... + if (!getVerticalTicks(image->rows.data(), 0, H, transparent, true, &yDivs[0], &yDivs[1], + &errorMsg, &numYDivs, true)) { + errorPixel = yDivs[0]; + errorEdge = "left"; + goto getout; + } + + // Copy patch size data into image... + image->info9Patch.numXDivs = numXDivs; + image->info9Patch.numYDivs = numYDivs; + + // Find left and right of padding area... + if (!getHorizontalTicks(image->rows[H-1], W, transparent, false, + &image->info9Patch.paddingLeft, &image->info9Patch.paddingRight, + &errorMsg, nullptr, false)) { + errorPixel = image->info9Patch.paddingLeft; + errorEdge = "bottom"; + goto getout; + } + + // Find top and bottom of padding area... + if (!getVerticalTicks(image->rows.data(), (W-1)*4, H, transparent, false, + &image->info9Patch.paddingTop, &image->info9Patch.paddingBottom, + &errorMsg, nullptr, false)) { + errorPixel = image->info9Patch.paddingTop; + errorEdge = "right"; + goto getout; + } + + // Find left and right of layout padding... + getHorizontalLayoutBoundsTicks(image->rows[H-1], W, transparent, false, + &image->layoutBoundsLeft, &image->layoutBoundsRight, &errorMsg); + + getVerticalLayoutBoundsTicks(image->rows.data(), (W-1)*4, H, transparent, false, + &image->layoutBoundsTop, &image->layoutBoundsBottom, &errorMsg); + + image->haveLayoutBounds = image->layoutBoundsLeft != 0 + || image->layoutBoundsRight != 0 + || image->layoutBoundsTop != 0 + || image->layoutBoundsBottom != 0; + + if (image->haveLayoutBounds) { + if (kDebug) { + printf("layoutBounds=%d %d %d %d\n", image->layoutBoundsLeft, image->layoutBoundsTop, + image->layoutBoundsRight, image->layoutBoundsBottom); + } + } + + // use opacity of pixels to estimate the round rect outline + getOutline(image); + + // If padding is not yet specified, take values from size. + if (image->info9Patch.paddingLeft < 0) { + image->info9Patch.paddingLeft = xDivs[0]; + image->info9Patch.paddingRight = W - 2 - xDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingRight = W - 2 - image->info9Patch.paddingRight; + } + if (image->info9Patch.paddingTop < 0) { + image->info9Patch.paddingTop = yDivs[0]; + image->info9Patch.paddingBottom = H - 2 - yDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingBottom = H - 2 - image->info9Patch.paddingBottom; + } + +/* if (kDebug) { + printf("Size ticks for %s: x0=%d, x1=%d, y0=%d, y1=%d\n", imageName, + xDivs[0], xDivs[1], + yDivs[0], yDivs[1]); + printf("padding ticks for %s: l=%d, r=%d, t=%d, b=%d\n", imageName, + image->info9Patch.paddingLeft, image->info9Patch.paddingRight, + image->info9Patch.paddingTop, image->info9Patch.paddingBottom); + }*/ + + // Remove frame from image. + newRows.resize(H - 2); + for (i = 0; i < H - 2; i++) { + newRows[i] = image->rows[i + 1]; + memmove(newRows[i], newRows[i] + 4, (W - 2) * 4); + } + image->rows.swap(newRows); + + image->width -= 2; + W = image->width; + image->height -= 2; + H = image->height; + + // Figure out the number of rows and columns in the N-patch + numCols = numXDivs + 1; + if (xDivs[0] == 0) { // Column 1 is strechable + numCols--; + } + if (xDivs[numXDivs - 1] == W) { + numCols--; + } + numRows = numYDivs + 1; + if (yDivs[0] == 0) { // Row 1 is strechable + numRows--; + } + if (yDivs[numYDivs - 1] == H) { + numRows--; + } + + // Make sure the amount of rows and columns will fit in the number of + // colors we can use in the 9-patch format. + if (numRows * numCols > 0x7F) { + errorMsg = "Too many rows and columns in 9-patch perimeter"; + goto getout; + } + + numColors = numRows * numCols; + image->info9Patch.numColors = numColors; + image->colors.resize(numColors); + + // Fill in color information for each patch. + + uint32_t c; + top = 0; + + // The first row always starts with the top being at y=0 and the bottom + // being either yDivs[1] (if yDivs[0]=0) of yDivs[0]. In the former case + // the first row is stretchable along the Y axis, otherwise it is fixed. + // The last row always ends with the bottom being bitmap.height and the top + // being either yDivs[numYDivs-2] (if yDivs[numYDivs-1]=bitmap.height) or + // yDivs[numYDivs-1]. In the former case the last row is stretchable along + // the Y axis, otherwise it is fixed. + // + // The first and last columns are similarly treated with respect to the X + // axis. + // + // The above is to help explain some of the special casing that goes on the + // code below. + + // The initial yDiv and whether the first row is considered stretchable or + // not depends on whether yDiv[0] was zero or not. + for (j = (yDivs[0] == 0 ? 1 : 0); j <= numYDivs && top < H; j++) { + if (j == numYDivs) { + bottom = H; + } else { + bottom = yDivs[j]; + } + left = 0; + // The initial xDiv and whether the first column is considered + // stretchable or not depends on whether xDiv[0] was zero or not. + for (i = xDivs[0] == 0 ? 1 : 0; i <= numXDivs && left < W; i++) { + if (i == numXDivs) { + right = W; + } else { + right = xDivs[i]; + } + c = getColor(image->rows.data(), left, top, right - 1, bottom - 1); + image->colors[colorIndex++] = c; + if (kDebug) { + if (c != android::Res_png_9patch::NO_COLOR) { + hasColor = true; + } + } + left = right; + } + top = bottom; + } + + assert(colorIndex == numColors); + + if (kDebug && hasColor) { + for (i = 0; i < numColors; i++) { + if (i == 0) printf("Colors:\n"); + printf(" #%08x", image->colors[i]); + if (i == numColors - 1) printf("\n"); + } + } +getout: + if (errorMsg) { + std::stringstream err; + err << "9-patch malformed: " << errorMsg; + if (!errorEdge) { + err << "." << std::endl; + if (errorPixel >= 0) { + err << "Found at pixel #" << errorPixel << " along " << errorEdge << " edge"; + } else { + err << "Found along " << errorEdge << " edge"; + } + } + *outError = err.str(); + return false; + } + return true; +} + + +bool Png::process(const Source& source, std::istream& input, std::ostream& output, + const Options& options, std::string* outError) { + png_byte signature[kPngSignatureSize]; + + // Read the PNG signature first. + if (!input.read(reinterpret_cast<char*>(signature), kPngSignatureSize)) { + *outError = strerror(errno); + return false; + } + + // If the PNG signature doesn't match, bail early. + if (png_sig_cmp(signature, 0, kPngSignatureSize) != 0) { + *outError = "not a valid png file"; + return false; + } + + SourceLogger logger(source); + bool result = false; + png_structp readPtr = nullptr; + png_infop infoPtr = nullptr; + png_structp writePtr = nullptr; + png_infop writeInfoPtr = nullptr; + PngInfo pngInfo = {}; + + readPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); + if (!readPtr) { + *outError = "failed to allocate read ptr"; + goto bail; + } + + infoPtr = png_create_info_struct(readPtr); + if (!infoPtr) { + *outError = "failed to allocate info ptr"; + goto bail; + } + + png_set_error_fn(readPtr, reinterpret_cast<png_voidp>(&logger), nullptr, logWarning); + + // Set the read function to read from std::istream. + png_set_read_fn(readPtr, (png_voidp)&input, readDataFromStream); + + if (!readPng(readPtr, infoPtr, &pngInfo, outError)) { + goto bail; + } + + if (util::stringEndsWith(source.path, ".9.png")) { + if (!do9Patch(&pngInfo, outError)) { + goto bail; + } + } + + writePtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); + if (!writePtr) { + *outError = "failed to allocate write ptr"; + goto bail; + } + + writeInfoPtr = png_create_info_struct(writePtr); + if (!writeInfoPtr) { + *outError = "failed to allocate write info ptr"; + goto bail; + } + + png_set_error_fn(writePtr, nullptr, nullptr, logWarning); + + // Set the write function to write to std::ostream. + png_set_write_fn(writePtr, (png_voidp)&output, writeDataToStream, flushDataToStream); + + if (!writePng(writePtr, writeInfoPtr, &pngInfo, options.grayScaleTolerance, &logger, + outError)) { + goto bail; + } + + result = true; +bail: + if (readPtr) { + png_destroy_read_struct(&readPtr, &infoPtr, nullptr); + } + + if (writePtr) { + png_destroy_write_struct(&writePtr, &writeInfoPtr); + } + return result; +} + +} // namespace aapt diff --git a/tools/aapt2/Png.h b/tools/aapt2/Png.h new file mode 100644 index 0000000..bc80754 --- /dev/null +++ b/tools/aapt2/Png.h @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#ifndef AAPT_PNG_H +#define AAPT_PNG_H + +#include "Source.h" + +#include <iostream> +#include <string> + +namespace aapt { + +struct Png { + struct Options { + int grayScaleTolerance = 0; + }; + + bool process(const Source& source, std::istream& input, std::ostream& output, + const Options& options, std::string* outError); +}; + +} // namespace aapt + +#endif // AAPT_PNG_H diff --git a/tools/aapt2/data/res/drawable/icon.png b/tools/aapt2/data/res/drawable/icon.png Binary files differnew file mode 100644 index 0000000..4bff9b9 --- /dev/null +++ b/tools/aapt2/data/res/drawable/icon.png diff --git a/tools/aapt2/data/res/drawable/test.9.png b/tools/aapt2/data/res/drawable/test.9.png Binary files differnew file mode 100644 index 0000000..33daa11 --- /dev/null +++ b/tools/aapt2/data/res/drawable/test.9.png |
