diff options
Diffstat (limited to 'core/java')
103 files changed, 6687 insertions, 1766 deletions
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 6ac2e80..7c40bb1 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -490,6 +490,7 @@ public class ActivityManager { /** * The id of the ActivityStack this Task was on most recently. + * @hide */ public int stackId; diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index cdec399..f10290d 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -105,6 +105,7 @@ import android.view.ContextThemeWrapper; import android.view.Display; import android.view.WindowManagerImpl; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.CaptioningManager; import android.view.inputmethod.InputMethodManager; import android.view.textservice.TextServicesManager; import android.accounts.AccountManager; @@ -307,6 +308,11 @@ class ContextImpl extends Context { return AccessibilityManager.getInstance(ctx); }}); + registerService(CAPTIONING_SERVICE, new ServiceFetcher() { + public Object getService(ContextImpl ctx) { + return new CaptioningManager(ctx); + }}); + registerService(ACCOUNT_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { IBinder b = ServiceManager.getService(ACCOUNT_SERVICE); diff --git a/core/java/android/bluetooth/BluetoothAssignedNumbers.java b/core/java/android/bluetooth/BluetoothAssignedNumbers.java index 580e9ff..124bdc1 100644 --- a/core/java/android/bluetooth/BluetoothAssignedNumbers.java +++ b/core/java/android/bluetooth/BluetoothAssignedNumbers.java @@ -513,6 +513,656 @@ public class BluetoothAssignedNumbers { public static final int RIVIERAWAVES = 0x0060; /* + * RDA Microelectronics. + */ + public static final int RDA_MICROELECTRONICS = 0x0061; + + /* + * Gibson Guitars. + */ + public static final int GIBSON_GUITARS = 0x0062; + + /* + * MiCommand Inc. + */ + public static final int MICOMMAND = 0x0063; + + /* + * Band XI International, LLC. + */ + public static final int BAND_XI_INTERNATIONAL = 0x0064; + + /* + * Hewlett-Packard Company. + */ + public static final int HEWLETT_PACKARD = 0x0065; + + /* + * 9Solutions Oy. + */ + public static final int NINE_SOLUTIONS = 0x0066; + + /* + * GN Netcom A/S. + */ + public static final int GN_NETCOM = 0x0067; + + /* + * General Motors. + */ + public static final int GENERAL_MOTORS = 0x0068; + + /* + * A&D Engineering, Inc. + */ + public static final int A_AND_D_ENGINEERING = 0x0069; + + /* + * MindTree Ltd. + */ + public static final int MINDTREE = 0x006A; + + /* + * Polar Electro OY. + */ + public static final int POLAR_ELECTRO = 0x006B; + + /* + * Beautiful Enterprise Co., Ltd. + */ + public static final int BEAUTIFUL_ENTERPRISE = 0x006C; + + /* + * BriarTek, Inc. + */ + public static final int BRIARTEK = 0x006D; + + /* + * Summit Data Communications, Inc. + */ + public static final int SUMMIT_DATA_COMMUNICATIONS = 0x006E; + + /* + * Sound ID. + */ + public static final int SOUND_ID = 0x006F; + + /* + * Monster, LLC. + */ + public static final int MONSTER = 0x0070; + + /* + * connectBlue AB. + */ + public static final int CONNECTBLUE = 0x0071; + + /* + * ShangHai Super Smart Electronics Co. Ltd. + */ + public static final int SHANGHAI_SUPER_SMART_ELECTRONICS = 0x0072; + + /* + * Group Sense Ltd. + */ + public static final int GROUP_SENSE = 0x0073; + + /* + * Zomm, LLC. + */ + public static final int ZOMM = 0x0074; + + /* + * Samsung Electronics Co. Ltd. + */ + public static final int SAMSUNG_ELECTRONICS = 0x0075; + + /* + * Creative Technology Ltd. + */ + public static final int CREATIVE_TECHNOLOGY = 0x0076; + + /* + * Laird Technologies. + */ + public static final int LAIRD_TECHNOLOGIES = 0x0077; + + /* + * Nike, Inc. + */ + public static final int NIKE = 0x0078; + + /* + * lesswire AG. + */ + public static final int LESSWIRE = 0x0079; + + /* + * MStar Semiconductor, Inc. + */ + public static final int MSTAR_SEMICONDUCTOR = 0x007A; + + /* + * Hanlynn Technologies. + */ + public static final int HANLYNN_TECHNOLOGIES = 0x007B; + + /* + * A & R Cambridge. + */ + public static final int A_AND_R_CAMBRIDGE = 0x007C; + + /* + * Seers Technology Co. Ltd. + */ + public static final int SEERS_TECHNOLOGY = 0x007D; + + /* + * Sports Tracking Technologies Ltd. + */ + public static final int SPORTS_TRACKING_TECHNOLOGIES = 0x007E; + + /* + * Autonet Mobile. + */ + public static final int AUTONET_MOBILE = 0x007F; + + /* + * DeLorme Publishing Company, Inc. + */ + public static final int DELORME_PUBLISHING_COMPANY = 0x0080; + + /* + * WuXi Vimicro. + */ + public static final int WUXI_VIMICRO = 0x0081; + + /* + * Sennheiser Communications A/S. + */ + public static final int SENNHEISER_COMMUNICATIONS = 0x0082; + + /* + * TimeKeeping Systems, Inc. + */ + public static final int TIMEKEEPING_SYSTEMS = 0x0083; + + /* + * Ludus Helsinki Ltd. + */ + public static final int LUDUS_HELSINKI = 0x0084; + + /* + * BlueRadios, Inc. + */ + public static final int BLUERADIOS = 0x0085; + + /* + * equinox AG. + */ + public static final int EQUINOX_AG = 0x0086; + + /* + * Garmin International, Inc. + */ + public static final int GARMIN_INTERNATIONAL = 0x0087; + + /* + * Ecotest. + */ + public static final int ECOTEST = 0x0088; + + /* + * GN ReSound A/S. + */ + public static final int GN_RESOUND = 0x0089; + + /* + * Jawbone. + */ + public static final int JAWBONE = 0x008A; + + /* + * Topcorn Positioning Systems, LLC. + */ + public static final int TOPCORN_POSITIONING_SYSTEMS = 0x008B; + + /* + * Qualcomm Labs, Inc. + */ + public static final int QUALCOMM_LABS = 0x008C; + + /* + * Zscan Software. + */ + public static final int ZSCAN_SOFTWARE = 0x008D; + + /* + * Quintic Corp. + */ + public static final int QUINTIC = 0x008E; + + /* + * Stollman E+V GmbH. + */ + public static final int STOLLMAN_E_PLUS_V = 0x008F; + + /* + * Funai Electric Co., Ltd. + */ + public static final int FUNAI_ELECTRIC = 0x0090; + + /* + * Advanced PANMOBIL Systems GmbH & Co. KG. + */ + public static final int ADVANCED_PANMOBIL_SYSTEMS = 0x0091; + + /* + * ThinkOptics, Inc. + */ + public static final int THINKOPTICS = 0x0092; + + /* + * Universal Electronics, Inc. + */ + public static final int UNIVERSAL_ELECTRONICS = 0x0093; + + /* + * Airoha Technology Corp. + */ + public static final int AIROHA_TECHNOLOGY = 0x0094; + + /* + * NEC Lighting, Ltd. + */ + public static final int NEC_LIGHTING = 0x0095; + + /* + * ODM Technology, Inc. + */ + public static final int ODM_TECHNOLOGY = 0x0096; + + /* + * Bluetrek Technologies Limited. + */ + public static final int BLUETREK_TECHNOLOGIES = 0x0097; + + /* + * zer01.tv GmbH. + */ + public static final int ZER01_TV = 0x0098; + + /* + * i.Tech Dynamic Global Distribution Ltd. + */ + public static final int I_TECH_DYNAMIC_GLOBAL_DISTRIBUTION = 0x0099; + + /* + * Alpwise. + */ + public static final int ALPWISE = 0x009A; + + /* + * Jiangsu Toppower Automotive Electronics Co., Ltd. + */ + public static final int JIANGSU_TOPPOWER_AUTOMOTIVE_ELECTRONICS = 0x009B; + + /* + * Colorfy, Inc. + */ + public static final int COLORFY = 0x009C; + + /* + * Geoforce Inc. + */ + public static final int GEOFORCE = 0x009D; + + /* + * Bose Corporation. + */ + public static final int BOSE = 0x009E; + + /* + * Suunto Oy. + */ + public static final int SUUNTO = 0x009F; + + /* + * Kensington Computer Products Group. + */ + public static final int KENSINGTON_COMPUTER_PRODUCTS_GROUP = 0x00A0; + + /* + * SR-Medizinelektronik. + */ + public static final int SR_MEDIZINELEKTRONIK = 0x00A1; + + /* + * Vertu Corporation Limited. + */ + public static final int VERTU = 0x00A2; + + /* + * Meta Watch Ltd. + */ + public static final int META_WATCH = 0x00A3; + + /* + * LINAK A/S. + */ + public static final int LINAK = 0x00A4; + + /* + * OTL Dynamics LLC. + */ + public static final int OTL_DYNAMICS = 0x00A5; + + /* + * Panda Ocean Inc. + */ + public static final int PANDA_OCEAN = 0x00A6; + + /* + * Visteon Corporation. + */ + public static final int VISTEON = 0x00A7; + + /* + * ARP Devices Limited. + */ + public static final int ARP_DEVICES = 0x00A8; + + /* + * Magneti Marelli S.p.A. + */ + public static final int MAGNETI_MARELLI = 0x00A9; + + /* + * CAEN RFID srl. + */ + public static final int CAEN_RFID = 0x00AA; + + /* + * Ingenieur-Systemgruppe Zahn GmbH. + */ + public static final int INGENIEUR_SYSTEMGRUPPE_ZAHN = 0x00AB; + + /* + * Green Throttle Games. + */ + public static final int GREEN_THROTTLE_GAMES = 0x00AC; + + /* + * Peter Systemtechnik GmbH. + */ + public static final int PETER_SYSTEMTECHNIK = 0x00AD; + + /* + * Omegawave Oy. + */ + public static final int OMEGAWAVE = 0x00AE; + + /* + * Cinetix. + */ + public static final int CINETIX = 0x00AF; + + /* + * Passif Semiconductor Corp. + */ + public static final int PASSIF_SEMICONDUCTOR = 0x00B0; + + /* + * Saris Cycling Group, Inc. + */ + public static final int SARIS_CYCLING_GROUP = 0x00B1; + + /* + * Bekey A/S. + */ + public static final int BEKEY = 0x00B2; + + /* + * Clarinox Technologies Pty. Ltd. + */ + public static final int CLARINOX_TECHNOLOGIES = 0x00B3; + + /* + * BDE Technology Co., Ltd. + */ + public static final int BDE_TECHNOLOGY = 0x00B4; + + /* + * Swirl Networks. + */ + public static final int SWIRL_NETWORKS = 0x00B5; + + /* + * Meso international. + */ + public static final int MESO_INTERNATIONAL = 0x00B6; + + /* + * TreLab Ltd. + */ + public static final int TRELAB = 0x00B7; + + /* + * Qualcomm Innovation Center, Inc. (QuIC). + */ + public static final int QUALCOMM_INNOVATION_CENTER = 0x00B8; + + /* + * Johnson Controls, Inc. + */ + public static final int JOHNSON_CONTROLS = 0x00B9; + + /* + * Starkey Laboratories Inc. + */ + public static final int STARKEY_LABORATORIES = 0x00BA; + + /* + * S-Power Electronics Limited. + */ + public static final int S_POWER_ELECTRONICS = 0x00BB; + + /* + * Ace Sensor Inc. + */ + public static final int ACE_SENSOR = 0x00BC; + + /* + * Aplix Corporation. + */ + public static final int APLIX = 0x00BD; + + /* + * AAMP of America. + */ + public static final int AAMP_OF_AMERICA = 0x00BE; + + /* + * Stalmart Technology Limited. + */ + public static final int STALMART_TECHNOLOGY = 0x00BF; + + /* + * AMICCOM Electronics Corporation. + */ + public static final int AMICCOM_ELECTRONICS = 0x00C0; + + /* + * Shenzhen Excelsecu Data Technology Co.,Ltd. + */ + public static final int SHENZHEN_EXCELSECU_DATA_TECHNOLOGY = 0x00C1; + + /* + * Geneq Inc. + */ + public static final int GENEQ = 0x00C2; + + /* + * adidas AG. + */ + public static final int ADIDAS = 0x00C3; + + /* + * LG Electronics. + */ + public static final int LG_ELECTRONICS = 0x00C4; + + /* + * Onset Computer Corporation. + */ + public static final int ONSET_COMPUTER = 0x00C5; + + /* + * Selfly BV. + */ + public static final int SELFLY = 0x00C6; + + /* + * Quuppa Oy. + */ + public static final int QUUPPA = 0x00C7; + + /* + * GeLo Inc. + */ + public static final int GELO = 0x00C8; + + /* + * Evluma. + */ + public static final int EVLUMA = 0x00C9; + + /* + * MC10. + */ + public static final int MC10 = 0x00CA; + + /* + * Binauric SE. + */ + public static final int BINAURIC = 0x00CB; + + /* + * Beats Electronics. + */ + public static final int BEATS_ELECTRONICS = 0x00CC; + + /* + * Microchip Technology Inc. + */ + public static final int MICROCHIP_TECHNOLOGY = 0x00CD; + + /* + * Elgato Systems GmbH. + */ + public static final int ELGATO_SYSTEMS = 0x00CE; + + /* + * ARCHOS SA. + */ + public static final int ARCHOS = 0x00CF; + + /* + * Dexcom, Inc. + */ + public static final int DEXCOM = 0x00D0; + + /* + * Polar Electro Europe B.V. + */ + public static final int POLAR_ELECTRO_EUROPE = 0x00D1; + + /* + * Dialog Semiconductor B.V. + */ + public static final int DIALOG_SEMICONDUCTOR = 0x00D2; + + /* + * Taixingbang Technology (HK) Co,. LTD. + */ + public static final int TAIXINGBANG_TECHNOLOGY = 0x00D3; + + /* + * Kawantech. + */ + public static final int KAWANTECH = 0x00D4; + + /* + * Austco Communication Systems. + */ + public static final int AUSTCO_COMMUNICATION_SYSTEMS = 0x00D5; + + /* + * Timex Group USA, Inc. + */ + public static final int TIMEX_GROUP_USA = 0x00D6; + + /* + * Qualcomm Technologies, Inc. + */ + public static final int QUALCOMM_TECHNOLOGIES = 0x00D7; + + /* + * Qualcomm Connected Experiences, Inc. + */ + public static final int QUALCOMM_CONNECTED_EXPERIENCES = 0x00D8; + + /* + * Voyetra Turtle Beach. + */ + public static final int VOYETRA_TURTLE_BEACH = 0x00D9; + + /* + * txtr GmbH. + */ + public static final int TXTR = 0x00DA; + + /* + * Biosentronics. + */ + public static final int BIOSENTRONICS = 0x00DB; + + /* + * Procter & Gamble. + */ + public static final int PROCTER_AND_GAMBLE = 0x00DC; + + /* + * Hosiden Corporation. + */ + public static final int HOSIDEN = 0x00DD; + + /* + * Muzik LLC. + */ + public static final int MUZIK = 0x00DE; + + /* + * Misfit Wearables Corp. + */ + public static final int MISFIT_WEARABLES = 0x00DF; + + /* + * Google. + */ + public static final int GOOGLE = 0x00E0; + + /* + * Danlers Ltd. + */ + public static final int DANLERS = 0x00E1; + + /* + * Semilink Inc. + */ + public static final int SEMILINK = 0x00E2; + + /* * You can't instantiate one of these. */ private BluetoothAssignedNumbers() { diff --git a/core/java/android/bluetooth/BluetoothGatt.java b/core/java/android/bluetooth/BluetoothGatt.java index b390aa1..a2bb78c 100644 --- a/core/java/android/bluetooth/BluetoothGatt.java +++ b/core/java/android/bluetooth/BluetoothGatt.java @@ -702,6 +702,10 @@ public final class BluetoothGatt implements BluetoothProfile { * @param start Start or stop advertising */ /*package*/ void listen(boolean start) { + if (mContext == null || !mContext.getResources(). + getBoolean(com.android.internal.R.bool.config_bluetooth_le_peripheral_mode_supported)) { + throw new UnsupportedOperationException("BluetoothGatt#listen is blocked"); + } if (DBG) Log.d(TAG, "listen() - start: " + start); if (mService == null || mClientIf == 0) return; @@ -728,6 +732,10 @@ public final class BluetoothGatt implements BluetoothProfile { /*package*/ void setAdvData(boolean advData, boolean includeName, boolean includeTxPower, Integer minInterval, Integer maxInterval, Integer appearance, Byte[] manufacturerData) { + if (mContext == null || !mContext.getResources(). + getBoolean(com.android.internal.R.bool.config_bluetooth_le_peripheral_mode_supported)) { + throw new UnsupportedOperationException("BluetoothGatt#setAdvData is blocked"); + } if (DBG) Log.d(TAG, "setAdvData()"); if (mService == null || mClientIf == 0) return; diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index 5a5764d..1962514 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -193,6 +193,11 @@ public final class BluetoothHeadset implements BluetoothProfile { "android.bluetooth.headset.intent.category.companyid"; /** + * A vendor-specific command for unsolicited result code. + */ + public static final String VENDOR_RESULT_CODE_COMMAND_ANDROID = "+ANDROID"; + + /** * Headset state when SCO audio is not connected. * This state can be one of * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of @@ -840,6 +845,46 @@ public final class BluetoothHeadset implements BluetoothProfile { } } + /** + * Sends a vendor-specific unsolicited result code to the headset. + * + * <p>The actual string to be sent is <code>command + ": " + arg</code>. + * For example, if {@code command} is {@link #VENDOR_RESULT_CODE_COMMAND_ANDROID} and {@code arg} + * is {@code "0"}, the string <code>"+ANDROID: 0"</code> will be sent. + * + * <p>Currently only {@link #VENDOR_RESULT_CODE_COMMAND_ANDROID} is allowed as {@code command}. + * + * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. + * + * @param device Bluetooth headset. + * @param command A vendor-specific command. + * @param arg The argument that will be attached to the command. + * @return {@code false} if there is no headset connected, or if the command is not an allowed + * vendor-specific unsolicited result code, or on error. {@code true} otherwise. + * @throws IllegalArgumentException if {@code command} is {@code null}. + */ + public boolean sendVendorSpecificResultCode(BluetoothDevice device, String command, + String arg) { + if (DBG) { + log("sendVendorSpecificResultCode()"); + } + if (command == null) { + throw new IllegalArgumentException("command is null"); + } + if (mService != null && isEnabled() && + isValidDevice(device)) { + try { + return mService.sendVendorSpecificResultCode(device, command, arg); + } catch (RemoteException e) { + Log.e(TAG, Log.getStackTraceString(new Throwable())); + } + } + if (mService == null) { + Log.w(TAG, "Proxy not attached to service"); + } + return false; + } + private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { if (DBG) Log.d(TAG, "Proxy object connected"); diff --git a/core/java/android/bluetooth/IBluetoothHeadset.aidl b/core/java/android/bluetooth/IBluetoothHeadset.aidl index 285eea7..524ca6f 100755 --- a/core/java/android/bluetooth/IBluetoothHeadset.aidl +++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl @@ -35,6 +35,9 @@ interface IBluetoothHeadset { boolean startVoiceRecognition(in BluetoothDevice device); boolean stopVoiceRecognition(in BluetoothDevice device); boolean isAudioConnected(in BluetoothDevice device); + boolean sendVendorSpecificResultCode(in BluetoothDevice device, + in String command, + in String arg); // APIs that can be made public in future int getBatteryUsageHint(in BluetoothDevice device); diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java index 024a521..4e8dd82 100644 --- a/core/java/android/content/ContentProviderClient.java +++ b/core/java/android/content/ContentProviderClient.java @@ -316,4 +316,11 @@ public class ContentProviderClient { public ContentProvider getLocalContentProvider() { return ContentProvider.coerceToLocalContentProvider(mContentProvider); } + + /** {@hide} */ + public static void closeQuietly(ContentProviderClient client) { + if (client != null) { + client.release(); + } + } } diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index a761a89..8a5a56c 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -141,7 +141,7 @@ public abstract class ContentResolver { public static final String SYNC_EXTRAS_PRIORITY = "sync_priority"; /** {@hide} Flag to allow sync to occur on metered network. */ - public static final String SYNC_EXTRAS_ALLOW_METERED = "allow_metered"; + public static final String SYNC_EXTRAS_DISALLOW_METERED = "disallow_metered"; /** * Set by the SyncManager to request that the SyncAdapter initialize itself for @@ -1669,7 +1669,7 @@ public abstract class ContentResolver { new SyncRequest.Builder() .setSyncAdapter(account, authority) .setExtras(extras) - .syncOnce(0, 0) // Immediate sync. + .syncOnce() .build(); requestSync(request); } @@ -1677,6 +1677,9 @@ public abstract class ContentResolver { /** * Register a sync with the SyncManager. These requests are built using the * {@link SyncRequest.Builder}. + * + * @param request The immutable SyncRequest object containing the sync parameters. Use + * {@link SyncRequest.Builder} to construct these. */ public static void requestSync(SyncRequest request) { try { @@ -1812,6 +1815,9 @@ public abstract class ContentResolver { * {@link #SYNC_EXTRAS_INITIALIZE}, {@link #SYNC_EXTRAS_FORCE}, * {@link #SYNC_EXTRAS_EXPEDITED}, {@link #SYNC_EXTRAS_MANUAL} set to true. * If any are supplied then an {@link IllegalArgumentException} will be thrown. + * <p>As of API level 19 this function introduces a default flexibility of ~4% (up to a maximum + * of one hour in the day) into the requested period. Use + * {@link SyncRequest.Builder#syncPeriodic(long, long)} to set this flexibility manually. * * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. @@ -1875,22 +1881,6 @@ public abstract class ContentResolver { } /** - * Remove the specified sync. This will remove any syncs that have been scheduled to run, but - * will not cancel any running syncs. - * <p>This method requires the caller to hold the permission</p> - * If the request is for a periodic sync this will cancel future occurrences of the sync. - * - * It is possible to cancel a sync using a SyncRequest object that is different from the object - * with which you requested the sync. Do so by building a SyncRequest with exactly the same - * service/adapter, frequency, <b>and</b> extras bundle. - * - * @param request SyncRequest object containing information about sync to cancel. - */ - public static void cancelSync(SyncRequest request) { - // TODO: Finish this implementation. - } - - /** * Get the list of information about the periodic syncs for the given account and authority. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#READ_SYNC_SETTINGS}. diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index cd1f87b..2ff9182 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2011,6 +2011,17 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a + * {@link android.view.accessibility.CaptioningManager} for obtaining + * captioning properties and listening for changes in captioning + * preferences. + * + * @see #getSystemService + * @see android.view.accessibility.CaptioningManager + */ + public static final String CAPTIONING_SERVICE = "captioning"; + + /** + * Use with {@link #getSystemService} to retrieve a * {@link android.app.NotificationManager} for controlling keyguard. * * @see #getSystemService diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index c99f09c..dfc0412 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -2687,10 +2687,6 @@ public class Intent implements Parcelable, Cloneable { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT"; - /** {@hide} */ - @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) - public static final String ACTION_MANAGE_DOCUMENT = "android.intent.action.MANAGE_DOCUMENT"; - // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent categories (see addCategory()). @@ -3304,8 +3300,10 @@ public class Intent implements Parcelable, Cloneable { /** * Optional extra for {@link #ACTION_SHUTDOWN} that allows the sender to qualify that * this shutdown is only for the user space of the system, not a complete shutdown. - * Hardware should not be shut down when this is true. The default if not supplied - * is false. + * When this is true, hardware devices can use this information to determine that + * they shouldn't do a complete shutdown of their device since this is not a + * complete shutdown down to the kernel, but only user space restarting. + * The default if not supplied is false. */ public static final String EXTRA_SHUTDOWN_USERSPACE_ONLY = "android.intent.extra.SHUTDOWN_USERSPACE_ONLY"; diff --git a/core/java/android/content/PeriodicSync.java b/core/java/android/content/PeriodicSync.java index 6aca151..b586eec 100644 --- a/core/java/android/content/PeriodicSync.java +++ b/core/java/android/content/PeriodicSync.java @@ -29,17 +29,13 @@ public class PeriodicSync implements Parcelable { public final Account account; /** The authority of the sync. Can be null. */ public final String authority; - /** The service for syncing, if this is an anonymous sync. Can be null.*/ - public final ComponentName service; /** Any extras that parameters that are to be passed to the sync adapter. */ public final Bundle extras; /** How frequently the sync should be scheduled, in seconds. Kept around for API purposes. */ public final long period; - /** Whether this periodic sync uses a service. */ - public final boolean isService; /** - * How much flexibility can be taken in scheduling the sync, in seconds. * {@hide} + * How much flexibility can be taken in scheduling the sync, in seconds. */ public final long flexTime; @@ -52,76 +48,44 @@ public class PeriodicSync implements Parcelable { public PeriodicSync(Account account, String authority, Bundle extras, long periodInSeconds) { this.account = account; this.authority = authority; - this.service = null; - this.isService = false; if (extras == null) { this.extras = new Bundle(); } else { this.extras = new Bundle(extras); } this.period = periodInSeconds; - // Old API uses default flex time. No-one should be using this ctor anyway. + // Initialise to a sane value. this.flexTime = 0L; } - // TODO: Add copy ctor from SyncRequest? - /** - * Create a copy of a periodic sync. * {@hide} + * Create a copy of a periodic sync. */ public PeriodicSync(PeriodicSync other) { this.account = other.account; this.authority = other.authority; - this.service = other.service; - this.isService = other.isService; this.extras = new Bundle(other.extras); this.period = other.period; this.flexTime = other.flexTime; } /** - * A PeriodicSync for a sync with a specified provider. * {@hide} + * A PeriodicSync for a sync with a specified provider. */ public PeriodicSync(Account account, String authority, Bundle extras, long period, long flexTime) { this.account = account; this.authority = authority; - this.service = null; - this.isService = false; - this.extras = new Bundle(extras); - this.period = period; - this.flexTime = flexTime; - } - - /** - * A PeriodicSync for a sync with a specified SyncService. - * {@hide} - */ - public PeriodicSync(ComponentName service, Bundle extras, - long period, - long flexTime) { - this.account = null; - this.authority = null; - this.service = service; - this.isService = true; this.extras = new Bundle(extras); this.period = period; this.flexTime = flexTime; } private PeriodicSync(Parcel in) { - this.isService = (in.readInt() != 0); - if (this.isService) { - this.service = in.readParcelable(null); - this.account = null; - this.authority = null; - } else { - this.account = in.readParcelable(null); - this.authority = in.readString(); - this.service = null; - } + this.account = in.readParcelable(null); + this.authority = in.readString(); this.extras = in.readBundle(); this.period = in.readLong(); this.flexTime = in.readLong(); @@ -134,13 +98,8 @@ public class PeriodicSync implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(isService ? 1 : 0); - if (account == null && authority == null) { - dest.writeParcelable(service, flags); - } else { - dest.writeParcelable(account, flags); - dest.writeString(authority); - } + dest.writeParcelable(account, flags); + dest.writeString(authority); dest.writeBundle(extras); dest.writeLong(period); dest.writeLong(flexTime); @@ -167,17 +126,8 @@ public class PeriodicSync implements Parcelable { return false; } final PeriodicSync other = (PeriodicSync) o; - if (this.isService != other.isService) { - return false; - } - boolean equal = false; - if (this.isService) { - equal = service.equals(other.service); - } else { - equal = account.equals(other.account) - && authority.equals(other.authority); - } - return equal + return account.equals(other.account) + && authority.equals(other.authority) && period == other.period && syncExtrasEquals(extras, other.extras); } @@ -208,7 +158,6 @@ public class PeriodicSync implements Parcelable { public String toString() { return "account: " + account + ", authority: " + authority + - ", service: " + service + ". period: " + period + "s " + ", flex: " + flexTime; } diff --git a/core/java/android/content/SyncRequest.java b/core/java/android/content/SyncRequest.java index 4474c70..d4e0c2a 100644 --- a/core/java/android/content/SyncRequest.java +++ b/core/java/android/content/SyncRequest.java @@ -20,20 +20,19 @@ import android.accounts.Account; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.util.Pair; public class SyncRequest implements Parcelable { private static final String TAG = "SyncRequest"; - /** Account to pass to the sync adapter. Can be null. */ + /** Account to pass to the sync adapter. May be null. */ private final Account mAccountToSync; /** Authority string that corresponds to a ContentProvider. */ private final String mAuthority; - /** {@link SyncService} identifier. */ + /** Sync service identifier. May be null.*/ private final ComponentName mComponentInfo; /** Bundle containing user info as well as sync settings. */ private final Bundle mExtras; - /** Allow this sync request on metered networks. */ - private final boolean mAllowMetered; + /** Disallow this sync request on metered networks. */ + private final boolean mDisallowMetered; /** * Anticipated upload size in bytes. * TODO: Not yet used - we put this information into the bundle for simplicity. @@ -70,14 +69,18 @@ public class SyncRequest implements Parcelable { return mIsPeriodic; } + /** + * {@hide} + * @return whether this is an expedited sync. + */ public boolean isExpedited() { return mIsExpedited; } /** * {@hide} - * @return true if this sync uses an account/authority pair, or false if - * this is an anonymous sync bound to an @link AnonymousSyncService. + * @return true if this sync uses an account/authority pair, or false if this sync is bound to + * a Sync Service. */ public boolean hasAuthority() { return mIsAuthority; @@ -85,31 +88,30 @@ public class SyncRequest implements Parcelable { /** * {@hide} - * Throws a runtime IllegalArgumentException if this function is called for an - * anonymous sync. - * - * @return (Account, Provider) for this SyncRequest. + * @return account object for this sync. + * @throws IllegalArgumentException if this function is called for a request that does not + * specify an account/provider authority. */ - public Pair<Account, String> getProviderInfo() { + public Account getAccount() { if (!hasAuthority()) { - throw new IllegalArgumentException("Cannot getProviderInfo() for an anonymous sync."); + throw new IllegalArgumentException("Cannot getAccount() for a sync that does not" + + "specify an authority."); } - return Pair.create(mAccountToSync, mAuthority); + return mAccountToSync; } /** * {@hide} - * Throws a runtime IllegalArgumentException if this function is called for a - * SyncRequest that is bound to an account/provider. - * - * @return ComponentName for the service that this sync will bind to. + * @return provider for this sync. + * @throws IllegalArgumentException if this function is called for a request that does not + * specify an account/provider authority. */ - public ComponentName getService() { - if (hasAuthority()) { - throw new IllegalArgumentException( - "Cannot getAnonymousService() for a sync that has specified a provider."); + public String getProvider() { + if (!hasAuthority()) { + throw new IllegalArgumentException("Cannot getProvider() for a sync that does not" + + "specify a provider."); } - return mComponentInfo; + return mAuthority; } /** @@ -127,6 +129,7 @@ public class SyncRequest implements Parcelable { public long getSyncFlexTime() { return mSyncFlexTimeSecs; } + /** * {@hide} * @return the last point in time at which this sync must scheduled. @@ -159,7 +162,7 @@ public class SyncRequest implements Parcelable { parcel.writeLong(mSyncFlexTimeSecs); parcel.writeLong(mSyncRunTimeSecs); parcel.writeInt((mIsPeriodic ? 1 : 0)); - parcel.writeInt((mAllowMetered ? 1 : 0)); + parcel.writeInt((mDisallowMetered ? 1 : 0)); parcel.writeLong(mTxBytes); parcel.writeLong(mRxBytes); parcel.writeInt((mIsAuthority ? 1 : 0)); @@ -177,7 +180,7 @@ public class SyncRequest implements Parcelable { mSyncFlexTimeSecs = in.readLong(); mSyncRunTimeSecs = in.readLong(); mIsPeriodic = (in.readInt() != 0); - mAllowMetered = (in.readInt() != 0); + mDisallowMetered = (in.readInt() != 0); mTxBytes = in.readLong(); mRxBytes = in.readLong(); mIsAuthority = (in.readInt() != 0); @@ -207,13 +210,13 @@ public class SyncRequest implements Parcelable { // For now we merge the sync config extras & the custom extras into one bundle. // TODO: pass the configuration extras through separately. mExtras.putAll(b.mSyncConfigExtras); - mAllowMetered = b.mAllowMetered; + mDisallowMetered = b.mDisallowMetered; mTxBytes = b.mTxBytes; mRxBytes = b.mRxBytes; } /** - * Builder class for a @link SyncRequest. As you build your SyncRequest this class will also + * Builder class for a {@link SyncRequest}. As you build your SyncRequest this class will also * perform validation. */ public static class Builder { @@ -229,12 +232,9 @@ public class SyncRequest implements Parcelable { private static final int SYNC_TARGET_SERVICE = 1; /** Specify that this is a sync with a provider. */ private static final int SYNC_TARGET_ADAPTER = 2; - /** - * Earliest point of displacement into the future at which this sync can - * occur. - */ + /** Earliest point of displacement into the future at which this sync can occur. */ private long mSyncFlexTimeSecs; - /** Displacement into the future at which this sync must occur. */ + /** Latest point of displacement into the future at which this sync must occur. */ private long mSyncRunTimeSecs; /** * Sync configuration information - custom user data explicitly provided by the developer. @@ -253,7 +253,7 @@ public class SyncRequest implements Parcelable { /** Expected download transfer in bytes. */ private long mRxBytes = -1L; /** Whether or not this sync can occur on metered networks. Default false. */ - private boolean mAllowMetered; + private boolean mDisallowMetered; /** Priority of this sync relative to others from calling app [-2, 2]. Default 0. */ private int mPriority = 0; /** @@ -283,9 +283,8 @@ public class SyncRequest implements Parcelable { private boolean mExpedited; /** - * The {@link SyncService} component that - * contains the sync logic if this is a provider-less sync, otherwise - * null. + * The sync component that contains the sync logic if this is a provider-less sync, + * otherwise null. */ private ComponentName mComponentName; /** @@ -303,46 +302,28 @@ public class SyncRequest implements Parcelable { } /** - * Developer can define timing constraints for this one-shot request. - * These values are elapsed real-time. - * - * @param whenSeconds The time in seconds at which you want this - * sync to occur. - * @param beforeSeconds The amount of time in advance of whenSeconds that this - * sync may be permitted to occur. This is rounded up to a minimum of 5 - * seconds, for any sync for which whenSeconds > 5. + * Request that a sync occur immediately. * * Example * <pre> - * Perform an immediate sync. - * SyncRequest.Builder builder = (new SyncRequest.Builder()).syncOnce(0, 0); - * That is, a sync 0 seconds from now with 0 seconds of flex. - * - * Perform a sync in exactly 5 minutes. - * SyncRequest.Builder builder = - * new SyncRequest.Builder().syncOnce(5 * MIN_IN_SECS, 0); - * - * Perform a sync in 5 minutes, with one minute of leeway (between 4 and 5 minutes from - * now). - * SyncRequest.Builder builder = - * new SyncRequest.Builder().syncOnce(5 * MIN_IN_SECS, 1 * MIN_IN_SECS); + * SyncRequest.Builder builder = (new SyncRequest.Builder()).syncOnce(); * </pre> */ - public Builder syncOnce(long whenSeconds, long beforeSeconds) { + public Builder syncOnce() { if (mSyncType != SYNC_TYPE_UNKNOWN) { throw new IllegalArgumentException("Sync type has already been defined."); } mSyncType = SYNC_TYPE_ONCE; - setupInterval(whenSeconds, beforeSeconds); + setupInterval(0, 0); return this; } /** * Build a periodic sync. Either this or syncOnce() <b>must</b> be called for this builder. - * Syncs are identified by target {@link SyncService}/{@link android.provider} and by the - * contents of the extras bundle. - * You cannot reuse the same builder for one-time syncs after having specified a periodic - * sync (by calling this function). If you do, an <code>IllegalArgumentException</code> + * Syncs are identified by target {@link android.provider}/{@link android.accounts.Account} + * and by the contents of the extras bundle. + * You cannot reuse the same builder for one-time syncs (by calling this function) after + * having specified a periodic sync. If you do, an <code>IllegalArgumentException</code> * will be thrown. * * Example usage. @@ -394,6 +375,7 @@ public class SyncRequest implements Parcelable { } /** + * {@hide} * Developer can provide insight into their payload size; optional. -1 specifies unknown, * so that you are not restricted to defining both fields. * @@ -407,21 +389,20 @@ public class SyncRequest implements Parcelable { } /** - * @param allow false to allow this transfer on metered networks. Default true. + * @see android.net.ConnectivityManager#isActiveNetworkMetered() + * @param disallow true to enforce that this transfer not occur on metered networks. + * Default false. */ - public Builder setAllowMetered(boolean allow) { - mAllowMetered = true; + public Builder setDisallowMetered(boolean disallow) { + mDisallowMetered = disallow; return this; } /** - * Specify an authority and account for this transfer. Cannot be used with - * {@link #setSyncAdapter(ComponentName cname)}. + * Specify an authority and account for this transfer. * - * @param authority - * @param account Account to sync. Can be null unless this is a periodic - * sync, for which verification by the ContentResolver will - * fail. If a sync is performed without an account, the + * @param authority String identifying which content provider to sync. + * @param account Account to sync. Can be null unless this is a periodic sync. */ public Builder setSyncAdapter(Account account, String authority) { if (mSyncTarget != SYNC_TARGET_UNKNOWN) { @@ -435,26 +416,10 @@ public class SyncRequest implements Parcelable { } /** - * Specify the {@link SyncService} component for this sync. This is not validated until - * sync time so providing an incorrect component name here will not fail. Cannot be used - * with {@link #setSyncAdapter(Account account, String authority)}. - * - * @param cname ComponentName to identify your Anonymous service - */ - public Builder setSyncAdapter(ComponentName cname) { - if (mSyncTarget != SYNC_TARGET_UNKNOWN) { - throw new IllegalArgumentException("Sync target has already been defined."); - } - mSyncTarget = SYNC_TARGET_SERVICE; - mComponentName = cname; - mAccount = null; - mAuthority = null; - return this; - } - - /** - * Developer-provided extras handed back when sync actually occurs. This bundle is copied - * into the SyncRequest returned by {@link #build()}. + * Optional developer-provided extras handed back in + * {@link AbstractThreadedSyncAdapter#onPerformSync(Account, Bundle, String, + * ContentProviderClient, SyncResult)} occurs. This bundle is copied into the SyncRequest + * returned by {@link #build()}. * * Example: * <pre> @@ -468,7 +433,7 @@ public class SyncRequest implements Parcelable { * Bundle extras = new Bundle(); * extras.setString("data", syncData); * builder.setExtras(extras); - * ContentResolver.sync(builder.build()); // Each sync() request creates a unique sync. + * ContentResolver.sync(builder.build()); // Each sync() request is for a unique sync. * } * </pre> * Only values of the following types may be used in the extras bundle: @@ -509,7 +474,8 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_IGNORE_SETTINGS}. * - * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in + * A sync can specify that system sync settings be ignored (user has turned sync off). Not + * valid for periodic sync and will throw an <code>IllegalArgumentException</code> in * {@link #build()}. * * @param ignoreSettings true to ignore the sync automatically settings. Default false. @@ -522,13 +488,13 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_IGNORE_BACKOFF}. * - * Ignoring back-off will force the sync scheduling process to ignore any back-off that was - * the result of a failed sync, as well as to invalidate any {@link SyncResult#delayUntil} - * value that may have been set by the adapter. Successive failures will not honor this - * flag. Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> - * in {@link #build()}. + * Force the sync scheduling process to ignore any back-off that was the result of a failed + * sync, as well as to invalidate any {@link SyncResult#delayUntil} value that may have + * been set by the adapter. Successive failures will not honor this flag. Not valid for + * periodic sync and will throw an <code>IllegalArgumentException</code> in + * {@link #build()}. * - * @param ignoreBackoff ignore back off settings. Default false. + * @param ignoreBackoff ignore back-off settings. Default false. */ public Builder setIgnoreBackoff(boolean ignoreBackoff) { mIgnoreBackoff = ignoreBackoff; @@ -538,8 +504,9 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_MANUAL}. * - * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in - * {@link #build()}. + * A manual sync is functionally equivalent to calling {@link #setIgnoreBackoff(boolean)} + * and {@link #setIgnoreSettings(boolean)}. Not valid for periodic sync and will throw an + * <code>IllegalArgumentException</code> in {@link #build()}. * * @param isManual User-initiated sync or not. Default false. */ @@ -549,7 +516,7 @@ public class SyncRequest implements Parcelable { } /** - * An expedited sync runs immediately and can preempt other non-expedited running syncs. + * An expedited sync runs immediately and will preempt another non-expedited running sync. * * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in * {@link #build()}. @@ -562,6 +529,7 @@ public class SyncRequest implements Parcelable { } /** + * {@hide} * @param priority the priority of this request among all requests from the calling app. * Range of [-2,2] similar to how this is done with notifications. */ @@ -581,18 +549,18 @@ public class SyncRequest implements Parcelable { * builder. */ public SyncRequest build() { - // Validate the extras bundle - ContentResolver.validateSyncExtrasBundle(mCustomExtras); if (mCustomExtras == null) { mCustomExtras = new Bundle(); } + // Validate the extras bundle + ContentResolver.validateSyncExtrasBundle(mCustomExtras); // Combine builder extra flags into the config bundle. mSyncConfigExtras = new Bundle(); if (mIgnoreBackoff) { mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true); } - if (mAllowMetered) { - mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_ALLOW_METERED, true); + if (mDisallowMetered) { + mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_DISALLOW_METERED, true); } if (mIgnoreSettings) { mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); @@ -613,13 +581,22 @@ public class SyncRequest implements Parcelable { // If this is a periodic sync ensure than invalid extras were not set. validatePeriodicExtras(mCustomExtras); validatePeriodicExtras(mSyncConfigExtras); + // Verify that account and provider are not null. + if (mAccount == null) { + throw new IllegalArgumentException("Account must not be null for periodic" + + " sync."); + } + if (mAuthority == null) { + throw new IllegalArgumentException("Authority must not be null for periodic" + + " sync."); + } } else if (mSyncType == SYNC_TYPE_UNKNOWN) { throw new IllegalArgumentException("Must call either syncOnce() or syncPeriodic()"); } // Ensure that a target for the sync has been set. if (mSyncTarget == SYNC_TARGET_UNKNOWN) { - throw new IllegalArgumentException("Must specify an adapter with one of" - + "setSyncAdapter(ComponentName) or setSyncAdapter(Account, String"); + throw new IllegalArgumentException("Must specify an adapter with " + + "setSyncAdapter(Account, String"); } return new SyncRequest(this); } diff --git a/core/java/android/content/SyncService.java b/core/java/android/content/SyncService.java deleted file mode 100644 index 100fd40..0000000 --- a/core/java/android/content/SyncService.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import android.app.Service; -import android.os.Bundle; -import android.os.IBinder; -import android.os.Process; -import android.os.Trace; - -import com.android.internal.annotations.GuardedBy; - -import java.util.HashMap; - -/** - * Simplified @link android.content.AbstractThreadedSyncAdapter. Folds that - * behaviour into a service to which the system can bind when requesting an - * anonymous (providerless/accountless) sync. - * <p> - * In order to perform an anonymous sync operation you must extend this service, - * implementing the abstract methods. This service must then be declared in the - * application's manifest as usual. You can use this service for other work, however you - * <b> must not </b> override the onBind() method unless you know what you're doing, - * which limits the usefulness of this service for other work. - * - * <pre> - * <service ndroid:name=".MyAnonymousSyncService" android:permission="android.permission.SYNC" /> - * </pre> - * Like @link android.content.AbstractThreadedSyncAdapter this service supports - * multiple syncs at the same time. Each incoming startSync() with a unique tag - * will spawn a thread to do the work of that sync. If startSync() is called - * with a tag that already exists, a SyncResult.ALREADY_IN_PROGRESS is returned. - * Remember that your service will spawn multiple threads if you schedule multiple syncs - * at once, so if you mutate local objects you must ensure synchronization. - */ -public abstract class SyncService extends Service { - - /** SyncAdapter Instantiation that any anonymous syncs call. */ - private final AnonymousSyncAdapterImpl mSyncAdapter = new AnonymousSyncAdapterImpl(); - - /** Keep track of on-going syncs, keyed by tag. */ - @GuardedBy("mLock") - private final HashMap<Bundle, AnonymousSyncThread> - mSyncThreads = new HashMap<Bundle, AnonymousSyncThread>(); - /** Lock object for accessing the SyncThreads HashMap. */ - private final Object mSyncThreadLock = new Object(); - - @Override - public IBinder onBind(Intent intent) { - return mSyncAdapter.asBinder(); - } - - /** {@hide} */ - private class AnonymousSyncAdapterImpl extends IAnonymousSyncAdapter.Stub { - - @Override - public void startSync(ISyncContext syncContext, Bundle extras) { - // Wrap the provided Sync Context because it may go away by the time - // we call it. - final SyncContext syncContextClient = new SyncContext(syncContext); - boolean alreadyInProgress = false; - synchronized (mSyncThreadLock) { - if (mSyncThreads.containsKey(extras)) { - // Don't want to call back to SyncManager while still - // holding lock. - alreadyInProgress = true; - } else { - AnonymousSyncThread syncThread = new AnonymousSyncThread( - syncContextClient, extras); - mSyncThreads.put(extras, syncThread); - syncThread.start(); - } - } - if (alreadyInProgress) { - syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); - } - } - - /** - * Used by the SM to cancel a specific sync using the {@link - * com.android.server.content.SyncManager.ActiveSyncContext} as a handle. - */ - @Override - public void cancelSync(ISyncContext syncContext) { - AnonymousSyncThread runningSync = null; - synchronized (mSyncThreadLock) { - for (AnonymousSyncThread thread : mSyncThreads.values()) { - if (thread.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { - runningSync = thread; - break; - } - } - } - if (runningSync != null) { - runningSync.interrupt(); - } - } - } - - /** - * {@hide} - * Similar to {@link android.content.AbstractThreadedSyncAdapter.SyncThread}. However while - * the ATSA considers an already in-progress sync to be if the account provided is currently - * syncing, this anonymous sync has no notion of account and therefore considers a sync unique - * if the provided bundle is different. - */ - private class AnonymousSyncThread extends Thread { - private final SyncContext mSyncContext; - private final Bundle mExtras; - - public AnonymousSyncThread(SyncContext syncContext, Bundle extras) { - mSyncContext = syncContext; - mExtras = extras; - } - - @Override - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - - Trace.traceBegin(Trace.TRACE_TAG_SYNC_MANAGER, getApplication().getPackageName()); - - SyncResult syncResult = new SyncResult(); - try { - if (isCancelled()) { - return; - } - // Run the sync based off of the provided code. - SyncService.this.onPerformSync(mExtras, syncResult); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_SYNC_MANAGER); - if (!isCancelled()) { - mSyncContext.onFinished(syncResult); - } - // Synchronize so that the assignment will be seen by other - // threads - // that also synchronize accesses to mSyncThreads. - synchronized (mSyncThreadLock) { - mSyncThreads.remove(mExtras); - } - } - } - - private boolean isCancelled() { - return Thread.currentThread().isInterrupted(); - } - } - - /** - * Initiate an anonymous sync using this service. SyncAdapter-specific - * parameters may be specified in extras, which is guaranteed to not be - * null. - */ - public abstract void onPerformSync(Bundle extras, SyncResult syncResult); - -} diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index eba69b6..2b0c896 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -216,6 +216,12 @@ interface IPackageManager { void resetPreferredActivities(int userId); + ResolveInfo getLastChosenActivity(in Intent intent, + String resolvedType, int flags); + + void setLastChosenActivity(in Intent intent, String resolvedType, int flags, + in IntentFilter filter, int match, in ComponentName activity); + void addPreferredActivity(in IntentFilter filter, int match, in ComponentName[] set, in ComponentName activity, int userId); @@ -226,7 +232,7 @@ interface IPackageManager { int getPreferredActivities(out List<IntentFilter> outFilters, out List<ComponentName> outActivities, String packageName); - + /** * As per {@link android.content.pm.PackageManager#setComponentEnabledSetting}. */ diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 81f860e..d58b14c 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -957,12 +957,24 @@ public abstract class PackageManager { * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device supports host- * based NFC card emulation. + * + * TODO remove when depending apps have moved to new constant. + * @hide + * @deprecated */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_NFC_HCE = "android.hardware.nfc.hce"; /** * Feature for {@link #getSystemAvailableFeatures} and + * {@link #hasSystemFeature}: The device supports host- + * based NFC card emulation. + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_NFC_HOST_CARD_EMULATION = "android.hardware.nfc.hce"; + + /** + * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device includes an accelerometer. */ @SdkConstant(SdkConstantType.FEATURE) diff --git a/core/java/android/ddm/DdmHandleProfiling.java b/core/java/android/ddm/DdmHandleProfiling.java index e0db5e7..ec08393 100644 --- a/core/java/android/ddm/DdmHandleProfiling.java +++ b/core/java/android/ddm/DdmHandleProfiling.java @@ -34,6 +34,8 @@ public class DdmHandleProfiling extends ChunkHandler { public static final int CHUNK_MPSS = type("MPSS"); public static final int CHUNK_MPSE = type("MPSE"); public static final int CHUNK_MPRQ = type("MPRQ"); + public static final int CHUNK_SPSS = type("SPSS"); + public static final int CHUNK_SPSE = type("SPSE"); private static DdmHandleProfiling mInstance = new DdmHandleProfiling(); @@ -50,6 +52,8 @@ public class DdmHandleProfiling extends ChunkHandler { DdmServer.registerHandler(CHUNK_MPSS, mInstance); DdmServer.registerHandler(CHUNK_MPSE, mInstance); DdmServer.registerHandler(CHUNK_MPRQ, mInstance); + DdmServer.registerHandler(CHUNK_SPSS, mInstance); + DdmServer.registerHandler(CHUNK_SPSE, mInstance); } /** @@ -82,6 +86,10 @@ public class DdmHandleProfiling extends ChunkHandler { return handleMPSE(request); } else if (type == CHUNK_MPRQ) { return handleMPRQ(request); + } else if (type == CHUNK_SPSS) { + return handleSPSS(request); + } else if (type == CHUNK_SPSE) { + return handleSPSE(request); } else { throw new RuntimeException("Unknown packet " + ChunkHandler.name(type)); @@ -144,7 +152,7 @@ public class DdmHandleProfiling extends ChunkHandler { } try { - Debug.startMethodTracingDdms(bufferSize, flags); + Debug.startMethodTracingDdms(bufferSize, flags, false, 0); return null; // empty response } catch (RuntimeException re) { return createFailChunk(1, re.getMessage()); @@ -178,11 +186,53 @@ public class DdmHandleProfiling extends ChunkHandler { * Handle a "Method PRofiling Query" request. */ private Chunk handleMPRQ(Chunk request) { - int result = Debug.isMethodTracingActive() ? 1 : 0; + int result = Debug.getMethodTracingMode(); /* create a non-empty reply so the handler fires on completion */ byte[] reply = { (byte) result }; return new Chunk(CHUNK_MPRQ, reply, 0, reply.length); } + + /* + * Handle a "Sample Profiling w/Streaming Start" request. + */ + private Chunk handleSPSS(Chunk request) { + ByteBuffer in = wrapChunk(request); + + int bufferSize = in.getInt(); + int flags = in.getInt(); + int interval = in.getInt(); + if (false) { + Log.v("ddm-heap", "Sample prof stream start: size=" + bufferSize + + ", flags=" + flags + ", interval=" + interval); + } + + try { + Debug.startMethodTracingDdms(bufferSize, flags, true, interval); + return null; // empty response + } catch (RuntimeException re) { + return createFailChunk(1, re.getMessage()); + } + } + + /* + * Handle a "Sample Profiling w/Streaming End" request. + */ + private Chunk handleSPSE(Chunk request) { + if (false) { + Log.v("ddm-heap", "Sample prof stream end"); + } + + try { + Debug.stopMethodTracing(); + } catch (RuntimeException re) { + Log.w("ddm-heap", "Sample prof stream end failed: " + + re.getMessage()); + return createFailChunk(1, re.getMessage()); + } + + /* VM sent the (perhaps very large) response directly */ + return null; + } } diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 848d7bc..a4a56d7 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -303,7 +303,8 @@ public interface CameraDevice extends AutoCloseable { * preview or other continuous stream of frames, without having to submit * requests through {@link #capture} at video rates.</p> * - * <p>To stop the repeating capture, call {@link #stopRepeating}</p> + * <p>To stop the repeating capture, call {@link #stopRepeating}. Calling + * {@link #flush} will also clear the request.</p> * * <p>Calling repeat will replace a burst set up by {@link * #setRepeatingBurst}, although any in-progress burst will be @@ -323,6 +324,8 @@ public interface CameraDevice extends AutoCloseable { * @see #capture * @see #captureBurst * @see #setRepeatingBurst + * @see #stopRepeating + * @see #flush */ public void setRepeatingRequest(CaptureRequest request, CaptureListener listener) throws CameraAccessException; @@ -348,7 +351,8 @@ public interface CameraDevice extends AutoCloseable { * requests through {@link #capture} at video rates.</p> * * <p>To stop the repeating capture, call {@link #stopRepeating}. Any - * ongoing burst will still be completed, however.</p> + * ongoing burst will still be completed, however. Calling + * {@link #flush} will also clear the request.</p> * * <p>Calling repeatBurst will replace a repeating request set up by * {@link #setRepeatingRequest}, although any in-progress capture will be completed @@ -367,6 +371,8 @@ public interface CameraDevice extends AutoCloseable { * @see #capture * @see #captureBurst * @see #setRepeatingRequest + * @see #stopRepeating + * @see #flush */ public void setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener) throws CameraAccessException; @@ -435,6 +441,38 @@ public interface CameraDevice extends AutoCloseable { public void setErrorListener(ErrorListener listener); /** + * Flush all captures currently pending and in-progress as fast as + * possible. + * + * <p>The camera device will discard all of its current work as fast as + * possible. Some in-flight captures may complete successfully and call + * {@link CaptureListener#onCaptureComplete}, while others will trigger + * their {@link CaptureListener#onCaptureFailed} callbacks. If a repeating + * request or a repeating burst is set, it will be cleared by the flush.</p> + * + * <p>This method is the fastest way to idle the camera device for + * reconfiguration with {@link #configureOutputs}, at the cost of discarding + * in-progress work. Once the flush is complete, the idle callback will be + * called.</p> + * + * <p>Flushing will introduce at least a brief pause in the stream of data + * from the camera device, since once the flush is complete, the first new + * request has to make it through the entire camera pipeline before new + * output buffers are produced.</p> + * + * <p>This means that using {@code flush()} to simply remove pending + * requests is not recommended; it's best used for quickly switching output + * configurations, or for cancelling long in-progress requests (such as a + * multi-second capture).</p> + * + * @throws CameraAccessException if the camera device is no longer connected + * @see #setRepeatingRequest + * @see #setRepeatingBurst + * @see #configureOutputs + */ + public void flush() throws CameraAccessException; + + /** * Close the connection to this camera device. After this call, all calls to * the camera device interface will throw a {@link IllegalStateException}, * except for calls to close(). diff --git a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl index 5a9b72f..b1724de 100644 --- a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl +++ b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl @@ -45,4 +45,6 @@ interface ICameraDeviceUser int getCameraInfo(out CameraMetadata info); int waitUntilIdle(); + + int flush(); } diff --git a/core/java/android/hardware/camera2/impl/CameraDevice.java b/core/java/android/hardware/camera2/impl/CameraDevice.java index e7495d3..64e4dc9 100644 --- a/core/java/android/hardware/camera2/impl/CameraDevice.java +++ b/core/java/android/hardware/camera2/impl/CameraDevice.java @@ -16,21 +16,26 @@ package android.hardware.camera2.impl; -import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.CaptureResult; -import android.hardware.camera2.ICameraDeviceUser; -import android.hardware.camera2.ICameraDeviceCallbacks; +import static android.hardware.camera2.CameraAccessException.CAMERA_IN_USE; + import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CameraProperties; import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.utils.CameraRuntimeException; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.ICameraDeviceCallbacks; +import android.hardware.camera2.ICameraDeviceUser; import android.hardware.camera2.utils.CameraBinderDecorator; +import android.hardware.camera2.utils.CameraRuntimeException; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; +import android.util.SparseArray; import android.view.Surface; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Stack; @@ -54,6 +59,8 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { new HashMap<Integer, CaptureListenerHolder>(); private final Stack<Integer> mRepeatingRequestIdStack = new Stack<Integer>(); + // Map stream IDs to Surfaces + private final SparseArray<Surface> mConfiguredOutputs = new SparseArray<Surface>(); private final String mCameraId; @@ -94,18 +101,49 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { @Override public void configureOutputs(List<Surface> outputs) throws CameraAccessException { synchronized (mLock) { - // TODO: delete outputs that aren't in this list that were configured previously - for (Surface s : outputs) { - try { + HashSet<Surface> addSet = new HashSet<Surface>(outputs); // Streams to create + List<Integer> deleteList = new ArrayList<Integer>(); // Streams to delete + + // Determine which streams need to be created, which to be deleted + for (int i = 0; i < mConfiguredOutputs.size(); ++i) { + int streamId = mConfiguredOutputs.keyAt(i); + Surface s = mConfiguredOutputs.valueAt(i); + + if (!outputs.contains(s)) { + deleteList.add(streamId); + } else { + addSet.remove(s); // Don't create a stream previously created + } + } + + try { + // TODO: mRemoteDevice.beginConfigure + + // Delete all streams first (to free up HW resources) + for (Integer streamId : deleteList) { + mRemoteDevice.deleteStream(streamId); + mConfiguredOutputs.delete(streamId); + } + + // Add all new streams + for (Surface s : addSet) { // TODO: remove width,height,format since we are ignoring // it. - mRemoteDevice.createStream(0, 0, 0, s); - } catch (CameraRuntimeException e) { - throw e.asChecked(); - } catch (RemoteException e) { - // impossible - return; + int streamId = mRemoteDevice.createStream(0, 0, 0, s); + mConfiguredOutputs.put(streamId, s); + } + + // TODO: mRemoteDevice.endConfigure + } catch (CameraRuntimeException e) { + if (e.getReason() == CAMERA_IN_USE) { + throw new IllegalStateException("The camera is currently busy." + + " You must call waitUntilIdle before trying to reconfigure."); } + + throw e.asChecked(); + } catch (RemoteException e) { + // impossible + return; } } } @@ -242,6 +280,20 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } @Override + public void flush() throws CameraAccessException { + synchronized (mLock) { + try { + mRemoteDevice.flush(); + } catch (CameraRuntimeException e) { + throw e.asChecked(); + } catch (RemoteException e) { + // impossible + return; + } + } + } + + @Override public void close() throws Exception { // TODO: every method should throw IllegalStateException after close has been called diff --git a/core/java/android/net/CaptivePortalTracker.java b/core/java/android/net/CaptivePortalTracker.java index 74c2c59..01977cd 100644 --- a/core/java/android/net/CaptivePortalTracker.java +++ b/core/java/android/net/CaptivePortalTracker.java @@ -16,22 +16,16 @@ package android.net; -import android.app.Activity; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.res.Resources; import android.database.ContentObserver; import android.net.ConnectivityManager; import android.net.IConnectivityManager; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Handler; -import android.os.UserHandle; import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; @@ -46,7 +40,6 @@ import android.telephony.CellInfoGsm; import android.telephony.CellInfoLte; import android.telephony.CellInfoWcdma; import android.telephony.TelephonyManager; -import android.text.TextUtils; import com.android.internal.util.State; import com.android.internal.util.StateMachine; @@ -60,8 +53,6 @@ import java.net.URL; import java.net.UnknownHostException; import java.util.List; -import com.android.internal.R; - /** * This class allows captive portal detection on a network. * @hide @@ -71,7 +62,6 @@ public class CaptivePortalTracker extends StateMachine { private static final String TAG = "CaptivePortalTracker"; private static final String DEFAULT_SERVER = "clients3.google.com"; - private static final String NOTIFICATION_ID = "CaptivePortal.Notification"; private static final int SOCKET_TIMEOUT_MS = 10000; @@ -93,7 +83,6 @@ public class CaptivePortalTracker extends StateMachine { private String mServer; private String mUrl; - private boolean mNotificationShown = false; private boolean mIsCaptivePortalCheckEnabled = false; private IConnectivityManager mConnService; private TelephonyManager mTelephonyManager; @@ -192,12 +181,12 @@ public class CaptivePortalTracker extends StateMachine { private class DefaultState extends State { @Override public void enter() { - if (DBG) log(getName() + "\n"); + setNotificationOff(); } @Override public boolean processMessage(Message message) { - if (DBG) log(getName() + message.toString() + "\n"); + if (DBG) log(getName() + message.toString()); switch (message.what) { case CMD_DETECT_PORTAL: NetworkInfo info = (NetworkInfo) message.obj; @@ -219,23 +208,24 @@ public class CaptivePortalTracker extends StateMachine { private class NoActiveNetworkState extends State { @Override public void enter() { - if (DBG) log(getName() + "\n"); mNetworkInfo = null; - /* Clear any previous notification */ - setNotificationVisible(false); } @Override public boolean processMessage(Message message) { - if (DBG) log(getName() + message.toString() + "\n"); + if (DBG) log(getName() + message.toString()); InetAddress server; NetworkInfo info; switch (message.what) { case CMD_CONNECTIVITY_CHANGE: info = (NetworkInfo) message.obj; - if (info.isConnected() && isActiveNetwork(info)) { - mNetworkInfo = info; - transitionTo(mDelayedCaptiveCheckState); + if (info.getType() == ConnectivityManager.TYPE_WIFI) { + if (info.isConnected() && isActiveNetwork(info)) { + mNetworkInfo = info; + transitionTo(mDelayedCaptiveCheckState); + } + } else { + log(getName() + " not a wifi connectivity change, ignore"); } break; default: @@ -248,7 +238,7 @@ public class CaptivePortalTracker extends StateMachine { private class ActiveNetworkState extends State { @Override public void enter() { - if (DBG) log(getName() + "\n"); + setNotificationOff(); } @Override @@ -281,7 +271,6 @@ public class CaptivePortalTracker extends StateMachine { private class DelayedCaptiveCheckState extends State { @Override public void enter() { - if (DBG) log(getName() + "\n"); Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0); if (mDeviceProvisioned) { sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS); @@ -292,7 +281,7 @@ public class CaptivePortalTracker extends StateMachine { @Override public boolean processMessage(Message message) { - if (DBG) log(getName() + message.toString() + "\n"); + if (DBG) log(getName() + message.toString()); switch (message.what) { case CMD_DELAYED_CAPTIVE_CHECK: if (message.arg1 == mDelayedCheckToken) { @@ -308,7 +297,12 @@ public class CaptivePortalTracker extends StateMachine { if (captive) { // Setup Wizard will assist the user in connecting to a captive // portal, so make the notification visible unless during setup - setNotificationVisible(true); + try { + mConnService.setProvisioningNotificationVisible(true, + mNetworkInfo.getType(), mNetworkInfo.getExtraInfo(), mUrl); + } catch(RemoteException e) { + e.printStackTrace(); + } } } else { Intent intent = new Intent( @@ -366,6 +360,15 @@ public class CaptivePortalTracker extends StateMachine { return false; } + private void setNotificationOff() { + try { + mConnService.setProvisioningNotificationVisible(false, ConnectivityManager.TYPE_NONE, + null, null); + } catch (RemoteException e) { + log("setNotificationOff: " + e); + } + } + /** * Do a URL fetch on a known server to see if we get the data we expect. * Measure the response time and broadcast that. @@ -394,17 +397,14 @@ public class CaptivePortalTracker extends StateMachine { long responseTimestamp = SystemClock.elapsedRealtime(); // we got a valid response, but not from the real google - boolean isCaptivePortal = urlConnection.getResponseCode() != 204; + int rspCode = urlConnection.getResponseCode(); + boolean isCaptivePortal = rspCode != 204; sendNetworkConditionsBroadcast(true /* response received */, isCaptivePortal, requestTimestamp, responseTimestamp); + + if (DBG) log("isCaptivePortal: ret=" + isCaptivePortal + " rspCode=" + rspCode); return isCaptivePortal; - } catch (SocketTimeoutException e) { - if (DBG) log("Probably a portal: exception " + e); - if (requestTimestamp != -1) { - sendFailedCaptivePortalCheckBroadcast(requestTimestamp); - } // else something went wrong with setting up the urlConnection - return true; } catch (IOException e) { if (DBG) log("Probably not a portal: exception " + e); if (requestTimestamp != -1) { @@ -435,77 +435,6 @@ public class CaptivePortalTracker extends StateMachine { return null; } - private void setNotificationVisible(boolean visible) { - // if it should be hidden and it is already hidden, then noop - if (!visible && !mNotificationShown) { - if (DBG) log("setNotivicationVisible: false and not shown, so noop"); - return; - } - - Resources r = Resources.getSystem(); - NotificationManager notificationManager = (NotificationManager) mContext - .getSystemService(Context.NOTIFICATION_SERVICE); - - if (visible) { - CharSequence title; - CharSequence details; - int icon; - String url = null; - switch (mNetworkInfo.getType()) { - case ConnectivityManager.TYPE_WIFI: - title = r.getString(R.string.wifi_available_sign_in, 0); - details = r.getString(R.string.network_available_sign_in_detailed, - mNetworkInfo.getExtraInfo()); - icon = R.drawable.stat_notify_wifi_in_range; - url = mUrl; - break; - case ConnectivityManager.TYPE_MOBILE: - title = r.getString(R.string.network_available_sign_in, 0); - // TODO: Change this to pull from NetworkInfo once a printable - // name has been added to it - details = mTelephonyManager.getNetworkOperatorName(); - icon = R.drawable.stat_notify_rssi_in_range; - try { - url = mConnService.getMobileProvisioningUrl(); - if (TextUtils.isEmpty(url)) { - url = mConnService.getMobileRedirectedProvisioningUrl(); - } - } catch(RemoteException e) { - e.printStackTrace(); - } - if (TextUtils.isEmpty(url)) { - url = mUrl; - } - break; - default: - title = r.getString(R.string.network_available_sign_in, 0); - details = r.getString(R.string.network_available_sign_in_detailed, - mNetworkInfo.getExtraInfo()); - icon = R.drawable.stat_notify_rssi_in_range; - url = mUrl; - break; - } - - Notification notification = new Notification(); - notification.when = 0; - notification.icon = icon; - notification.flags = Notification.FLAG_AUTO_CANCEL; - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | - Intent.FLAG_ACTIVITY_NEW_TASK); - notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); - notification.tickerText = title; - notification.setLatestEventInfo(mContext, title, details, notification.contentIntent); - - if (DBG) log("setNotivicationVisible: make visible"); - notificationManager.notify(NOTIFICATION_ID, 1, notification); - } else { - if (DBG) log("setNotivicationVisible: cancel notification"); - notificationManager.cancel(NOTIFICATION_ID, 1); - } - mNotificationShown = visible; - } - private void sendFailedCaptivePortalCheckBroadcast(long requestTimestampMs) { sendNetworkConditionsBroadcast(false /* response received */, false /* ignored */, requestTimestampMs, 0 /* ignored */); diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index f6a3a4a..3874369 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -624,6 +624,29 @@ public class ConnectivityManager { } /** + * Returns details about the Provisioning or currently active default data network. When + * connected, this network is the default route for outgoing connections. + * You should always check {@link NetworkInfo#isConnected()} before initiating + * network traffic. This may return {@code null} when there is no default + * network. + * + * @return a {@link NetworkInfo} object for the current default network + * or {@code null} if no network default network is currently active + * + * <p>This method requires the call to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. + * + * {@hide} + */ + public NetworkInfo getProvisioningOrActiveNetworkInfo() { + try { + return mService.getProvisioningOrActiveNetworkInfo(); + } catch (RemoteException e) { + return null; + } + } + + /** * Returns the IP information for the current default network. * * @return a {@link LinkProperties} object describing the IP info @@ -1357,63 +1380,19 @@ public class ConnectivityManager { } /** - * The ResultReceiver resultCode for checkMobileProvisioning (CMP_RESULT_CODE) - */ - - /** - * No connection was possible to the network. - * {@hide} - */ - public static final int CMP_RESULT_CODE_NO_CONNECTION = 0; - - /** - * A connection was made to the internet, all is well. - * {@hide} - */ - public static final int CMP_RESULT_CODE_CONNECTABLE = 1; - - /** - * A connection was made but there was a redirection, we appear to be in walled garden. - * This is an indication of a warm sim on a mobile network. - * {@hide} - */ - public static final int CMP_RESULT_CODE_REDIRECTED = 2; - - /** - * A connection was made but no dns server was available to resolve a name to address. - * This is an indication of a warm sim on a mobile network. + * Check mobile provisioning. * - * {@hide} - */ - public static final int CMP_RESULT_CODE_NO_DNS = 3; - - /** - * A connection was made but could not open a TCP connection. - * This is an indication of a warm sim on a mobile network. - * {@hide} - */ - public static final int CMP_RESULT_CODE_NO_TCP_CONNECTION = 4; - - /** - * Check mobile provisioning. The resultCode passed to - * onReceiveResult will be one of the CMP_RESULT_CODE_xxxx values above. - * This may take a minute or more to complete. - * - * @param sendNotificaiton, when true a notification will be sent to user. * @param suggestedTimeOutMs, timeout in milliseconds - * @param resultReceiver needs to be supplied to receive the result * * @return time out that will be used, maybe less that suggestedTimeOutMs * -1 if an error. * * {@hide} */ - public int checkMobileProvisioning(boolean sendNotification, int suggestedTimeOutMs, - ResultReceiver resultReceiver) { + public int checkMobileProvisioning(int suggestedTimeOutMs) { int timeOutMs = -1; try { - timeOutMs = mService.checkMobileProvisioning(sendNotification, suggestedTimeOutMs, - resultReceiver); + timeOutMs = mService.checkMobileProvisioning(suggestedTimeOutMs); } catch (RemoteException e) { } return timeOutMs; @@ -1481,4 +1460,20 @@ public class ConnectivityManager { return null; } } + + /** + * Set sign in error notification to visible or in visible + * + * @param visible + * @param networkType + * + * {@hide} + */ + public void setProvisioningNotificationVisible(boolean visible, int networkType, + String extraInfo, String url) { + try { + mService.setProvisioningNotificationVisible(visible, networkType, extraInfo, url); + } catch (RemoteException e) { + } + } } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index bf2dade..c07e900 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -50,6 +50,8 @@ interface IConnectivityManager NetworkInfo getNetworkInfo(int networkType); NetworkInfo[] getAllNetworkInfo(); + NetworkInfo getProvisioningOrActiveNetworkInfo(); + boolean isNetworkSupported(int networkType); LinkProperties getActiveLinkProperties(); @@ -141,7 +143,7 @@ interface IConnectivityManager int findConnectionTypeForIface(in String iface); - int checkMobileProvisioning(boolean sendNotification, int suggestedTimeOutMs, in ResultReceiver resultReceiver); + int checkMobileProvisioning(int suggestedTimeOutMs); String getMobileProvisioningUrl(); @@ -153,4 +155,5 @@ interface IConnectivityManager LinkInfo[] getAllLinkInfo(); + void setProvisioningNotificationVisible(boolean visible, int networkType, in String extraInfo, in String url); } diff --git a/core/java/android/net/LinkInfo.java b/core/java/android/net/LinkInfo.java index 98e8f35..47b8a95 100644 --- a/core/java/android/net/LinkInfo.java +++ b/core/java/android/net/LinkInfo.java @@ -30,7 +30,7 @@ import android.os.Parcelable; */ public class LinkInfo implements Parcelable { - public static final int UNKNOWN = Integer.MAX_VALUE; + public static final int UNKNOWN = -1; public static final int NORMALIZED_MIN_SIGNAL_STRENGTH = 0; @@ -43,8 +43,8 @@ public class LinkInfo implements Parcelable public int mNormalizedSignalStrength = UNKNOWN; - public int mPacketCount = UNKNOWN; - public int mPacketErrorCount = UNKNOWN; + public long mPacketCount = UNKNOWN; + public long mPacketErrorCount = UNKNOWN; public int mTheoreticalTxBandwidth = UNKNOWN; public int mTheoreticalRxBandwidth = UNKNOWN; public int mTheoreticalLatency = UNKNOWN; @@ -82,8 +82,8 @@ public class LinkInfo implements Parcelable dest.writeInt(objectType); dest.writeInt(mNetworkType); dest.writeInt(mNormalizedSignalStrength); - dest.writeInt(mPacketCount); - dest.writeInt(mPacketErrorCount); + dest.writeLong(mPacketCount); + dest.writeLong(mPacketErrorCount); dest.writeInt(mTheoreticalTxBandwidth); dest.writeInt(mTheoreticalRxBandwidth); dest.writeInt(mTheoreticalLatency); @@ -116,8 +116,8 @@ public class LinkInfo implements Parcelable protected void initializeFromParcel(Parcel in) { mNetworkType = in.readInt(); mNormalizedSignalStrength = in.readInt(); - mPacketCount = in.readInt(); - mPacketErrorCount = in.readInt(); + mPacketCount = in.readLong(); + mPacketErrorCount = in.readLong(); mTheoreticalTxBandwidth = in.readInt(); mTheoreticalRxBandwidth = in.readInt(); mTheoreticalLatency = in.readInt(); diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index faa13b0..125d5c1 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -61,8 +61,12 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { private ITelephony mPhoneService; private String mApnType; + private NetworkInfo mNetworkInfo; private boolean mTeardownRequested = false; private Handler mTarget; + private Context mContext; + private LinkProperties mLinkProperties; + private LinkCapabilities mLinkCapabilities; private boolean mPrivateDnsRouteSet = false; private boolean mDefaultRouteSet = false; @@ -106,6 +110,7 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { IntentFilter filter = new IntentFilter(); filter.addAction(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); + filter.addAction(TelephonyIntents.ACTION_DATA_CONNECTION_CONNECTED_TO_PROVISIONING_APN); filter.addAction(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED); mContext.registerReceiver(new MobileDataStateReceiver(), filter); @@ -184,10 +189,41 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { public void releaseWakeLock() { } + private void updateLinkProperitesAndCapatilities(Intent intent) { + mLinkProperties = intent.getParcelableExtra( + PhoneConstants.DATA_LINK_PROPERTIES_KEY); + if (mLinkProperties == null) { + loge("CONNECTED event did not supply link properties."); + mLinkProperties = new LinkProperties(); + } + mLinkCapabilities = intent.getParcelableExtra( + PhoneConstants.DATA_LINK_CAPABILITIES_KEY); + if (mLinkCapabilities == null) { + loge("CONNECTED event did not supply link capabilities."); + mLinkCapabilities = new LinkCapabilities(); + } + } + private class MobileDataStateReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(TelephonyIntents. + ACTION_DATA_CONNECTION_CONNECTED_TO_PROVISIONING_APN)) { + String apnName = intent.getStringExtra(PhoneConstants.DATA_APN_KEY); + String apnType = intent.getStringExtra(PhoneConstants.DATA_APN_TYPE_KEY); + if (!TextUtils.equals(mApnType, apnType)) { + return; + } + if (DBG) { + log("Broadcast received: " + intent.getAction() + " apnType=" + apnType + + " apnName=" + apnName); + } + + // Make us in the connecting state until we make a new TYPE_MOBILE_PROVISIONING + mMobileDataState = PhoneConstants.DataState.CONNECTING; + updateLinkProperitesAndCapatilities(intent); + setDetailedState(DetailedState.CONNECTED_TO_PROVISIONING_NETWORK, "", apnName); + } else if (intent.getAction().equals(TelephonyIntents. ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) { String apnType = intent.getStringExtra(PhoneConstants.DATA_APN_TYPE_KEY); if (VDBG) { @@ -249,18 +285,7 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { setDetailedState(DetailedState.SUSPENDED, reason, apnName); break; case CONNECTED: - mLinkProperties = intent.getParcelableExtra( - PhoneConstants.DATA_LINK_PROPERTIES_KEY); - if (mLinkProperties == null) { - loge("CONNECTED event did not supply link properties."); - mLinkProperties = new LinkProperties(); - } - mLinkCapabilities = intent.getParcelableExtra( - PhoneConstants.DATA_LINK_CAPABILITIES_KEY); - if (mLinkCapabilities == null) { - loge("CONNECTED event did not supply link capabilities."); - mLinkCapabilities = new LinkCapabilities(); - } + updateLinkProperitesAndCapatilities(intent); setDetailedState(DetailedState.CONNECTED, reason, apnName); break; } @@ -319,8 +344,8 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { String reason = intent.getStringExtra(PhoneConstants.FAILURE_REASON_KEY); String apnName = intent.getStringExtra(PhoneConstants.DATA_APN_KEY); if (DBG) { - log("Received " + intent.getAction() + - " broadcast" + (reason == null ? "" : "(" + reason + ")")); + log("Broadcast received: " + intent.getAction() + + " reason=" + reason == null ? "null" : reason); } setDetailedState(DetailedState.FAILED, reason, apnName); } else { @@ -412,6 +437,13 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { return (setEnableApn(mApnType, false) != PhoneConstants.APN_REQUEST_FAILED); } + /** + * @return true if this is ready to operate + */ + public boolean isReady() { + return mDataConnectionTrackerAc != null; + } + @Override public void captivePortalCheckComplete() { // not implemented @@ -574,6 +606,40 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { } } + /** + * Inform DCT mobile provisioning has started, it ends when provisioning completes. + */ + public void enableMobileProvisioning(String url) { + if (DBG) log("enableMobileProvisioning(url=" + url + ")"); + final AsyncChannel channel = mDataConnectionTrackerAc; + if (channel != null) { + Message msg = Message.obtain(); + msg.what = DctConstants.CMD_ENABLE_MOBILE_PROVISIONING; + msg.setData(Bundle.forPair(DctConstants.PROVISIONING_URL_KEY, url)); + channel.sendMessage(msg); + } + } + + /** + * Return if this network is the provisioning network. Valid only if connected. + * @param met + */ + public boolean isProvisioningNetwork() { + boolean retVal; + try { + Message msg = Message.obtain(); + msg.what = DctConstants.CMD_IS_PROVISIONING_APN; + msg.setData(Bundle.forPair(DctConstants.APN_TYPE_KEY, mApnType)); + Message result = mDataConnectionTrackerAc.sendMessageSynchronously(msg); + retVal = result.arg1 == DctConstants.ENABLED; + } catch (NullPointerException e) { + loge("isProvisioningNetwork: X " + e); + retVal = false; + } + if (DBG) log("isProvisioningNetwork: retVal=" + retVal); + return retVal; + } + @Override public void addStackedLink(LinkProperties link) { mLinkProperties.addStackedLink(link); diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 689dae5..dabc73a 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -84,6 +84,12 @@ public class NetworkInfo implements Parcelable { VERIFYING_POOR_LINK, /** Checking if network is a captive portal */ CAPTIVE_PORTAL_CHECK, + /** + * Network is connected to provisioning network + * TODO: Probably not needed when we add TYPE_PROVISIONING_NETWORK + * @hide + */ + CONNECTED_TO_PROVISIONING_NETWORK } /** @@ -108,6 +114,7 @@ public class NetworkInfo implements Parcelable { stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED); stateMap.put(DetailedState.FAILED, State.DISCONNECTED); stateMap.put(DetailedState.BLOCKED, State.DISCONNECTED); + stateMap.put(DetailedState.CONNECTED_TO_PROVISIONING_NETWORK, State.CONNECTED); } private int mNetworkType; diff --git a/core/java/android/net/SamplingDataTracker.java b/core/java/android/net/SamplingDataTracker.java index b5dc140..ac24930 100644 --- a/core/java/android/net/SamplingDataTracker.java +++ b/core/java/android/net/SamplingDataTracker.java @@ -37,12 +37,12 @@ public class SamplingDataTracker public static class SamplingSnapshot { - public int mTxByteCount; - public int mRxByteCount; - public int mTxPacketCount; - public int mRxPacketCount; - public int mTxPacketErrorCount; - public int mRxPacketErrorCount; + public long mTxByteCount; + public long mRxByteCount; + public long mTxPacketCount; + public long mRxPacketCount; + public long mTxPacketErrorCount; + public long mRxPacketErrorCount; public long mTimestamp; } @@ -76,32 +76,37 @@ public class SamplingDataTracker if (DBG) Slog.d(TAG, "Found data for interface " + currentIface); if (mapIfaceToSample.containsKey(currentIface)) { - SamplingSnapshot ss = new SamplingSnapshot(); - - ss.mTxByteCount = Integer.parseInt(tokens[1]); - ss.mTxPacketCount = Integer.parseInt(tokens[2]); - ss.mTxPacketErrorCount = Integer.parseInt(tokens[3]); - ss.mRxByteCount = Integer.parseInt(tokens[9]); - ss.mRxPacketCount = Integer.parseInt(tokens[10]); - ss.mRxPacketErrorCount = Integer.parseInt(tokens[11]); - - ss.mTimestamp = SystemClock.elapsedRealtime(); - - if (DBG) { - Slog.d(TAG, "Interface = " + currentIface); - Slog.d(TAG, "ByteCount = " + String.valueOf(ss.mTxByteCount)); - Slog.d(TAG, "TxPacketCount = " + String.valueOf(ss.mTxPacketCount)); - Slog.d(TAG, "TxPacketErrorCount = " - + String.valueOf(ss.mTxPacketErrorCount)); - Slog.d(TAG, "RxByteCount = " + String.valueOf(ss.mRxByteCount)); - Slog.d(TAG, "RxPacketCount = " + String.valueOf(ss.mRxPacketCount)); - Slog.d(TAG, "RxPacketErrorCount = " - + String.valueOf(ss.mRxPacketErrorCount)); - Slog.d(TAG, "Timestamp = " + String.valueOf(ss.mTimestamp)); - Slog.d(TAG, "---------------------------"); + try { + SamplingSnapshot ss = new SamplingSnapshot(); + + ss.mTxByteCount = Long.parseLong(tokens[1]); + ss.mTxPacketCount = Long.parseLong(tokens[2]); + ss.mTxPacketErrorCount = Long.parseLong(tokens[3]); + ss.mRxByteCount = Long.parseLong(tokens[9]); + ss.mRxPacketCount = Long.parseLong(tokens[10]); + ss.mRxPacketErrorCount = Long.parseLong(tokens[11]); + + ss.mTimestamp = SystemClock.elapsedRealtime(); + + if (DBG) { + Slog.d(TAG, "Interface = " + currentIface); + Slog.d(TAG, "ByteCount = " + String.valueOf(ss.mTxByteCount)); + Slog.d(TAG, "TxPacketCount = " + String.valueOf(ss.mTxPacketCount)); + Slog.d(TAG, "TxPacketErrorCount = " + + String.valueOf(ss.mTxPacketErrorCount)); + Slog.d(TAG, "RxByteCount = " + String.valueOf(ss.mRxByteCount)); + Slog.d(TAG, "RxPacketCount = " + String.valueOf(ss.mRxPacketCount)); + Slog.d(TAG, "RxPacketErrorCount = " + + String.valueOf(ss.mRxPacketErrorCount)); + Slog.d(TAG, "Timestamp = " + String.valueOf(ss.mTimestamp)); + Slog.d(TAG, "---------------------------"); + } + + mapIfaceToSample.put(currentIface, ss); + + } catch (NumberFormatException e) { + // just ignore this data point } - - mapIfaceToSample.put(currentIface, ss); } } @@ -179,7 +184,7 @@ public class SamplingDataTracker } } - public int getSampledTxByteCount() { + public long getSampledTxByteCount() { synchronized(mSamplingDataLock) { if (mBeginningSample != null && mEndingSample != null) { return mEndingSample.mTxByteCount - mBeginningSample.mTxByteCount; @@ -189,7 +194,7 @@ public class SamplingDataTracker } } - public int getSampledTxPacketCount() { + public long getSampledTxPacketCount() { synchronized(mSamplingDataLock) { if (mBeginningSample != null && mEndingSample != null) { return mEndingSample.mTxPacketCount - mBeginningSample.mTxPacketCount; @@ -199,7 +204,7 @@ public class SamplingDataTracker } } - public int getSampledTxPacketErrorCount() { + public long getSampledTxPacketErrorCount() { synchronized(mSamplingDataLock) { if (mBeginningSample != null && mEndingSample != null) { return mEndingSample.mTxPacketErrorCount - mBeginningSample.mTxPacketErrorCount; @@ -209,7 +214,7 @@ public class SamplingDataTracker } } - public int getSampledRxByteCount() { + public long getSampledRxByteCount() { synchronized(mSamplingDataLock) { if (mBeginningSample != null && mEndingSample != null) { return mEndingSample.mRxByteCount - mBeginningSample.mRxByteCount; @@ -219,7 +224,7 @@ public class SamplingDataTracker } } - public int getSampledRxPacketCount() { + public long getSampledRxPacketCount() { synchronized(mSamplingDataLock) { if (mBeginningSample != null && mEndingSample != null) { return mEndingSample.mRxPacketCount - mBeginningSample.mRxPacketCount; @@ -229,31 +234,31 @@ public class SamplingDataTracker } } - public int getSampledPacketCount() { + public long getSampledPacketCount() { return getSampledPacketCount(mBeginningSample, mEndingSample); } - public int getSampledPacketCount(SamplingSnapshot begin, SamplingSnapshot end) { + public long getSampledPacketCount(SamplingSnapshot begin, SamplingSnapshot end) { if (begin != null && end != null) { - int rxPacketCount = end.mRxPacketCount - begin.mRxPacketCount; - int txPacketCount = end.mTxPacketCount - begin.mTxPacketCount; + long rxPacketCount = end.mRxPacketCount - begin.mRxPacketCount; + long txPacketCount = end.mTxPacketCount - begin.mTxPacketCount; return rxPacketCount + txPacketCount; } else { return LinkInfo.UNKNOWN; } } - public int getSampledPacketErrorCount() { + public long getSampledPacketErrorCount() { if (mBeginningSample != null && mEndingSample != null) { - int rxPacketErrorCount = getSampledRxPacketErrorCount(); - int txPacketErrorCount = getSampledTxPacketErrorCount(); + long rxPacketErrorCount = getSampledRxPacketErrorCount(); + long txPacketErrorCount = getSampledTxPacketErrorCount(); return rxPacketErrorCount + txPacketErrorCount; } else { return LinkInfo.UNKNOWN; } } - public int getSampledRxPacketErrorCount() { + public long getSampledRxPacketErrorCount() { synchronized(mSamplingDataLock) { if (mBeginningSample != null && mEndingSample != null) { return mEndingSample.mRxPacketErrorCount - mBeginningSample.mRxPacketErrorCount; diff --git a/core/java/android/net/WifiLinkInfo.java b/core/java/android/net/WifiLinkInfo.java index f3b0032..a21f1fe 100644 --- a/core/java/android/net/WifiLinkInfo.java +++ b/core/java/android/net/WifiLinkInfo.java @@ -39,8 +39,8 @@ public final class WifiLinkInfo extends LinkInfo public int mRssi = UNKNOWN; /* packet statistics */ - public int mTxGood = UNKNOWN; - public int mTxBad = UNKNOWN; + public long mTxGood = UNKNOWN; + public long mTxBad = UNKNOWN; /** * Implement the Parcelable interface. @@ -51,8 +51,8 @@ public final class WifiLinkInfo extends LinkInfo dest.writeInt(mType); dest.writeInt(mRssi); - dest.writeInt(mTxGood); - dest.writeInt(mTxBad); + dest.writeLong(mTxGood); + dest.writeLong(mTxBad); dest.writeString(mBssid); } @@ -65,8 +65,8 @@ public final class WifiLinkInfo extends LinkInfo li.mType = in.readInt(); li.mRssi = in.readInt(); - li.mTxGood = in.readInt(); - li.mTxBad = in.readInt(); + li.mTxGood = in.readLong(); + li.mTxBad = in.readLong(); li.mBssid = in.readString(); diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl index 9c97659..15d0475 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -48,5 +48,6 @@ interface INfcAdapter void dispatch(in Tag tag); + void setReaderMode (IBinder b, int flags); void setP2pModes(int initatorModes, int targetModes); } diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java index 10183c0..d0d943c 100644 --- a/core/java/android/nfc/NfcActivityManager.java +++ b/core/java/android/nfc/NfcActivityManager.java @@ -19,6 +19,7 @@ package android.nfc; import android.app.Activity; import android.app.Application; import android.net.Uri; +import android.os.Binder; import android.os.Bundle; import android.os.RemoteException; import android.util.Log; @@ -111,6 +112,9 @@ public final class NfcActivityManager extends INdefPushCallback.Stub NfcAdapter.CreateBeamUrisCallback uriCallback = null; Uri[] uris = null; int flags = 0; + int readerModeFlags = 0; + Binder token; + public NfcActivityState(Activity activity) { if (activity.getWindow().isDestroyed()) { throw new IllegalStateException("activity is already destroyed"); @@ -120,6 +124,7 @@ public final class NfcActivityManager extends INdefPushCallback.Stub resumed = activity.isResumed(); this.activity = activity; + this.token = new Binder(); registerApplication(activity.getApplication()); } public void destroy() { @@ -131,6 +136,8 @@ public final class NfcActivityManager extends INdefPushCallback.Stub onNdefPushCompleteCallback = null; uriCallback = null; uris = null; + readerModeFlags = 0; + token = null; } @Override public String toString() { @@ -190,6 +197,44 @@ public final class NfcActivityManager extends INdefPushCallback.Stub mDefaultEvent = new NfcEvent(mAdapter); } + public void enableReaderMode(Activity activity, int flags) { + boolean isResumed; + Binder token; + synchronized (NfcActivityManager.this) { + NfcActivityState state = getActivityState(activity); + state.readerModeFlags = flags; + token = state.token; + isResumed = state.resumed; + } + if (isResumed) { + setReaderMode(token, flags); + } + } + + public void disableReaderMode(Activity activity) { + boolean isResumed; + Binder token; + synchronized (NfcActivityManager.this) { + NfcActivityState state = getActivityState(activity); + state.readerModeFlags = 0; + token = state.token; + isResumed = state.resumed; + } + if (isResumed) { + setReaderMode(token, 0); + } + + } + + public void setReaderMode(Binder token, int flags) { + if (DBG) Log.d(TAG, "Setting reader mode"); + try { + NfcAdapter.sService.setReaderMode(token, flags); + } catch (RemoteException e) { + mAdapter.attemptDeadServiceRecovery(e); + } + } + public void setNdefPushContentUri(Activity activity, Uri[] uris) { boolean isResumed; synchronized (NfcActivityManager.this) { @@ -341,11 +386,18 @@ public final class NfcActivityManager extends INdefPushCallback.Stub /** Callback from Activity life-cycle, on main thread */ @Override public void onActivityResumed(Activity activity) { + int readerModeFlags = 0; + Binder token; synchronized (NfcActivityManager.this) { NfcActivityState state = findActivityState(activity); if (DBG) Log.d(TAG, "onResume() for " + activity + " " + state); if (state == null) return; state.resumed = true; + token = state.token; + readerModeFlags = state.readerModeFlags; + } + if (readerModeFlags != 0) { + setReaderMode(token, readerModeFlags); } requestNfcServiceCallback(); } @@ -353,11 +405,19 @@ public final class NfcActivityManager extends INdefPushCallback.Stub /** Callback from Activity life-cycle, on main thread */ @Override public void onActivityPaused(Activity activity) { + boolean readerModeFlagsSet; + Binder token; synchronized (NfcActivityManager.this) { NfcActivityState state = findActivityState(activity); if (DBG) Log.d(TAG, "onPause() for " + activity + " " + state); if (state == null) return; state.resumed = false; + token = state.token; + readerModeFlagsSet = state.readerModeFlags != 0; + } + if (readerModeFlagsSet) { + // Restore default p2p modes + setReaderMode(token, 0); } } @@ -381,5 +441,4 @@ public final class NfcActivityManager extends INdefPushCallback.Stub } } } - } diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 2a4f93c..fa0c1f6 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -195,6 +195,50 @@ public final class NfcAdapter { public static final int STATE_ON = 3; public static final int STATE_TURNING_OFF = 4; + /** + * Flag for use with {@link #enableReaderMode(Activity, int)}. + * <p> + * Setting this flag enables polling for Nfc-A technology. + */ + public static final int FLAG_READER_NFC_A = 0x1; + + /** + * Flag for use with {@link #enableReaderMode(Activity, int)}. + * <p> + * Setting this flag enables polling for Nfc-B technology. + */ + public static final int FLAG_READER_NFC_B = 0x2; + + /** + * Flag for use with {@link #enableReaderMode(Activity, int)}. + * <p> + * Setting this flag enables polling for Nfc-F technology. + */ + public static final int FLAG_READER_NFC_F = 0x4; + + /** + * Flag for use with {@link #enableReaderMode(Activity, int)}. + * <p> + * Setting this flag enables polling for Nfc-V (ISO15693) technology. + */ + public static final int FLAG_READER_NFC_V = 0x8; + + /** + * Flag for use with {@link #enableReaderMode(Activity, int)}. + * <p> + * Setting this flag enables polling for Kovio technology. + */ + public static final int FLAG_READER_KOVIO = 0x10; + + /** + * Flag for use with {@link #enableReaderMode(Activity, int)}. + * <p> + * Setting this flag allows the caller to prevent the + * platform from performing an NDEF check on the tags it + * finds. + */ + public static final int FLAG_READER_SKIP_NDEF_CHECK = 0x80; + /** @hide */ public static final int FLAG_NDEF_PUSH_NO_CONFIRM = 0x1; @@ -1112,6 +1156,44 @@ public final class NfcAdapter { } /** + * Limit the NFC controller to reader mode while this Activity is in the foreground. + * + * <p>In this mode the NFC controller will only act as an NFC tag reader/writer, + * thus disabling any peer-to-peer (Android Beam) and card-emulation modes of + * the NFC adapter on this device. + * + * <p>Use {@link #FLAG_READER_SKIP_NDEF_CHECK} to prevent the platform from + * performing any NDEF checks in reader mode. Note that this will prevent the + * {@link Ndef} tag technology from being enumerated on the tag, and that + * NDEF-based tag dispatch will not be functional. + * + * <p>It is recommended to combine this method with + * {@link #enableForegroundDispatch(Activity, PendingIntent, IntentFilter[], String[][]) + * to ensure that tags are delivered to this activity. + * + * <p>For interacting with tags that are emulated on another Android device + * using Android's host-based card-emulation, the recommended flags are + * {@link #FLAG_READER_NFC_A} and {@link #FLAG_READER_SKIP_NDEF_CHECK}. + * + * @param activity the Activity that requests the adapter to be in reader mode + * @param flags Flags indicating poll technologies and other optional parameters + */ + public void enableReaderMode(Activity activity, int flags) { + mNfcActivityManager.enableReaderMode(activity, flags); + } + + /** + * Restore the NFC adapter to normal mode of operation: supporting + * peer-to-peer (Android Beam), card emulation, and polling for + * all supported tag technologies. + * + * @param activity the Activity that currently has reader mode enabled + */ + public void disableReaderMode(Activity activity) { + mNfcActivityManager.disableReaderMode(activity); + } + + /** * Enable NDEF message push over NFC while this Activity is in the foreground. * * <p>You must explicitly call this method every time the activity is diff --git a/core/java/android/nfc/cardemulation/ApduServiceInfo.java b/core/java/android/nfc/cardemulation/ApduServiceInfo.java index 3f7e3ef..41c6603 100644 --- a/core/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/core/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -75,16 +75,22 @@ public final class ApduServiceInfo implements Parcelable { final HashMap<String, AidGroup> mCategoryToGroup; /** + * Whether this service should only be started when the device is unlocked. + */ + final boolean mRequiresDeviceUnlock; + + /** * @hide */ public ApduServiceInfo(ResolveInfo info, boolean onHost, String description, - ArrayList<AidGroup> aidGroups) { + ArrayList<AidGroup> aidGroups, boolean requiresUnlock) { this.mService = info; this.mDescription = description; this.mAidGroups = aidGroups; this.mAids = new ArrayList<String>(); this.mCategoryToGroup = new HashMap<String, AidGroup>(); this.mOnHost = onHost; + this.mRequiresDeviceUnlock = requiresUnlock; for (AidGroup aidGroup : aidGroups) { this.mCategoryToGroup.put(aidGroup.category, aidGroup); this.mAids.addAll(aidGroup.aids); @@ -99,8 +105,12 @@ public final class ApduServiceInfo implements Parcelable { if (onHost) { parser = si.loadXmlMetaData(pm, HostApduService.SERVICE_META_DATA); if (parser == null) { - throw new XmlPullParserException("No " + HostApduService.SERVICE_META_DATA + - " meta-data"); + Log.d(TAG, "Didn't find service meta-data, trying legacy."); + parser = si.loadXmlMetaData(pm, HostApduService.OLD_SERVICE_META_DATA); + if (parser == null) { + throw new XmlPullParserException("No " + HostApduService.SERVICE_META_DATA + + " meta-data"); + } } } else { parser = si.loadXmlMetaData(pm, OffHostApduService.SERVICE_META_DATA); @@ -132,12 +142,16 @@ public final class ApduServiceInfo implements Parcelable { mService = info; mDescription = sa.getString( com.android.internal.R.styleable.HostApduService_description); + mRequiresDeviceUnlock = sa.getBoolean( + com.android.internal.R.styleable.HostApduService_requireDeviceUnlock, + false); } else { TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.OffHostApduService); mService = info; mDescription = sa.getString( com.android.internal.R.styleable.OffHostApduService_description); + mRequiresDeviceUnlock = false; } mAidGroups = new ArrayList<AidGroup>(); @@ -160,12 +174,12 @@ public final class ApduServiceInfo implements Parcelable { com.android.internal.R.styleable.AidGroup_description); String groupCategory = groupAttrs.getString( com.android.internal.R.styleable.AidGroup_category); - if (!CardEmulationManager.CATEGORY_PAYMENT.equals(groupCategory)) { - groupCategory = CardEmulationManager.CATEGORY_OTHER; + if (!CardEmulation.CATEGORY_PAYMENT.equals(groupCategory)) { + groupCategory = CardEmulation.CATEGORY_OTHER; } currentGroup = mCategoryToGroup.get(groupCategory); if (currentGroup != null) { - if (!CardEmulationManager.CATEGORY_OTHER.equals(groupCategory)) { + if (!CardEmulation.CATEGORY_OTHER.equals(groupCategory)) { Log.e(TAG, "Not allowing multiple aid-groups in the " + groupCategory + " category"); currentGroup = null; @@ -226,6 +240,10 @@ public final class ApduServiceInfo implements Parcelable { return mOnHost; } + public boolean requiresUnlock() { + return mRequiresDeviceUnlock; + } + public CharSequence loadLabel(PackageManager pm) { return mService.loadLabel(pm); } @@ -287,6 +305,7 @@ public final class ApduServiceInfo implements Parcelable { if (mAidGroups.size() > 0) { dest.writeTypedList(mAidGroups); } + dest.writeInt(mRequiresDeviceUnlock ? 1 : 0); }; public static final Parcelable.Creator<ApduServiceInfo> CREATOR = @@ -301,7 +320,8 @@ public final class ApduServiceInfo implements Parcelable { if (numGroups > 0) { source.readTypedList(aidGroups, AidGroup.CREATOR); } - return new ApduServiceInfo(info, onHost, description, aidGroups); + boolean requiresUnlock = (source.readInt() != 0) ? true : false; + return new ApduServiceInfo(info, onHost, description, aidGroups, requiresUnlock); } @Override diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java new file mode 100644 index 0000000..3cd7863 --- /dev/null +++ b/core/java/android/nfc/cardemulation/CardEmulation.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.app.ActivityThread; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.nfc.INfcCardEmulation; +import android.nfc.NfcAdapter; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; + +import java.util.HashMap; +import java.util.List; + +public final class CardEmulation { + static final String TAG = "CardEmulation"; + + /** + * Activity action: ask the user to change the default + * card emulation service for a certain category. This will + * show a dialog that asks the user whether he wants to + * replace the current default service with the service + * identified with the ComponentName specified in + * {@link #EXTRA_SERVICE_COMPONENT}, for the category + * specified in {@link #EXTRA_CATEGORY} + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHANGE_DEFAULT = + "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT"; + + /** + * The category extra for {@link #ACTION_CHANGE_DEFAULT} + * + * @see #ACTION_CHANGE_DEFAULT + */ + public static final String EXTRA_CATEGORY = "category"; + + /** + * The ComponentName object passed in as a parcelable + * extra for {@link #ACTION_CHANGE_DEFAULT} + * + * @see #ACTION_CHANGE_DEFAULT + */ + public static final String EXTRA_SERVICE_COMPONENT = "component"; + + /** + * The payment category can be used to indicate that an AID + * represents a payment application. + */ + public static final String CATEGORY_PAYMENT = "payment"; + + /** + * If an AID group does not contain a category, or the + * specified category is not defined by the platform version + * that is parsing the AID group, all AIDs in the group will + * automatically be categorized under the {@link #CATEGORY_OTHER} + * category. + */ + public static final String CATEGORY_OTHER = "other"; + + /** + * Return value for {@link #getSelectionModeForCategory(String)}. + * + * <p>In this mode, the user has set a default service for this + * AID category. If a remote reader selects any of the AIDs + * that the default service has registered in this category, + * that service will automatically be bound to to handle + * the transaction. + * + * <p>There are still cases where a service that is + * not the default for a category can selected: + * <p> + * If a remote reader selects an AID in this category + * that is not handled by the default service, and there is a set + * of other services {S} that do handle this AID, the + * user is asked if he wants to use any of the services in + * {S} instead. + * <p> + * As a special case, if the size of {S} is one, containing a single service X, + * and all AIDs X has registered in this category are not + * registered by any other service, then X will be + * selected automatically without asking the user. + * <p>Example: + * <ul> + * <li>Service A registers AIDs "1", "2" and "3" in the category + * <li>Service B registers AIDs "3" and "4" in the category + * <li>Service C registers AIDs "5" and "6" in the category + * </ul> + * In this case, the following will happen when service A + * is the default: + * <ul> + * <li>Reader selects AID "1", "2" or "3": service A is invoked automatically + * <li>Reader selects AID "4": the user is asked to confirm he + * wants to use service B, because its AIDs overlap with service A. + * <li>Reader selects AID "5" or "6": service C is invoked automatically, + * because all AIDs it has asked for are only registered by C, + * and there is no overlap. + * </ul> + * + */ + public static final int SELECTION_MODE_PREFER_DEFAULT = 0; + + /** + * Return value for {@link #getSelectionModeForCategory(String)}. + * + * <p>In this mode, whenever an AID of this category is selected, + * the user is asked which service he wants to use to handle + * the transaction, even if there is only one matching service. + */ + public static final int SELECTION_MODE_ALWAYS_ASK = 1; + + /** + * Return value for {@link #getSelectionModeForCategory(String)}. + * + * <p>In this mode, the user will only be asked to select a service + * if the selected AID has been registered by multiple applications. + */ + public static final int SELECTION_MODE_ASK_IF_CONFLICT = 2; + + static boolean sIsInitialized = false; + static HashMap<Context, CardEmulation> sCardEmus = new HashMap(); + static INfcCardEmulation sService; + + final Context mContext; + + private CardEmulation(Context context, INfcCardEmulation service) { + mContext = context.getApplicationContext(); + sService = service; + } + + public static synchronized CardEmulation getInstance(NfcAdapter adapter) { + if (adapter == null) throw new NullPointerException("NfcAdapter is null"); + Context context = adapter.getContext(); + if (context == null) { + Log.e(TAG, "NfcAdapter context is null."); + throw new UnsupportedOperationException(); + } + if (!sIsInitialized) { + IPackageManager pm = ActivityThread.getPackageManager(); + if (pm == null) { + Log.e(TAG, "Cannot get PackageManager"); + throw new UnsupportedOperationException(); + } + try { + if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) { + Log.e(TAG, "This device does not support card emulation"); + throw new UnsupportedOperationException(); + } + } catch (RemoteException e) { + Log.e(TAG, "PackageManager query failed."); + throw new UnsupportedOperationException(); + } + sIsInitialized = true; + } + CardEmulation manager = sCardEmus.get(context); + if (manager == null) { + // Get card emu service + INfcCardEmulation service = adapter.getCardEmulationService(); + manager = new CardEmulation(context, service); + sCardEmus.put(context, manager); + } + return manager; + } + + /** + * Allows an application to query whether a service is currently + * the default service to handle a card emulation category. + * + * <p>Note that if {@link #getSelectionModeForCategory(String)} + * returns {@link #SELECTION_MODE_ALWAYS_ASK}, this method will always + * return false. + * + * @param service The ComponentName of the service + * @param category The category + * @return whether service is currently the default service for the category. + */ + public boolean isDefaultServiceForCategory(ComponentName service, String category) { + try { + return sService.isDefaultServiceForCategory(UserHandle.myUserId(), service, category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.isDefaultServiceForCategory(UserHandle.myUserId(), service, + category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + } + } + + /** + * + * Allows an application to query whether a service is currently + * the default handler for a specified ISO7816-4 Application ID. + * + * @param service The ComponentName of the service + * @param aid The ISO7816-4 Application ID + * @return + */ + public boolean isDefaultServiceForAid(ComponentName service, String aid) { + try { + return sService.isDefaultServiceForAid(UserHandle.myUserId(), service, aid); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.isDefaultServiceForAid(UserHandle.myUserId(), service, aid); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Returns the application selection mode for the passed in category. + * Valid return values are: + * <p>{@link #SELECTION_MODE_PREFER_DEFAULT} the user has requested a default + * application for this category, which will be preferred. + * <p>{@link #SELECTION_MODE_ALWAYS_ASK} the user has requested to be asked + * every time what app he would like to use in this category. + * <p>{@link #SELECTION_MODE_ASK_IF_CONFLICT} the user will only be asked + * to pick a service if there is a conflict. + * @param category The category, for example {@link #CATEGORY_PAYMENT} + * @return + */ + public int getSelectionModeForCategory(String category) { + if (CATEGORY_PAYMENT.equals(category)) { + String defaultComponent = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT); + if (defaultComponent != null) { + return SELECTION_MODE_PREFER_DEFAULT; + } else { + return SELECTION_MODE_ALWAYS_ASK; + } + } else { + // All other categories are in "only ask if conflict" mode + return SELECTION_MODE_ASK_IF_CONFLICT; + } + } + + /** + * @hide + */ + public boolean setDefaultServiceForCategory(ComponentName service, String category) { + try { + return sService.setDefaultServiceForCategory(UserHandle.myUserId(), service, category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setDefaultServiceForCategory(UserHandle.myUserId(), service, + category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * @hide + */ + public boolean setDefaultForNextTap(ComponentName service) { + try { + return sService.setDefaultForNextTap(UserHandle.myUserId(), service); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setDefaultForNextTap(UserHandle.myUserId(), service); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + /** + * @hide + */ + public List<ApduServiceInfo> getServices(String category) { + try { + return sService.getServices(UserHandle.myUserId(), category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + try { + return sService.getServices(UserHandle.myUserId(), category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return null; + } + } + } + + void recoverService() { + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); + sService = adapter.getCardEmulationService(); + } +} diff --git a/core/java/android/nfc/cardemulation/CardEmulationManager.java b/core/java/android/nfc/cardemulation/CardEmulationManager.java index 537fded..124ea1c 100644 --- a/core/java/android/nfc/cardemulation/CardEmulationManager.java +++ b/core/java/android/nfc/cardemulation/CardEmulationManager.java @@ -27,11 +27,16 @@ import android.nfc.INfcCardEmulation; import android.nfc.NfcAdapter; import android.os.RemoteException; import android.os.UserHandle; +import android.provider.Settings; import android.util.Log; import java.util.HashMap; import java.util.List; +/** + * TODO Remove when calling .apks are upgraded + * @hide + */ public final class CardEmulationManager { static final String TAG = "CardEmulationManager"; @@ -78,19 +83,68 @@ public final class CardEmulationManager { */ public static final String CATEGORY_OTHER = "other"; - static boolean sIsInitialized = false; - static HashMap<Context, CardEmulationManager> sCardEmuManagers = new HashMap(); - static INfcCardEmulation sService; + /** + * Return value for {@link #getSelectionModeForCategory(String)}. + * + * <p>In this mode, the user has set a default service for this + * AID category. If a remote reader selects any of the AIDs + * that the default service has registered in this category, + * that service will automatically be bound to to handle + * the transaction. + * + * <p>There are still cases where a service that is + * not the default for a category can selected: + * <p> + * If a remote reader selects an AID in this category + * that is not handled by the default service, and there is a set + * of other services {S} that do handle this AID, the + * user is asked if he wants to use any of the services in + * {S} instead. + * <p> + * As a special case, if the size of {S} is one, containing a single service X, + * and all AIDs X has registered in this category are not + * registered by any other service, then X will be + * selected automatically without asking the user. + * <p>Example: + * <ul> + * <li>Service A registers AIDs "1", "2" and "3" in the category + * <li>Service B registers AIDs "3" and "4" in the category + * <li>Service C registers AIDs "5" and "6" in the category + * </ul> + * In this case, the following will happen when service A + * is the default: + * <ul> + * <li>Reader selects AID "1", "2" or "3": service A is invoked automatically + * <li>Reader selects AID "4": the user is asked to confirm he + * wants to use service B, because its AIDs overlap with service A. + * <li>Reader selects AID "5" or "6": service C is invoked automatically, + * because all AIDs it has asked for are only registered by C, + * and there is no overlap. + * </ul> + * + */ + public static final int SELECTION_MODE_PREFER_DEFAULT = 0; /** - * @hide + * Return value for {@link #getSelectionModeForCategory(String)}. + * + * <p>In this mode, whenever an AID of this category is selected, + * the user is asked which service he wants to use to handle + * the transaction, even if there is only one matching service. */ - public static final String PAYMENT_MODE_AUTO = "auto"; + public static final int SELECTION_MODE_ALWAYS_ASK = 1; /** - * @hide + * Return value for {@link #getSelectionModeForCategory(String)}. + * + * <p>In this mode, the user will only be asked to select a service + * if the selected AID has been registered by multiple applications. */ - public static final String PAYMENT_MODE_MANUAL = "manual"; + public static final int SELECTION_MODE_ASK_IF_CONFLICT = 2; + + static boolean sIsInitialized = false; + static HashMap<Context, CardEmulationManager> sCardEmuManagers = new HashMap(); + static INfcCardEmulation sService; final Context mContext; @@ -113,7 +167,7 @@ public final class CardEmulationManager { throw new UnsupportedOperationException(); } try { - if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HCE)) { + if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) { Log.e(TAG, "This device does not support card emulation"); throw new UnsupportedOperationException(); } @@ -137,6 +191,10 @@ public final class CardEmulationManager { * Allows an application to query whether a service is currently * the default service to handle a card emulation category. * + * <p>Note that if {@link #getSelectionModeForCategory(String)} + * returns {@link #SELECTION_MODE_ALWAYS_ASK}, this method will always + * return false. + * * @param service The ComponentName of the service * @param category The category * @return whether service is currently the default service for the category. @@ -147,6 +205,10 @@ public final class CardEmulationManager { } catch (RemoteException e) { // Try one more time recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } try { return sService.isDefaultServiceForCategory(UserHandle.myUserId(), service, category); @@ -186,6 +248,33 @@ public final class CardEmulationManager { } /** + * Returns the application selection mode for the passed in category. + * Valid return values are: + * <p>{@link #SELECTION_MODE_PREFER_DEFAULT} the user has requested a default + * application for this category, which will be preferred. + * <p>{@link #SELECTION_MODE_ALWAYS_ASK} the user has requested to be asked + * every time what app he would like to use in this category. + * <p>{@link #SELECTION_MODE_ASK_IF_CONFLICT} the user will only be asked + * to pick a service if there is a conflict. + * @param category The category, for example {@link #CATEGORY_PAYMENT} + * @return + */ + public int getSelectionModeForCategory(String category) { + if (CATEGORY_PAYMENT.equals(category)) { + String defaultComponent = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT); + if (defaultComponent != null) { + return SELECTION_MODE_PREFER_DEFAULT; + } else { + return SELECTION_MODE_ALWAYS_ASK; + } + } else { + // All other categories are in "only ask if conflict" mode + return SELECTION_MODE_ASK_IF_CONFLICT; + } + } + + /** * @hide */ public boolean setDefaultServiceForCategory(ComponentName service, String category) { diff --git a/core/java/android/nfc/cardemulation/HostApduService.java b/core/java/android/nfc/cardemulation/HostApduService.java index cdc4adb..174acc0 100644 --- a/core/java/android/nfc/cardemulation/HostApduService.java +++ b/core/java/android/nfc/cardemulation/HostApduService.java @@ -4,13 +4,11 @@ import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.Service; import android.content.Intent; -import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; -import android.os.Parcel; import android.os.RemoteException; import android.util.Log; @@ -42,13 +40,31 @@ public abstract class HostApduService extends Service { */ @SdkConstant(SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = + "android.nfc.cardemulation.action.HOST_APDU_SERVICE"; + + /** + * The name of the meta-data element that contains + * more information about this service. + */ + public static final String SERVICE_META_DATA = + "android.nfc.cardemulation.host_apdu_service"; + + /** + * The {@link Intent} that must be declared as handled by the service. + * TODO Remove + * @hide + */ + public static final String OLD_SERVICE_INTERFACE = "android.nfc.HostApduService"; /** * The name of the meta-data element that contains * more information about this service. + * + * TODO Remove + * @hide */ - public static final String SERVICE_META_DATA = "android.nfc.HostApduService"; + public static final String OLD_SERVICE_META_DATA = "android.nfc.HostApduService"; /** * Reason for {@link #onDeactivated(int)}. @@ -65,7 +81,7 @@ public abstract class HostApduService extends Service { * currently active on the logical channel). * * <p>Note that this next AID may still be resolved to this - * service, in which case {@link #processCommandApdu(byte[], int)} + * service, in which case {@link #processCommandApdu(byte[], Bundle)} * will be called again. */ public static final int DEACTIVATION_DESELECTED = 1; @@ -133,7 +149,7 @@ public abstract class HostApduService extends Service { byte[] apdu = dataBundle.getByteArray(KEY_DATA); if (apdu != null) { - byte[] responseApdu = processCommandApdu(apdu, 0); + byte[] responseApdu = processCommandApdu(apdu, null); if (responseApdu != null) { if (mNfcService == null) { Log.e(TAG, "Response not sent; service was deactivated."); @@ -232,7 +248,7 @@ public abstract class HostApduService extends Service { * transaction. * * <p>Note: this method may be called anywhere between - * the first {@link #processCommandApdu(byte[], int)} + * the first {@link #processCommandApdu(byte[], Bundle)} * call and a {@link #onDeactivated(int)} call. */ public final void notifyUnhandled() { @@ -292,8 +308,11 @@ public abstract class HostApduService extends Service { * @param flags * @return a byte-array containing the response APDU, or null if no * response APDU can be sent at this point. + * @hide */ - public abstract byte[] processCommandApdu(byte[] commandApdu, int flags); + public byte[] processCommandApdu(byte[] commandApdu, int flags) { + return null; + } /** * This method will be called in two possible scenarios: diff --git a/core/java/android/nfc/cardemulation/OffHostApduService.java b/core/java/android/nfc/cardemulation/OffHostApduService.java index 79599db..15f63f9 100644 --- a/core/java/android/nfc/cardemulation/OffHostApduService.java +++ b/core/java/android/nfc/cardemulation/OffHostApduService.java @@ -42,13 +42,14 @@ public abstract class OffHostApduService extends Service { */ @SdkConstant(SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = - "android.nfc.OffHostApduService"; + "android.nfc.cardemulation.action.OFF_HOST_APDU_SERVICE"; /** * The name of the meta-data element that contains * more information about this service. */ - public static final String SERVICE_META_DATA = "android.nfc.OffHostApduService"; + public static final String SERVICE_META_DATA = + "android.nfc.cardemulation.off_host_apdu_service"; /** * The Android platform itself will not bind to this service, diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index f474504..32b1b60 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -22,6 +22,7 @@ import android.util.SparseArray; import java.io.Serializable; import java.util.ArrayList; +import java.util.List; import java.util.Set; /** @@ -545,6 +546,13 @@ public final class Bundle implements Parcelable, Cloneable { mFdsKnown = false; } + /** {@hide} */ + public void putParcelableList(String key, List<? extends Parcelable> value) { + unparcel(); + mMap.put(key, value); + mFdsKnown = false; + } + /** * Inserts a SparceArray of Parcelable values into the mapping of this * Bundle, replacing any existing value for the given key. Either key diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 0a6db25..60ce132 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -639,16 +639,19 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo * * @hide */ - public static void startMethodTracingDdms(int bufferSize, int flags) { - VMDebug.startMethodTracingDdms(bufferSize, flags); + public static void startMethodTracingDdms(int bufferSize, int flags, + boolean samplingEnabled, int intervalUs) { + VMDebug.startMethodTracingDdms(bufferSize, flags, samplingEnabled, intervalUs); } /** - * Determine whether method tracing is currently active. + * Determine whether method tracing is currently active and what type is + * active. + * * @hide */ - public static boolean isMethodTracingActive() { - return VMDebug.isMethodTracingActive(); + public static int getMethodTracingMode() { + return VMDebug.getMethodTracingMode(); } /** diff --git a/core/java/android/print/IPrintManager.aidl b/core/java/android/print/IPrintManager.aidl index 3bfd9a1..fb6bb2e 100644 --- a/core/java/android/print/IPrintManager.aidl +++ b/core/java/android/print/IPrintManager.aidl @@ -41,7 +41,9 @@ interface IPrintManager { void startPrinterDiscovery(in IPrinterDiscoveryObserver observer, in List<PrinterId> priorityList, int userId); void stopPrinterDiscovery(in IPrinterDiscoveryObserver observer, int userId); - void requestPrinterUpdate(in PrinterId printerId, int userId); + void validatePrinters(in List<PrinterId> printerIds, int userId); + void startPrinterStateTracking(in PrinterId printerId, int userId); + void stopPrinterStateTracking(in PrinterId printerId, int userId); void destroyPrinterDiscoverySession(in IPrinterDiscoveryObserver observer, int userId); } diff --git a/core/java/android/print/PageRange.java b/core/java/android/print/PageRange.java index ba455f6..cdcd0c7 100644 --- a/core/java/android/print/PageRange.java +++ b/core/java/android/print/PageRange.java @@ -42,8 +42,6 @@ public final class PageRange implements Parcelable { * @throws IllegalArgumentException If start is less than zero. * @throws IllegalArgumentException If end is less than zero. * @throws IllegalArgumentException If start greater than end. - * - * @hide */ public PageRange(int start, int end) { if (start < 0) { diff --git a/core/java/android/print/PrintAttributes.java b/core/java/android/print/PrintAttributes.java index a902c72..caa10ae 100644 --- a/core/java/android/print/PrintAttributes.java +++ b/core/java/android/print/PrintAttributes.java @@ -50,9 +50,15 @@ public final class PrintAttributes implements Parcelable { /** Fitting mode: No fitting. */ - public static final int FITTING_MODE_NONE = 0x00000001; - /** Fitting mode: Fit the content to the page. */ - public static final int FITTING_MODE_FIT_TO_PAGE = 0x00000002; + public static final int FITTING_MODE_NONE = 1 << 0; + /** Fitting mode: Scale the content to fit in the page + * without cropping it in any dimension. */ + public static final int FITTING_MODE_SCALE_TO_FIT = 1 << 1; + /** + * Fitting mode: Uniformly scale the content to fill the entire page + * potentially cropping the content if it overflows in one dimension. + */ + public static final int FITTING_MODE_SCALE_TO_FILL = 1 << 2; private static final int VALID_DUPLEX_MODES = @@ -62,7 +68,7 @@ public final class PrintAttributes implements Parcelable { COLOR_MODE_MONOCHROME | COLOR_MODE_COLOR; private static final int VALID_FITTING_MODES = - FITTING_MODE_NONE | FITTING_MODE_FIT_TO_PAGE; + FITTING_MODE_NONE | FITTING_MODE_SCALE_TO_FIT | FITTING_MODE_SCALE_TO_FILL; private static final int VALID_ORIENTATIONS = ORIENTATION_PORTRAIT | ORIENTATION_LANDSCAPE; @@ -252,7 +258,8 @@ public final class PrintAttributes implements Parcelable { * @return The fitting mode or zero if not set. * * @see #FITTING_MODE_NONE - * @see #FITTING_MODE_FIT_TO_PAGE + * @see #FITTING_MODE_SCALE_TO_FILL + * @see #FITTING_MODE_SCALE_TO_FIT */ public int getFittingMode() { return mFittingMode; @@ -264,12 +271,13 @@ public final class PrintAttributes implements Parcelable { * @param The fitting mode. * * @see #FITTING_MODE_NONE - * @see #FITTING_MODE_FIT_TO_PAGE + * @see #FITTING_MODE_SCALE_TO_FILL + * @see #FITTING_MODE_SCALE_TO_FIT * * @hide */ public void setFittingMode(int fittingMode) { - enfoceValidFittingMode(fittingMode); + enforceValidFittingMode(fittingMode); mFittingMode = fittingMode; } @@ -1220,6 +1228,8 @@ public final class PrintAttributes implements Parcelable { * This class specifies content margins. */ public static final class Margins { + public static final Margins NO_MARGINS = new Margins(0, 0, 0, 0); + private final int mLeftMils; private final int mTopMils; private final int mRightMils; @@ -1232,24 +1242,13 @@ public final class PrintAttributes implements Parcelable { * @param topMils The top margin in mils (thousands of an inch). * @param rightMils The right margin in mils (thousands of an inch). * @param bottomMils The bottom margin in mils (thousands of an inch). - * - * @throws IllegalArgumentException If the leftMils is less than zero. - * @throws IllegalArgumentException If the topMils is less than zero. - * @throws IllegalArgumentException If the rightMils is less than zero. - * @throws IllegalArgumentException If the bottomMils is less than zero. */ public Margins(int leftMils, int topMils, int rightMils, int bottomMils) { - if (leftMils < 0) { - throw new IllegalArgumentException("leftMils cannot be less than zero."); + if (leftMils > rightMils) { + throw new IllegalArgumentException("leftMils cannot be less than rightMils."); } - if (topMils < 0) { - throw new IllegalArgumentException("topMils cannot be less than zero."); - } - if (rightMils < 0) { - throw new IllegalArgumentException("rightMils cannot be less than zero."); - } - if (bottomMils < 0) { - throw new IllegalArgumentException("bottomMils cannot be less than zero."); + if (topMils > bottomMils) { + throw new IllegalArgumentException("topMils cannot be less than bottomMils."); } mTopMils = topMils; mLeftMils = leftMils; @@ -1504,8 +1503,11 @@ public final class PrintAttributes implements Parcelable { case FITTING_MODE_NONE: { return "FITTING_MODE_NONE"; } - case FITTING_MODE_FIT_TO_PAGE: { - return "FITTING_MODE_FIT_TO_PAGE"; + case FITTING_MODE_SCALE_TO_FIT: { + return "FITTING_MODE_SCALE_TO_FIT"; + } + case FITTING_MODE_SCALE_TO_FILL: { + return "FITTING_MODE_SCALE_TO_FILL"; } default: return "FITTING_MODE_UNKNOWN"; @@ -1513,25 +1515,25 @@ public final class PrintAttributes implements Parcelable { } static void enforceValidDuplexMode(int duplexMode) { - if ((duplexMode & VALID_DUPLEX_MODES) == 0) { + if ((duplexMode & VALID_DUPLEX_MODES) == 0 && Integer.bitCount(duplexMode) == 1) { throw new IllegalArgumentException("invalid duplex mode: " + duplexMode); } } static void enforceValidColorMode(int colorMode) { - if ((colorMode & VALID_COLOR_MODES) == 0) { + if ((colorMode & VALID_COLOR_MODES) == 0 && Integer.bitCount(colorMode) == 1) { throw new IllegalArgumentException("invalid color mode: " + colorMode); } } - static void enfoceValidFittingMode(int fittingMode) { - if ((fittingMode & VALID_FITTING_MODES) == 0) { + static void enforceValidFittingMode(int fittingMode) { + if ((fittingMode & VALID_FITTING_MODES) == 0 && Integer.bitCount(fittingMode) == 1) { throw new IllegalArgumentException("invalid fitting mode: " + fittingMode); } } static void enforceValidOrientation(int orientation) { - if ((orientation & VALID_ORIENTATIONS) == 0) { + if ((orientation & VALID_ORIENTATIONS) == 0 && Integer.bitCount(orientation) == 1) { throw new IllegalArgumentException("invalid orientation: " + orientation); } } diff --git a/core/java/android/print/PrintDocumentAdapter.java b/core/java/android/print/PrintDocumentAdapter.java index d320226..33b4aad 100644 --- a/core/java/android/print/PrintDocumentAdapter.java +++ b/core/java/android/print/PrintDocumentAdapter.java @@ -18,8 +18,8 @@ package android.print; import android.os.Bundle; import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; -import java.io.FileDescriptor; import java.util.List; /** @@ -41,7 +41,7 @@ import java.util.List; * <li> * After every call to {@link #onLayout(PrintAttributes, PrintAttributes, * CancellationSignal, LayoutResultCallback, Bundle)}, you may get a call to - * {@link #onWrite(PageRange[], FileDescriptor, CancellationSignal, WriteResultCallback)} + * {@link #onWrite(PageRange[], ParcelFileDescriptor, CancellationSignal, WriteResultCallback)} * asking you to write a PDF file with the content for specific pages. * </li> * <li> @@ -64,7 +64,7 @@ import java.util.List; * PrintAttributes, CancellationSignal, LayoutResultCallback, Bundle)} on * the UI thread (assuming onStart initializes resources needed for layout). * This will ensure that the UI does not change while you are laying out the - * printed content. Then you can handle {@link #onWrite(PageRange[], FileDescriptor, + * printed content. Then you can handle {@link #onWrite(PageRange[], ParcelFileDescriptor, * CancellationSignal, WriteResultCallback)} and {@link #onFinish()} on another * thread. This will ensure that the UI is frozen for the minimal amount of * time. Also this assumes that you will generate the printed content in @@ -101,6 +101,28 @@ public abstract class PrintDocumentAdapter { * LayoutResultCallback#onLayoutFailed(CharSequence)}, if an error occurred. * </p> * <p> + * When doing a layout you may satisfy some of the constraints in the print + * attributes such as applying the appropriate fitting, emitting content in the + * requested orientation, using the specified margins, generating content with + * the desired color mode, producing output with the given media size. Ideally, + * you will satisfy all of these constraints. It is important that if you + * satisfy a given constraint, you update the {@link PrintDocumentInfo} that + * is returned in the given {@link LayoutResultCallback}. This way the printer + * will have more accurate information about the content, thus producing a + * better output. For example, assume that your application is printing + * an image and the print attributes request landscape and fitting mode scale + * to fill. The result of this operation should be the entire media is filled + * and the content is rotated ninety degrees. In this case it is beneficial + * you do the rotation and select a higher resolution image to utilize + * the wider media (the height is now the width), rather to use a lower + * resolution image that is later stretched by the printer. If you applied + * the rotation you have to update the returned print document info to + * reflect that the content is already in landscape by calling + * {@link PrintDocumentInfo.Builder#setOrientation(int)} with {@link + * PrintAttributes#ORIENTATION_LANDSCAPE}. In this case the printer does not + * have to rotate the content. + * </p> + * <p> * <strong>Note:</strong> If the content is large and a layout will be * performed, it is a good practice to schedule the work on a dedicated * thread and register an observer in the provided {@link @@ -128,10 +150,10 @@ public abstract class PrintDocumentAdapter { * from of a PDF file to the given file descriptor. This method is invoked * on the main thread. *<p> - * After you are done writing, you should <strong>not</strong> close the - * file descriptor, rather you must invoke: {@link WriteResultCallback - * #onWriteFinished(List)}, if writing completed successfully; or {@link - * WriteResultCallback#onWriteFailed(CharSequence)}, if an error occurred. + * After you are done writing, you should close the file descriptor and + * invoke {@link WriteResultCallback #onWriteFinished(List)}, if writing + * completed successfully; or {@link WriteResultCallback#onWriteFailed( + * CharSequence)}, if an error occurred. * </p> * <p> * <strong>Note:</strong> If the printed content is large, it is a good @@ -149,7 +171,7 @@ public abstract class PrintDocumentAdapter { * @see WriteResultCallback * @see CancellationSignal */ - public abstract void onWrite(PageRange[] pages, FileDescriptor destination, + public abstract void onWrite(PageRange[] pages, ParcelFileDescriptor destination, CancellationSignal cancellationSignal, WriteResultCallback callback); /** @@ -163,7 +185,7 @@ public abstract class PrintDocumentAdapter { /** * Base class for implementing a callback for the result of {@link - * PrintDocumentAdapter#onWrite(PageRange[], FileDescriptor, CancellationSignal, + * PrintDocumentAdapter#onWrite(PageRange[], ParcelFileDescriptor, CancellationSignal, * WriteResultCallback)}. */ public static abstract class WriteResultCallback { diff --git a/core/java/android/print/PrintDocumentInfo.java b/core/java/android/print/PrintDocumentInfo.java index 653ad4b..f2b91ae 100644 --- a/core/java/android/print/PrintDocumentInfo.java +++ b/core/java/android/print/PrintDocumentInfo.java @@ -18,6 +18,8 @@ package android.print; import android.os.Parcel; import android.os.Parcelable; +import android.print.PrintAttributes.Margins; +import android.print.PrintAttributes.MediaSize; import android.text.TextUtils; /** @@ -26,12 +28,17 @@ import android.text.TextUtils; public final class PrintDocumentInfo implements Parcelable { /** - * Constant for unknown page count (default). + * Constant for an unknown media size. + */ + public static final MediaSize MEDIA_SIZE_UNKNOWN = new MediaSize("Unknown", "Unknown", 1, 1); + + /** + * Constant for unknown page count.. */ public static final int PAGE_COUNT_UNKNOWN = -1; /** - * Content type: unknown (default). + * Content type: unknown. */ public static final int CONTENT_TYPE_UNKNOWN = -1; @@ -48,13 +55,18 @@ public final class PrintDocumentInfo implements Parcelable { private String mName; private int mPageCount; private int mContentType; + private int mOrientation; + private int mFittingMode; + private int mColorMode; + private Margins mMargins; + private MediaSize mMediaSize; + private long mDataSize; /** * Creates a new instance. */ private PrintDocumentInfo() { - mPageCount = PAGE_COUNT_UNKNOWN; - mContentType = CONTENT_TYPE_UNKNOWN; + /* do nothing */ } /** @@ -66,6 +78,12 @@ public final class PrintDocumentInfo implements Parcelable { mName = prototype.mName; mPageCount = prototype.mPageCount; mContentType = prototype.mContentType; + mOrientation = prototype.mOrientation; + mFittingMode = prototype.mFittingMode; + mColorMode = prototype.mColorMode; + mMargins = prototype.mMargins; + mMediaSize = prototype.mMediaSize; + mDataSize = prototype.mDataSize; } /** @@ -77,6 +95,12 @@ public final class PrintDocumentInfo implements Parcelable { mName = parcel.readString(); mPageCount = parcel.readInt(); mContentType = parcel.readInt(); + mOrientation = parcel.readInt(); + mFittingMode = parcel.readInt(); + mColorMode = parcel.readInt(); + mMargins = Margins.createFromParcel(parcel); + mMediaSize = MediaSize.createFromParcel(parcel); + mDataSize = parcel.readLong(); } /** @@ -112,6 +136,81 @@ public final class PrintDocumentInfo implements Parcelable { return mContentType; } + /** + * Gets the document orientation. + * + * @return The orientation. + * + * @see PrintAttributes#ORIENTATION_PORTRAIT PrintAttributes.ORIENTATION_PORTRAIT + * @see PrintAttributes#ORIENTATION_LANDSCAPE PrintAttributes.ORIENTATION_LANDSCAPE + */ + public int getOrientation() { + return mOrientation; + } + + /** + * Gets the document fitting mode. + * + * @return The fitting mode. + * + * @see PrintAttributes#FITTING_MODE_NONE PrintAttributes.FITTING_MODE_NONE + * @see PrintAttributes#FITTING_MODE_SCALE_TO_FILL PrintAttributes.FITTING_MODE_SCALE_TO_FILL + * @see PrintAttributes#FITTING_MODE_SCALE_TO_FIT PrintAttributes.FITTING_MODE_SCALE_TO_FIT + */ + public int getFittingMode() { + return mFittingMode; + } + + /** + * Gets document color mode. + * + * @return The color mode. + * + * @see PrintAttributes#COLOR_MODE_COLOR PrintAttributes.COLOR_MODE_COLOR + * @see PrintAttributes#COLOR_MODE_MONOCHROME PrintAttributes.COLOR_MODE_MONOCHROME + */ + public int getColorMode() { + return mColorMode; + } + + /** + * Gets the document margins. + * + * @return The margins. + */ + public Margins getMargins() { + return mMargins; + } + + /** + * Gets the media size. + * + * @return The media size. + */ + public MediaSize getMediaSize() { + return mMediaSize; + } + + /** + * Gets the document data size in bytes. + * + * @return The data size. + */ + public long getDataSize() { + return mDataSize; + } + + /** + * Sets the document data size in bytes. + * + * @param dataSize The data size. + * + * @hide + */ + public void setDataSize(long dataSize) { + mDataSize = dataSize; + } + @Override public int describeContents() { return 0; @@ -122,6 +221,12 @@ public final class PrintDocumentInfo implements Parcelable { parcel.writeString(mName); parcel.writeInt(mPageCount); parcel.writeInt(mContentType); + parcel.writeInt(mOrientation); + parcel.writeInt(mFittingMode); + parcel.writeInt(mColorMode); + mMargins.writeToParcel(parcel); + mMediaSize.writeToParcel(parcel); + parcel.writeLong(mDataSize); } @Override @@ -131,6 +236,13 @@ public final class PrintDocumentInfo implements Parcelable { result = prime * result + ((mName != null) ? mName.hashCode() : 0); result = prime * result + mContentType; result = prime * result + mPageCount; + result = prime * result + mOrientation; + result = prime * result + mFittingMode; + result = prime * result + mColorMode; + result = prime * result + (mMargins != null ? mMargins.hashCode() : 0); + result = prime * result + (mMediaSize != null ? mMediaSize.hashCode() : 0); + result = prime * result + (int) mDataSize; + result = prime * result + (int) mDataSize >> 32; return result; } @@ -155,6 +267,32 @@ public final class PrintDocumentInfo implements Parcelable { if (mPageCount != other.mPageCount) { return false; } + if (mOrientation != other.mOrientation) { + return false; + } + if (mFittingMode != other.mFittingMode) { + return false; + } + if (mColorMode != other.mColorMode) { + return false; + } + if (mMargins == null) { + if (other.mMargins != null) { + return false; + } + } else if (!mMargins.equals(other.mMargins)) { + return false; + } + if (mMediaSize == null) { + if (other.mMediaSize != null) { + return false; + } + } else if (!mMediaSize.equals(other.mMediaSize)) { + return false; + } + if (mDataSize != other.mDataSize) { + return false; + } return true; } @@ -165,6 +303,12 @@ public final class PrintDocumentInfo implements Parcelable { builder.append("name=").append(mName); builder.append(", pageCount=").append(mPageCount); builder.append(", contentType=").append(contentTyepToString(mContentType)); + builder.append(", orientation=").append(PrintAttributes.orientationToString(mOrientation)); + builder.append(", fittingMode=").append(PrintAttributes.fittingModeToString(mFittingMode)); + builder.append(", colorMode=").append(PrintAttributes.colorModeToString(mColorMode)); + builder.append(", margins=").append(mMargins); + builder.append(", mediaSize=").append(mMediaSize); + builder.append(", size=").append(mDataSize); builder.append("}"); return builder.toString(); } @@ -191,21 +335,62 @@ public final class PrintDocumentInfo implements Parcelable { /** * Constructor. + * <p> + * The values of the relevant properties are initialized from the + * provided print attributes. For example, the orientation is set + * to be the same as the orientation returned by calling {@link + * PrintAttributes#getOrientation() PrintAttributes.getOrientation()}. + * </p> * * @param name The document name. Cannot be empty. + * @param attributes Print attributes. Cannot be null. * * @throws IllegalArgumentException If the name is empty. */ + public Builder(String name, PrintAttributes attributes) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("name cannot be empty"); + } + if (attributes == null) { + throw new IllegalArgumentException("attributes cannot be null"); + } + mPrototype = new PrintDocumentInfo(); + mPrototype.mName = name; + mPrototype.mOrientation = attributes.getOrientation(); + mPrototype.mFittingMode = attributes.getFittingMode(); + mPrototype.mColorMode = attributes.getColorMode(); + mPrototype.mMargins = attributes.getMargins(); + mPrototype.mMediaSize = attributes.getMediaSize(); + } + + /** + * Constructor. + * <p> + * The values of the relevant properties are initialized with default + * values. Please refer to the documentation of the individual setters + * for information about the default values. + * </p> + * + * @param name The document name. Cannot be empty. + */ public Builder(String name) { if (TextUtils.isEmpty(name)) { throw new IllegalArgumentException("name cannot be empty"); } mPrototype = new PrintDocumentInfo(); mPrototype.mName = name; + mPrototype.mOrientation = PrintAttributes.ORIENTATION_PORTRAIT; + mPrototype.mFittingMode = PrintAttributes.FITTING_MODE_NONE; + mPrototype.mColorMode = PrintAttributes.COLOR_MODE_COLOR; + mPrototype.mMargins = Margins.NO_MARGINS; + mPrototype.mMediaSize = MEDIA_SIZE_UNKNOWN; } /** * Sets the total number of pages. + * <p> + * <strong>Default: </strong> {@link #PAGE_COUNT_UNKNOWN} + * </p> * * @param pageCount The number of pages. Must be greater than * or equal to zero or {@link PrintDocumentInfo#PAGE_COUNT_UNKNOWN}. @@ -222,6 +407,9 @@ public final class PrintDocumentInfo implements Parcelable { /** * Sets the content type. + * <p> + * <strong>Default: </strong> {@link #CONTENT_TYPE_UNKNOWN} + * </p> * * @param type The content type. * @@ -235,6 +423,95 @@ public final class PrintDocumentInfo implements Parcelable { } /** + * Sets the orientation. + * <p> + * <strong>Default: </strong> {@link PrintAttributes#ORIENTATION_PORTRAIT + * PrintAttributes.ORIENTATION_PORTRAIT} + * </p> + * + * @param orientation The orientation. + * + * @see PrintAttributes#ORIENTATION_PORTRAIT PrintAttributes.ORIENTATION_PORTRAIT + * @see PrintAttributes#ORIENTATION_LANDSCAPE PrintAttributes.ORIENTATION_LANDSCAPE + */ + public Builder setOrientation(int orientation) { + PrintAttributes.enforceValidOrientation(orientation); + mPrototype.mOrientation = orientation; + return this; + } + + /** + * Sets the content fitting mode. + * <p> + * <strong>Default: </strong> {@link PrintAttributes#FITTING_MODE_NONE + * PrintAttributes.FITTING_MODE_NONE} + * </p> + * + * @param fittingMode The fitting mode. + * + * @see PrintAttributes#FITTING_MODE_NONE PrintAttributes.FITTING_MODE_NONE + * @see PrintAttributes#FITTING_MODE_SCALE_TO_FILL PrintAttributes.FITTING_MODE_SCALE_TO_FILL + * @see PrintAttributes#FITTING_MODE_SCALE_TO_FIT PrintAttributes.FITTING_MODE_SCALE_TO_FIT + */ + public Builder setFittingMode(int fittingMode) { + PrintAttributes.enforceValidFittingMode(fittingMode); + mPrototype.mFittingMode = fittingMode; + return this; + } + + /** + * Sets the content color mode. + * <p> + * <strong>Default: </strong> {@link PrintAttributes#COLOR_MODE_COLOR + * PrintAttributes.COLOR_MODE_COLOR} + * </p> + * + * @param colorMode The color mode. + * + * @see PrintAttributes#COLOR_MODE_COLOR PrintAttributes.COLOR_MODE_COLOR + * @see PrintAttributes#COLOR_MODE_MONOCHROME PrintAttributes.COLOR_MODE_MONOCHROME + */ + public Builder setColorMode(int colorMode) { + PrintAttributes.enforceValidColorMode(colorMode); + mPrototype.mColorMode = colorMode; + return this; + } + + /** + * Sets the document margins. + * <p> + * <strong>Default: </strong> {@link PrintAttributes.Margins#NO_MARGINS Margins.NO_MARGINS} + * </p> + * + * @param margins The margins. Cannot be null. + */ + public Builder setMargins(Margins margins) { + if (margins == null) { + throw new IllegalArgumentException("margins cannot be null"); + } + mPrototype.mMargins = margins; + return this; + } + + /** + * Sets the document media size. + * <p> + * <strong>Default: </strong>#MEDIA_SIZE_UNKNOWN + * </p> + * + * @param mediaSize The media size. Cannot be null. + * + * @see #MEDIA_SIZE_UNKNOWN + */ + public Builder setMediaSize(MediaSize mediaSize) { + if (mediaSize == null) { + throw new IllegalArgumentException("media size cannot be null"); + } + mPrototype.mMediaSize = mediaSize; + return this; + } + + /** * Creates a new {@link PrintDocumentInfo} instance. * * @return The new instance. diff --git a/core/java/android/print/PrintFileDocumentAdapter.java b/core/java/android/print/PrintFileDocumentAdapter.java index dbc8b6f..b905396 100644 --- a/core/java/android/print/PrintFileDocumentAdapter.java +++ b/core/java/android/print/PrintFileDocumentAdapter.java @@ -21,6 +21,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.CancellationSignal; import android.os.CancellationSignal.OnCancelListener; +import android.os.ParcelFileDescriptor; import android.util.Log; import com.android.internal.R; @@ -28,7 +29,6 @@ import com.android.internal.R; import libcore.io.IoUtils; import java.io.File; -import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -81,7 +81,7 @@ public class PrintFileDocumentAdapter extends PrintDocumentAdapter { } @Override - public void onWrite(PageRange[] pages, FileDescriptor destination, + public void onWrite(PageRange[] pages, ParcelFileDescriptor destination, CancellationSignal cancellationSignal, WriteResultCallback callback) { mWriteFileAsyncTask = new WriteFileAsyncTask(destination, cancellationSignal, callback); mWriteFileAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, @@ -90,13 +90,13 @@ public class PrintFileDocumentAdapter extends PrintDocumentAdapter { private final class WriteFileAsyncTask extends AsyncTask<Void, Void, Void> { - private final FileDescriptor mDestination; + private final ParcelFileDescriptor mDestination; private final WriteResultCallback mResultCallback; private final CancellationSignal mCancellationSignal; - public WriteFileAsyncTask(FileDescriptor destination, + public WriteFileAsyncTask(ParcelFileDescriptor destination, CancellationSignal cancellationSignal, WriteResultCallback callback) { mDestination = destination; mResultCallback = callback; @@ -112,7 +112,7 @@ public class PrintFileDocumentAdapter extends PrintDocumentAdapter { @Override protected Void doInBackground(Void... params) { InputStream in = null; - OutputStream out = new FileOutputStream(mDestination); + OutputStream out = new FileOutputStream(mDestination.getFileDescriptor()); final byte[] buffer = new byte[8192]; try { in = new FileInputStream(mFile); diff --git a/core/java/android/print/PrintJobInfo.java b/core/java/android/print/PrintJobInfo.java index 602f3c1..b919ad6 100644 --- a/core/java/android/print/PrintJobInfo.java +++ b/core/java/android/print/PrintJobInfo.java @@ -44,6 +44,13 @@ public final class PrintJobInfo implements Parcelable { public static final int STATE_ANY_VISIBLE_TO_CLIENTS = -2; /** + * Constant for matching any active print job state. + * + * @hide + */ + public static final int STATE_ANY_ACTIVE = -3; + + /** * Print job state: The print job is being created but not yet * ready to be printed. * <p> @@ -55,7 +62,7 @@ public final class PrintJobInfo implements Parcelable { public static final int STATE_CREATED = 1; /** - * Print job status: The print jobs is created, it is ready + * Print job state: The print jobs is created, it is ready * to be printed and should be processed. * <p> * Next valid states: {@link #STATE_STARTED}, {@link #STATE_FAILED}, @@ -65,40 +72,49 @@ public final class PrintJobInfo implements Parcelable { public static final int STATE_QUEUED = 2; /** - * Print job status: The print job is being printed. + * Print job state: The print job is being printed. * <p> * Next valid states: {@link #STATE_COMPLETED}, {@link #STATE_FAILED}, - * {@link #STATE_CANCELED} + * {@link #STATE_CANCELED}, {@link #STATE_BLOCKED} * </p> */ public static final int STATE_STARTED = 3; /** - * Print job status: The print job was successfully printed. + * Print job state: The print job is blocked. + * <p> + * Next valid states: {@link #STATE_FAILED}, {@link #STATE_CANCELED}, + * {@link #STATE_STARTED} + * </p> + */ + public static final int STATE_BLOCKED = 4; + + /** + * Print job state: The print job was successfully printed. * This is a terminal state. * <p> * Next valid states: None * </p> */ - public static final int STATE_COMPLETED = 4; + public static final int STATE_COMPLETED = 5; /** - * Print job status: The print job was printing but printing failed. + * Print job state: The print job was printing but printing failed. * This is a terminal state. * <p> * Next valid states: None * </p> */ - public static final int STATE_FAILED = 5; + public static final int STATE_FAILED = 6; /** - * Print job status: The print job was canceled. + * Print job state: The print job was canceled. * This is a terminal state. * <p> * Next valid states: None * </p> */ - public static final int STATE_CANCELED = 6; + public static final int STATE_CANCELED = 7; /** The unique print job id. */ private int mId; @@ -127,8 +143,8 @@ public final class PrintJobInfo implements Parcelable { /** How many copies to print. */ private int mCopies; - /** Failure reason if this job failed. */ - private String mFailureReason; + /** Reason for the print job being in its current state. */ + private String mStateReason; /** The pages to print */ private PageRange[] mPageRanges; @@ -155,7 +171,7 @@ public final class PrintJobInfo implements Parcelable { mUserId = other.mUserId; mTag = other.mTag; mCopies = other.mCopies; - mFailureReason = other.mFailureReason; + mStateReason = other.mStateReason; mPageRanges = other.mPageRanges; mAttributes = other.mAttributes; mDocumentInfo = other.mDocumentInfo; @@ -171,7 +187,7 @@ public final class PrintJobInfo implements Parcelable { mUserId = parcel.readInt(); mTag = parcel.readString(); mCopies = parcel.readInt(); - mFailureReason = parcel.readString(); + mStateReason = parcel.readString(); if (parcel.readInt() == 1) { Parcelable[] parcelables = parcel.readParcelableArray(null); mPageRanges = new PageRange[parcelables.length]; @@ -377,25 +393,27 @@ public final class PrintJobInfo implements Parcelable { } /** - * The failure reason if this print job failed. + * Gets the reason for the print job being in the current state. * - * @return The failure reason. + * @return The reason, or null if there is no reason or the + * reason is unknown. * * @hide */ - public String getFailureReason() { - return mFailureReason; + public String getStateReason() { + return mStateReason; } /** - * The failure reason if this print job failed. + * Sets the reason for the print job being in the current state. * - * @param failureReason The failure reason. + * @param stateReason The reason, or null if there is no reason + * or the reason is unknown. * * @hide */ - public void setFailureReason(String failureReason) { - mFailureReason = failureReason; + public void setStateReason(String stateReason) { + mStateReason = stateReason; } /** @@ -476,7 +494,7 @@ public final class PrintJobInfo implements Parcelable { parcel.writeInt(mUserId); parcel.writeString(mTag); parcel.writeInt(mCopies); - parcel.writeString(mFailureReason); + parcel.writeString(mStateReason); if (mPageRanges != null) { parcel.writeInt(1); parcel.writeParcelableArray(mPageRanges, flags); diff --git a/core/java/android/print/PrintManager.java b/core/java/android/print/PrintManager.java index d3e35c3..6e32c05 100644 --- a/core/java/android/print/PrintManager.java +++ b/core/java/android/print/PrintManager.java @@ -36,7 +36,6 @@ import com.android.internal.os.SomeArgs; import libcore.io.IoUtils; import java.io.File; -import java.io.FileDescriptor; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; @@ -163,7 +162,7 @@ public final class PrintManager { * @param pdfFile The PDF file to print. * @param documentInfo Information about the printed document. * @param attributes The default print job attributes. - * @return The created print job. + * @return The created print job on success or null on failure. * * @see PrintJob */ @@ -181,7 +180,7 @@ public final class PrintManager { * @param printJobName A name for the new print job. * @param documentAdapter An adapter that emits the document to print. * @param attributes The default print job attributes. - * @return The created print job. + * @return The created print job on success or null on failure. * * @see PrintJob */ @@ -279,7 +278,7 @@ public final class PrintManager { } SomeArgs args = SomeArgs.obtain(); args.arg1 = pages; - args.arg2 = fd.getFileDescriptor(); + args.arg2 = fd; args.arg3 = callback; args.argi1 = sequence; mHandler.removeMessages(MyHandler.MSG_WRITE); @@ -342,7 +341,7 @@ public final class PrintManager { case MSG_WRITE: { SomeArgs args = (SomeArgs) message.obj; PageRange[] pages = (PageRange[]) args.arg1; - FileDescriptor fd = (FileDescriptor) args.arg2; + ParcelFileDescriptor fd = (ParcelFileDescriptor) args.arg2; IWriteResultCallback callback = (IWriteResultCallback) args.arg3; final int sequence = args.argi1; args.recycle(); @@ -428,12 +427,12 @@ public final class PrintManager { } private final class MyWriteResultCallback extends WriteResultCallback { - private FileDescriptor mFd; + private ParcelFileDescriptor mFd; private int mSequence; private IWriteResultCallback mCallback; public MyWriteResultCallback(IWriteResultCallback callback, - FileDescriptor fd, int sequence) { + ParcelFileDescriptor fd, int sequence) { mFd = fd; mSequence = sequence; mCallback = callback; diff --git a/core/java/android/print/PrinterCapabilitiesInfo.java b/core/java/android/print/PrinterCapabilitiesInfo.java index 70b418c..941e6e1 100644 --- a/core/java/android/print/PrinterCapabilitiesInfo.java +++ b/core/java/android/print/PrinterCapabilitiesInfo.java @@ -863,12 +863,12 @@ public final class PrinterCapabilitiesInfo implements Parcelable { while (currentModes > 0) { final int currentMode = (1 << Integer.numberOfTrailingZeros(currentModes)); currentModes &= ~currentMode; - PrintAttributes.enfoceValidFittingMode(currentMode); + PrintAttributes.enforceValidFittingMode(currentMode); } if ((fittingModes & defaultFittingMode) == 0) { throw new IllegalArgumentException("Default fitting mode not in fiting modes."); } - PrintAttributes.enfoceValidFittingMode(defaultFittingMode); + PrintAttributes.enforceValidFittingMode(defaultFittingMode); mPrototype.mFittingModes = fittingModes; mPrototype.mDefaults[PROPERTY_FITTING_MODE] = defaultFittingMode; return this; diff --git a/core/java/android/print/PrinterDiscoverySession.java b/core/java/android/print/PrinterDiscoverySession.java index 8fbdd9c..46f0bef 100644 --- a/core/java/android/print/PrinterDiscoverySession.java +++ b/core/java/android/print/PrinterDiscoverySession.java @@ -74,6 +74,7 @@ public final class PrinterDiscoverySession { public final void startPrinterDisovery(List<PrinterId> priorityList) { if (isDestroyed()) { Log.w(LOG_TAG, "Ignoring start printers dsicovery - session destroyed"); + return; } if (!mIsPrinterDiscoveryStarted) { mIsPrinterDiscoveryStarted = true; @@ -88,6 +89,7 @@ public final class PrinterDiscoverySession { public final void stopPrinterDiscovery() { if (isDestroyed()) { Log.w(LOG_TAG, "Ignoring stop printers discovery - session destroyed"); + return; } if (mIsPrinterDiscoveryStarted) { mIsPrinterDiscoveryStarted = false; @@ -99,14 +101,39 @@ public final class PrinterDiscoverySession { } } - public final void requestPrinterUpdate(PrinterId printerId) { + public final void startPrinterStateTracking(PrinterId printerId) { + if (isDestroyed()) { + Log.w(LOG_TAG, "Ignoring start printer state tracking - session destroyed"); + return; + } + try { + mPrintManager.startPrinterStateTracking(printerId, mUserId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error starting printer state tracking", re); + } + } + + public final void stopPrinterStateTracking(PrinterId printerId) { if (isDestroyed()) { - Log.w(LOG_TAG, "Ignoring reqeust printer update - session destroyed"); + Log.w(LOG_TAG, "Ignoring stop printer state tracking - session destroyed"); + return; + } + try { + mPrintManager.stopPrinterStateTracking(printerId, mUserId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error stoping printer state tracking", re); + } + } + + public final void validatePrinters(List<PrinterId> printerIds) { + if (isDestroyed()) { + Log.w(LOG_TAG, "Ignoring validate printers - session destroyed"); + return; } try { - mPrintManager.requestPrinterUpdate(printerId, mUserId); + mPrintManager.validatePrinters(printerIds, mUserId); } catch (RemoteException re) { - Log.e(LOG_TAG, "Error requesting printer update", re); + Log.e(LOG_TAG, "Error validating printers", re); } } diff --git a/core/java/android/print/PrinterInfo.java b/core/java/android/print/PrinterInfo.java index 6f567a6..0ea319b 100644 --- a/core/java/android/print/PrinterInfo.java +++ b/core/java/android/print/PrinterInfo.java @@ -25,10 +25,14 @@ import android.text.TextUtils; */ public final class PrinterInfo implements Parcelable { - /** Printer status: the printer is ready to print. */ - public static final int STATUS_READY = 1; + /** Printer status: the printer is idle and ready to print. */ + public static final int STATUS_IDLE = 1; - // TODO: Add printer status constants. + /** Printer status: the printer is busy printing. */ + public static final int STATUS_BUSY = 2; + + /** Printer status: the printer is not available. */ + public static final int STATUS_UNAVAILABLE = 3; private PrinterId mId; @@ -237,6 +241,21 @@ public final class PrinterInfo implements Parcelable { } /** + * Sets the printer status. + * + * @param status The status. + * @return This builder. + * + * @see PrinterInfo#STATUS_IDLE + * @see PrinterInfo#STATUS_BUSY + * @see PrinterInfo#STATUS_UNAVAILABLE + */ + public Builder setStatus(int status) { + mPrototype.mStatus = status; + return this; + } + + /** * Sets the printer name. * * @param name The name. @@ -279,7 +298,9 @@ public final class PrinterInfo implements Parcelable { } private boolean isValidStatus(int status) { - return (status == PrinterInfo.STATUS_READY); + return (status == STATUS_IDLE + || status == STATUS_IDLE + || status == STATUS_UNAVAILABLE); } } diff --git a/core/java/android/print/pdf/PdfDocument.java b/core/java/android/print/pdf/PdfDocument.java index dbd7dd1..a2883cf 100644 --- a/core/java/android/print/pdf/PdfDocument.java +++ b/core/java/android/print/pdf/PdfDocument.java @@ -44,7 +44,7 @@ import java.util.List; * PdfDocument document = PdfDocument.open(); * * // crate a page description - * PageInfo pageInfo = new PageInfo.Builder(new Rect(0, 0, 100, 100), 1, 300).create(); + * PageInfo pageInfo = new PageInfo.Builder(new Rect(0, 0, 100, 100), 1).create(); * * // start a page * Page page = document.startPage(pageInfo); @@ -125,8 +125,7 @@ public final class PdfDocument { throw new IllegalStateException("Previous page not finished!"); } Canvas canvas = new PdfCanvas(nativeCreatePage(pageInfo.mPageSize, - pageInfo.mContentSize, pageInfo.mInitialTransform.native_instance), - pageInfo.mDensity); + pageInfo.mContentSize, pageInfo.mInitialTransform.native_instance)); mCurrentPage = new Page(canvas, pageInfo); return mCurrentPage; } @@ -230,25 +229,14 @@ public final class PdfDocument { private final class PdfCanvas extends Canvas { - public PdfCanvas(int nativeCanvas, int density) { + public PdfCanvas(int nativeCanvas) { super(nativeCanvas); - super.setDensity(density); } @Override public void setBitmap(Bitmap bitmap) { throw new UnsupportedOperationException(); } - - @Override - public void setDensity(int density) { - throw new UnsupportedOperationException(); - } - - @Override - public void setScreenDensity(int density) { - throw new UnsupportedOperationException(); - } } /** @@ -259,7 +247,6 @@ public final class PdfDocument { private Rect mContentSize; private Matrix mInitialTransform; private int mPageNumber; - private int mDensity; /** * Creates a new instance. @@ -269,7 +256,7 @@ public final class PdfDocument { } /** - * Gets the page size in pixels. + * Gets the page size in PostScript points (1/72th of an inch). * * @return The page size. */ @@ -278,7 +265,7 @@ public final class PdfDocument { } /** - * Get the content size in pixels. + * Get the content size in PostScript points (1/72th of an inch). * * @return The content size. */ @@ -307,15 +294,6 @@ public final class PdfDocument { } /** - * Gets the density of the page in DPI. - * - * @return The density. - */ - public int getDesity() { - return mDensity; - } - - /** * Builder for creating a {@link PageInfo}. */ public static final class Builder { @@ -324,11 +302,10 @@ public final class PdfDocument { /** * Creates a new builder with the mandatory page info attributes. * - * @param pageSize The page size in points, <strong>not</strong> dips. + * @param pageSize The page size in PostScript (1/72th of an inch). * @param pageNumber The page number. - * @param density The page density in DPI. */ - public Builder(Rect pageSize, int pageNumber, int density) { + public Builder(Rect pageSize, int pageNumber) { if (pageSize.width() == 0 || pageSize.height() == 0) { throw new IllegalArgumentException("page width and height" + " must be greater than zero!"); @@ -336,16 +313,12 @@ public final class PdfDocument { if (pageNumber < 0) { throw new IllegalArgumentException("pageNumber cannot be less than zero!"); } - if (density <= 0) { - throw new IllegalArgumentException("density must be greater than zero!"); - } mPageInfo.mPageSize = pageSize; mPageInfo.mPageNumber = pageNumber; - mPageInfo.mDensity = density; } /** - * Sets the content size in pixels. + * Sets the content size in PostScript point (1/72th of an inch). * * @param contentSize The content size. */ diff --git a/core/java/android/print/pdf/PrintedPdfDocument.java b/core/java/android/print/pdf/PrintedPdfDocument.java new file mode 100644 index 0000000..bee17ef --- /dev/null +++ b/core/java/android/print/pdf/PrintedPdfDocument.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.print.pdf; + +import android.content.Context; +import android.graphics.Rect; +import android.print.PrintAttributes; +import android.print.PrintAttributes.Margins; +import android.print.PrintAttributes.MediaSize; +import android.print.pdf.PdfDocument; +import android.print.pdf.PdfDocument.Page; +import android.print.pdf.PdfDocument.PageInfo; + +import java.io.OutputStream; +import java.util.List; + +/** + * This class is a helper for printing content to a different media + * size. This class is responsible for computing a correct page size + * given some print constraints, i.e. {@link PrintAttributes}. It is + * an adapter around a {@link PdfDocument}. + */ +public final class PrintedPdfDocument { + private static final int MILS_PER_INCH = 1000; + private static final int POINTS_IN_INCH = 72; + + private final PdfDocument mDocument = PdfDocument.open(); + private final Rect mPageSize = new Rect(); + private final Rect mContentSize = new Rect(); + + /** + * Opens a new document. The document pages are computed based on + * the passes in {@link PrintAttributes}. + * <p> + * <strong>Note:</strong> You must close the document after you are + * done by calling {@link #close()} + * </p> + * + * @param context Context instance for accessing resources. + * @param attributes The print attributes. + * @return The document. + * + * @see #close() + */ + public static PrintedPdfDocument open(Context context, PrintAttributes attributes) { + return new PrintedPdfDocument(context, attributes); + } + + /** + * Creates a new instance. + * + * @param context Context instance for accessing resources and services. + * @param attributes The {@link PrintAttributes} to user. + */ + private PrintedPdfDocument(Context context, PrintAttributes attributes) { + MediaSize mediaSize = attributes.getMediaSize(); + + // Compute the size of the target canvas from the attributes. + final int pageWidth = (int) (((float) mediaSize.getWidthMils() / MILS_PER_INCH) + * POINTS_IN_INCH); + final int pageHeight = (int) (((float) mediaSize.getHeightMils() / MILS_PER_INCH) + * POINTS_IN_INCH); + mPageSize.set(0, 0, pageWidth, pageHeight); + + // Compute the content size from the attributes. + Margins margins = attributes.getMargins(); + final int marginLeft = (int) (((float) margins.getLeftMils() /MILS_PER_INCH) + * POINTS_IN_INCH); + final int marginTop = (int) (((float) margins.getTopMils() / MILS_PER_INCH) + * POINTS_IN_INCH); + final int marginRight = (int) (((float) margins.getRightMils() / MILS_PER_INCH) + * POINTS_IN_INCH); + final int marginBottom = (int) (((float) margins.getBottomMils() / MILS_PER_INCH) + * POINTS_IN_INCH); + mContentSize.set(mPageSize.left + marginLeft, mPageSize.top + marginTop, + mPageSize.right - marginRight, mPageSize.bottom - marginBottom); + } + + /** + * Starts a page using a page size computed from the print attributes + * passed in {@link #open(Context, PrintAttributes)} and the given page + * number to create appropriate {@link PageInfo}. + * <p> + * After the page is created you can draw arbitrary content on the page's + * canvas which you can get by calling {@link Page#getCanvas() Page.getCanvas()}. + * After you are done drawing the content you should finish the page by calling + * {@link #finishPage(Page)}. After the page is finished you should no longer + * access the page or its canvas. + * </p> + * <p> + * <strong>Note:</strong> Do not call this method after {@link #close()}. + * </p> + * + * @param pageNumber The page number. + * @return A blank page. + * + * @see #finishPage(Page) + */ + public Page startPage(int pageNumber) { + PageInfo pageInfo = new PageInfo + .Builder(mPageSize, 0) + .setContentSize(mContentSize) + .create(); + Page page = mDocument.startPage(pageInfo); + return page; + } + + /** + * Finishes a started page. You should always finish the last started page. + * <p> + * <strong>Note:</strong> Do not call this method after {@link #close()}. + * </p> + * + * @param page The page. + * + * @see #startPage(int) + */ + public void finishPage(Page page) { + mDocument.finishPage(page); + } + + /** + * Writes the document to an output stream. + * <p> + * <strong>Note:</strong> Do not call this method after {@link #close()}. + * </p> + * + * @param out The output stream. + */ + public void writeTo(OutputStream out) { + mDocument.writeTo(out); + } + + /** + * Gets the pages of the document. + * + * @return The pages. + */ + public List<PageInfo> getPages() { + return mDocument.getPages(); + } + + /** + * Closes this document. This method should be called after you + * are done working with the document. After this call the document + * is considered closed and none of its methods should be called. + */ + public void close() { + mDocument.close(); + } +} diff --git a/core/java/android/printservice/IPrintService.aidl b/core/java/android/printservice/IPrintService.aidl index 2cee1d8..ee36619 100644 --- a/core/java/android/printservice/IPrintService.aidl +++ b/core/java/android/printservice/IPrintService.aidl @@ -33,6 +33,8 @@ oneway interface IPrintService { void createPrinterDiscoverySession(); void startPrinterDiscovery(in List<PrinterId> priorityList); void stopPrinterDiscovery(); - void requestPrinterUpdate(in PrinterId printerId); + void validatePrinters(in List<PrinterId> printerIds); + void startPrinterStateTracking(in PrinterId printerId); + void stopPrinterStateTracking(in PrinterId printerId); void destroyPrinterDiscoverySession(); } diff --git a/core/java/android/printservice/PrintDocument.java b/core/java/android/printservice/PrintDocument.java index 7437dc5..8292cfb 100644 --- a/core/java/android/printservice/PrintDocument.java +++ b/core/java/android/printservice/PrintDocument.java @@ -21,12 +21,15 @@ import android.os.RemoteException; import android.print.PrintDocumentInfo; import android.util.Log; -import java.io.FileDescriptor; import java.io.IOException; /** * This class represents a printed document from the perspective of a print * service. It exposes APIs to query the document and obtain its data. + * <p> + * <strong>Note: </strong> All methods of this class must be executed on the + * main application thread. + * </p> */ public final class PrintDocument { @@ -51,6 +54,7 @@ public final class PrintDocument { * @return The document info. */ public PrintDocumentInfo getInfo() { + PrintService.throwIfNotCalledOnMainThread(); return mInfo; } @@ -64,7 +68,8 @@ public final class PrintDocument { * * @return A file descriptor for reading the data. */ - public FileDescriptor getData() { + public ParcelFileDescriptor getData() { + PrintService.throwIfNotCalledOnMainThread(); ParcelFileDescriptor source = null; ParcelFileDescriptor sink = null; try { @@ -72,7 +77,7 @@ public final class PrintDocument { source = fds[0]; sink = fds[1]; mPrintServiceClient.writePrintJobData(sink, mPrintJobId); - return source.getFileDescriptor(); + return source; } catch (IOException ioe) { Log.e(LOG_TAG, "Error calling getting print job data!", ioe); } catch (RemoteException re) { diff --git a/core/java/android/printservice/PrintJob.java b/core/java/android/printservice/PrintJob.java index d2fbef2..8bae9d6 100644 --- a/core/java/android/printservice/PrintJob.java +++ b/core/java/android/printservice/PrintJob.java @@ -18,6 +18,7 @@ package android.printservice; import android.os.RemoteException; import android.print.PrintJobInfo; +import android.text.TextUtils; import android.util.Log; /** @@ -123,6 +124,21 @@ public final class PrintJob { } /** + * Gets whether this print job is blocked. Such a print job is halted + * due to an abnormal condition and can be started or canceled or failed. + * + * @return Whether the print job is blocked. + * + * @see #start() + * @see #cancel() + * @see #fail(CharSequence) + */ + public boolean isBlocked() { + PrintService.throwIfNotCalledOnMainThread(); + return getInfo().getState() == PrintJobInfo.STATE_BLOCKED; + } + + /** * Gets whether this print job is completed. Such a print job * is successfully printed. This is a final state. * @@ -163,21 +179,49 @@ public final class PrintJob { /** * Starts the print job. You should call this method if {@link - * #isQueued()} returns true and you started printing. + * #isQueued()} or {@link #isBlocked()} returns true and you started + * resumed printing. * - * @return Whether the job as started. + * @return Whether the job was started. * * @see #isQueued() + * @see #isBlocked() */ public boolean start() { PrintService.throwIfNotCalledOnMainThread(); - if (isQueued()) { + final int state = getInfo().getState(); + if (state == PrintJobInfo.STATE_QUEUED + || state == PrintJobInfo.STATE_BLOCKED) { return setState(PrintJobInfo.STATE_STARTED, null); } return false; } /** + * Blocks the print job. You should call this method if {@link + * #isStarted()} or {@link #isBlocked()} returns true and you need + * to block the print job. For example, the user has to add some + * paper to continue printing. To resume the print job call {@link + * #start()}. + * + * @return Whether the job was blocked. + * + * @see #isStarted() + * @see #isBlocked() + */ + public boolean block(String reason) { + PrintService.throwIfNotCalledOnMainThread(); + PrintJobInfo info = getInfo(); + final int state = info.getState(); + if (state == PrintJobInfo.STATE_STARTED + || (state == PrintJobInfo.STATE_BLOCKED + && !TextUtils.equals(info.getStateReason(), reason))) { + return setState(PrintJobInfo.STATE_BLOCKED, reason); + } + return false; + } + + /** * Completes the print job. You should call this method if {@link * #isStarted()} returns true and you are done printing. * @@ -195,8 +239,8 @@ public final class PrintJob { /** * Fails the print job. You should call this method if {@link - * #isQueued()} or {@link #isStarted()} returns true you failed - * while printing. + * #isQueued()} or {@link #isStarted()} or {@link #isBlocked()} + * returns true you failed while printing. * * @param error The human readable, short, and translated reason * for the failure. @@ -204,10 +248,11 @@ public final class PrintJob { * * @see #isQueued() * @see #isStarted() + * @see #isBlocked() */ public boolean fail(String error) { PrintService.throwIfNotCalledOnMainThread(); - if (isQueued() || isStarted()) { + if (!isInImmutableState()) { return setState(PrintJobInfo.STATE_FAILED, error); } return false; @@ -215,18 +260,19 @@ public final class PrintJob { /** * Cancels the print job. You should call this method if {@link - * #isQueued()} or {@link #isStarted()} returns true and you canceled - * the print job as a response to a call to {@link - * PrintService#onRequestCancelPrintJob(PrintJob)}. + * #isQueued()} or {@link #isStarted() or #isBlocked()} returns + * true and you canceled the print job as a response to a call to + * {@link PrintService#onRequestCancelPrintJob(PrintJob)}. * * @return Whether the job is canceled. * * @see #isStarted() * @see #isQueued() + * @see #isBlocked() */ public boolean cancel() { PrintService.throwIfNotCalledOnMainThread(); - if (isQueued() || isStarted()) { + if (!isInImmutableState()) { return setState(PrintJobInfo.STATE_CANCELED, null); } return false; @@ -277,7 +323,8 @@ public final class PrintJob { private boolean isInImmutableState() { final int state = mCachedInfo.getState(); return state == PrintJobInfo.STATE_COMPLETED - || state == PrintJobInfo.STATE_CANCELED; + || state == PrintJobInfo.STATE_CANCELED + || state == PrintJobInfo.STATE_FAILED; } private boolean setState(int state, String error) { @@ -287,7 +334,7 @@ public final class PrintJob { // we may not be able to re-fetch it later if the job gets // removed from the spooler as a result of the state change. mCachedInfo.setState(state); - mCachedInfo.setFailureReason(error); + mCachedInfo.setStateReason(error); return true; } } catch (RemoteException re) { diff --git a/core/java/android/printservice/PrintService.java b/core/java/android/printservice/PrintService.java index f6c0a9a..96552af 100644 --- a/core/java/android/printservice/PrintService.java +++ b/core/java/android/printservice/PrintService.java @@ -314,8 +314,20 @@ public abstract class PrintService extends Service { } @Override - public void requestPrinterUpdate(PrinterId printerId) { - mHandler.obtainMessage(ServiceHandler.MSG_REQUEST_PRINTER_UPDATE, + public void validatePrinters(List<PrinterId> printerIds) { + mHandler.obtainMessage(ServiceHandler.MSG_VALIDATE_PRINTERS, + printerIds).sendToTarget(); + } + + @Override + public void startPrinterStateTracking(PrinterId printerId) { + mHandler.obtainMessage(ServiceHandler.MSG_START_PRINTER_STATE_TRACKING, + printerId).sendToTarget(); + } + + @Override + public void stopPrinterStateTracking(PrinterId printerId) { + mHandler.obtainMessage(ServiceHandler.MSG_STOP_PRINTER_STATE_TRACKING, printerId).sendToTarget(); } @@ -344,10 +356,12 @@ public abstract class PrintService extends Service { public static final int MSG_DESTROY_PRINTER_DISCOVERY_SESSION = 2; public static final int MSG_START_PRINTER_DISCOVERY = 3; public static final int MSG_STOP_PRINTER_DISCOVERY = 4; - public static final int MSG_REQUEST_PRINTER_UPDATE = 5; - public static final int MSG_ON_PRINTJOB_QUEUED = 6; - public static final int MSG_ON_REQUEST_CANCEL_PRINTJOB = 7; - public static final int MSG_SET_CLEINT = 8; + public static final int MSG_VALIDATE_PRINTERS = 5; + public static final int MSG_START_PRINTER_STATE_TRACKING = 6; + public static final int MSG_STOP_PRINTER_STATE_TRACKING = 7; + public static final int MSG_ON_PRINTJOB_QUEUED = 8; + public static final int MSG_ON_REQUEST_CANCEL_PRINTJOB = 9; + public static final int MSG_SET_CLEINT = 10; public ServiceHandler(Looper looper) { super(looper, null, true); @@ -391,10 +405,24 @@ public abstract class PrintService extends Service { } } break; - case MSG_REQUEST_PRINTER_UPDATE: { + case MSG_VALIDATE_PRINTERS: { + if (mDiscoverySession != null) { + List<PrinterId> printerIds = (List<PrinterId>) message.obj; + mDiscoverySession.validatePrinters(printerIds); + } + } break; + + case MSG_START_PRINTER_STATE_TRACKING: { + if (mDiscoverySession != null) { + PrinterId printerId = (PrinterId) message.obj; + mDiscoverySession.startPrinterStateTracking(printerId); + } + } break; + + case MSG_STOP_PRINTER_STATE_TRACKING: { if (mDiscoverySession != null) { PrinterId printerId = (PrinterId) message.obj; - mDiscoverySession.requestPrinterUpdate(printerId); + mDiscoverySession.stopPrinterStateTracking(printerId); } } break; diff --git a/core/java/android/printservice/PrinterDiscoverySession.java b/core/java/android/printservice/PrinterDiscoverySession.java index 8b959a6..6464cc1 100644 --- a/core/java/android/printservice/PrinterDiscoverySession.java +++ b/core/java/android/printservice/PrinterDiscoverySession.java @@ -53,15 +53,23 @@ import java.util.List; * session. Printers are <strong>not</strong> persisted across sessions. * </p> * <p> - * The system will make a call to - * {@link PrinterDiscoverySession#onRequestPrinterUpdate(PrinterId)} if you - * need to update a given printer. It is possible that you add a printer without + * The system will make a call to {@link #onValidatePrinters(List)} if you + * need to update some printers. It is possible that you add a printer without * specifying its capabilities. This enables you to avoid querying all discovered * printers for their capabilities, rather querying the capabilities of a printer * only if necessary. For example, the system will request that you update a printer - * if it gets selected by the user. If you did not report the printer capabilities - * when adding it, you must do so after the system requests a printer update. - * Otherwise, the printer will be ignored. + * if it gets selected by the user. When validating printers you do not need to + * provide the printers' capabilities but may do so. + * </p> + * <p> + * If the system is interested in being constantly updated for the state of a + * printer you will receive a call to {@link #onStartPrinterStateTracking(PrinterId)} + * after which you will have to do a best effort to keep the system updated for + * changes in the printer state and capabilities. You also <strong>must</strong> + * update the printer capabilities if you did not provide them when adding it, or + * the printer will be ignored. When the system is no longer interested in getting + * updates for a printer you will receive a call to {@link #onStopPrinterStateTracking( + * PrinterId)}. * </p> * <p> * <strong>Note: </strong> All callbacks in this class are executed on the main @@ -72,7 +80,7 @@ import java.util.List; public abstract class PrinterDiscoverySession { private static final String LOG_TAG = "PrinterDiscoverySession"; - private static final int MAX_ITEMS_PER_CALLBACK = 100; + private static final int MAX_ITEMS_PER_CALLBACK = 50; private static int sIdCounter = 0; @@ -115,7 +123,7 @@ public abstract class PrinterDiscoverySession { * the printer that was added but not removed. * <p> * <strong>Note: </strong> Calls to this method after the session is - * destroyed, i.e. after the {@link #onDestroy()} callback, will be ignored. + * destroyed, that is after the {@link #onDestroy()} callback, will be ignored. * </p> * * @return The printers. @@ -139,7 +147,7 @@ public abstract class PrinterDiscoverySession { * times during the life of this session. Duplicates will be ignored. * <p> * <strong>Note: </strong> Calls to this method after the session is - * destroyed, i.e. after the {@link #onDestroy()} callback, will be ignored. + * destroyed, that is after the {@link #onDestroy()} callback, will be ignored. * </p> * * @param printers The printers to add. @@ -218,7 +226,7 @@ public abstract class PrinterDiscoverySession { * call this method multiple times during the lifetime of this session. * <p> * <strong>Note: </strong> Calls to this method after the session is - * destroyed, i.e. after the {@link #onDestroy()} callback, will be ignored. + * destroyed, that is after the {@link #onDestroy()} callback, will be ignored. * </p> * * @param printerIds The ids of the removed printers. @@ -293,7 +301,7 @@ public abstract class PrinterDiscoverySession { * during the lifetime of this session. * <p> * <strong>Note: </strong> Calls to this method after the session is - * destroyed, i.e. after the {@link #onDestroy()} callback, will be ignored. + * destroyed, that is after the {@link #onDestroy()} callback, will be ignored. * </p> * * @param printers The printers to update. @@ -441,7 +449,9 @@ public abstract class PrinterDiscoverySession { * <p> * <strong>Note: </strong>You are also given a list of printers whose availability * has to be checked first. For example, these printers could be the user's favorite - * ones, therefore they have to be verified first. + * ones, therefore they have to be verified first. You do <strong>not need</strong> + * to provide the capabilities of the printers, rather verify whether they exist + * similarly to {@link #onValidatePrinters(List)}. * </p> * * @param priorityList The list of printers to validate first. Never null. @@ -463,9 +473,28 @@ public abstract class PrinterDiscoverySession { public abstract void onStopPrinterDiscovery(); /** - * Requests that you update a printer. You are responsible for updating - * the printer by also reporting its capabilities via calling {@link - * #updatePrinters(List)}. + * Callback asking you to validate that the given printers are valid, that + * is they exist. You are responsible for checking whether these printers + * exist and for the ones that do exist notify the system via calling + * {@link #updatePrinters(List)}. + * <p> + * <strong>Note: </strong> You are <strong>not required</strong> to provide + * the printer capabilities when updating the printers that do exist. + * <p> + * + * @param printerIds The printers to validate. + * + * @see #updatePrinters(List) + * @see PrinterInfo.Builder#setCapabilities(PrinterCapabilitiesInfo) + * PrinterInfo.Builder.setCapabilities(PrinterCapabilitiesInfo) + */ + public abstract void onValidatePrinters(List<PrinterId> printerIds); + + /** + * Callback asking you to start tracking the state of a printer. Tracking + * the state means that you should do a best effort to observe the state + * of this printer and notify the system if that state changes via calling + * {@link #updatePrinters(List)}. * <p> * <strong>Note: </strong> A printer can be initially added without its * capabilities to avoid polling printers that the user will not select. @@ -473,18 +502,33 @@ public abstract class PrinterDiscoverySession { * printer <strong>including</strong> its capabilities. Otherwise, the * printer will be ignored. * <p> - * A scenario when you may be requested to update a printer is if the user - * selects it and the system has to present print options UI based on the - * printer's capabilities. + * <p> + * A scenario when you may be requested to track a printer's state is if + * the user selects that printer and the system has to present print + * options UI based on the printer's capabilities. In this case the user + * should be promptly informed if, for example, the printer becomes + * unavailable. * </p> * - * @param printerId The printer id. + * @param printerId The printer to start tracking. * + * @see #onStopPrinterStateTracking(PrinterId) * @see #updatePrinters(List) * @see PrinterInfo.Builder#setCapabilities(PrinterCapabilitiesInfo) * PrinterInfo.Builder.setCapabilities(PrinterCapabilitiesInfo) */ - public abstract void onRequestPrinterUpdate(PrinterId printerId); + public abstract void onStartPrinterStateTracking(PrinterId printerId); + + /** + * Callback asking you to stop tracking the state of a printer. The passed + * in printer id is the one for which you received a call to {@link + * #onStartPrinterStateTracking(PrinterId)}. + * + * @param printerId The printer to stop tracking. + * + * @see #onStartPrinterStateTracking(PrinterId) + */ + public abstract void onStopPrinterStateTracking(PrinterId printerId); /** * Notifies you that the session is destroyed. After this callback is invoked @@ -538,9 +582,21 @@ public abstract class PrinterDiscoverySession { } } - void requestPrinterUpdate(PrinterId printerId) { - if (!mIsDestroyed) { - onRequestPrinterUpdate(printerId); + void validatePrinters(List<PrinterId> printerIds) { + if (!mIsDestroyed && mObserver != null) { + onValidatePrinters(printerIds); + } + } + + void startPrinterStateTracking(PrinterId printerId) { + if (!mIsDestroyed && mObserver != null) { + onStartPrinterStateTracking(printerId); + } + } + + void stopPrinterStateTracking(PrinterId printerId) { + if (!mIsDestroyed && mObserver != null) { + onStopPrinterStateTracking(printerId); } } diff --git a/core/java/android/provider/AlarmClock.java b/core/java/android/provider/AlarmClock.java index f3267ee..724d76d 100644 --- a/core/java/android/provider/AlarmClock.java +++ b/core/java/android/provider/AlarmClock.java @@ -36,10 +36,15 @@ public final class AlarmClock { * <p> * Activates an existing alarm or creates a new one. * </p><p> - * This action requests an alarm to be set for a given time of day. If an alarm already - * exists for this time, an implementation may use it rather than create a new one. If no time - * of day is specified, the implementation should start an activity that is capable of setting - * an alarm (SKIP_UI is ignored in this case). This action always enables the alarm. + * This action requests an alarm to be set for a given time of day. If no time of day is + * specified, an implementation should start an activity that is capable of setting an alarm + * ({@link #EXTRA_SKIP_UI} is ignored in this case). If a time of day is specified, and + * {@link #EXTRA_SKIP_UI} is {@code true}, and the alarm is not repeating, the implementation + * should remove this alarm after it has been dismissed. If an identical alarm exists matching + * all parameters, the implementation may re-use it instead of creating a new one (in this case, + * the alarm should not be removed after dismissal). + * + * This action always enables the alarm. * </p> * <h3>Request parameters</h3> * <ul> @@ -52,8 +57,6 @@ public final class AlarmClock { * vibrator for this alarm. * <li>{@link #EXTRA_SKIP_UI} <em>(optional)</em>: Whether or not to display an activity for * setting this alarm. - * <li>{@link #EXTRA_DELETE_AFTER_USE} <em>(optional)</em>: Whether or not to delete this - * alarm after it is dismissed. * </ul> */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) @@ -65,13 +68,14 @@ public final class AlarmClock { * Activates an existing timer or creates a new one. * </p><p> * This action requests a timer to be started for a specific {@link #EXTRA_LENGTH length} of - * time. If a timer already exists for this {@link #EXTRA_LENGTH length}, an implementation may - * use it rather than create a new one. If no {@link #EXTRA_LENGTH length} is specified, the - * implementation should start an activity that is capable of setting a timer - * ({@link #EXTRA_SKIP_UI} is ignored in this case). - * </p><p> - * An existing timer should only be used if it matches the provided extras and is not currently - * in use. + * time. If no {@link #EXTRA_LENGTH length} is specified, the implementation should start an + * activity that is capable of setting a timer ({@link #EXTRA_SKIP_UI} is ignored in this case). + * If a {@link #EXTRA_LENGTH length} is specified, and {@link #EXTRA_SKIP_UI} is {@code true}, + * the implementation should remove this timer after it has been dismissed. If an identical, + * unused timer exists matching both parameters, an implementation may re-use it instead of + * creating a new one (in this case, the timer should not be removed after dismissal). + * + * This action always starts the timer. * </p> * * <h3>Request parameters</h3> @@ -80,14 +84,21 @@ public final class AlarmClock { * <li>{@link #EXTRA_MESSAGE} <em>(optional)</em>: A custom message for the timer. * <li>{@link #EXTRA_SKIP_UI} <em>(optional)</em>: Whether or not to display an activity for * setting this timer. - * <li>{@link #EXTRA_DELETE_AFTER_USE} <em>(optional)</em>: Whether or not to delete this - * timer after it is dismissed. * </ul> */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_SET_TIMER = "android.intent.action.SET_TIMER"; /** + * Activity Action: Show the alarms. + * <p> + * This action opens the alarms page. + * </p> + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SHOW_ALARMS = "android.intent.action.SHOW_ALARMS"; + + /** * Bundle extra: Weekdays for repeating alarm. * <p> * Used by {@link #ACTION_SET_ALARM}. @@ -103,30 +114,10 @@ public final class AlarmClock { * <li> {@link java.util.Calendar#FRIDAY}, * <li> {@link java.util.Calendar#SATURDAY} * </ul> - * <p> - * Note: If this extra is provided, {@link #EXTRA_DELETE_AFTER_USE} is ignored. - * </p> */ public static final String EXTRA_DAYS = "android.intent.extra.alarm.DAYS"; /** - * Bundle extra: Whether or not to delete this alarm/timer after it's dismissed. - * <p> - * Used by {@link #ACTION_SET_ALARM} and {@link #ACTION_SET_TIMER}. - * </p><p> - * If this value is true, the alarm/timer used by this action should be deleted after it's been - * dismissed. The alarm/timer should only be removed if was actually created by the action. If - * an existing alarm/timer was used, it should not be deleted after it's dismissed. - * </p><p> - * The value is a {@link Boolean}. - * </p> - * - * @see #ACTION_SET_ALARM - * @see #ACTION_SET_TIMER - */ - public static final String EXTRA_DELETE_AFTER_USE = "android.intent.extra.alarm.DELETE_AFTER_USE"; - - /** * Bundle extra: The hour of the alarm. * <p> * Used by {@link #ACTION_SET_ALARM}. diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 65c9220..f445fd5 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -19,9 +19,7 @@ package android.provider; import static android.net.TrafficStats.KB_IN_BYTES; import static libcore.io.OsConstants.SEEK_SET; -import android.content.ContentProvider; import android.content.ContentResolver; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -33,6 +31,7 @@ import android.graphics.BitmapFactory; import android.graphics.Point; import android.net.Uri; import android.os.Bundle; +import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.OnCloseListener; import android.util.Log; @@ -51,448 +50,532 @@ import java.util.List; /** * Defines the contract between a documents provider and the platform. * <p> - * A document provider is a {@link ContentProvider} that presents a set of - * documents in a hierarchical structure. The system provides UI that visualizes - * all available document providers, offering users the ability to open existing - * documents or create new documents. - * <p> - * Each provider expresses one or more "roots" which each serve as the top-level - * of a tree. For example, a root could represent an account, or a physical - * storage device. Under each root, documents are referenced by a unique - * {@link DocumentColumns#DOC_ID}, and each root starts at the - * {@link Documents#DOC_ID_ROOT} document. - * <p> - * Documents can be either an openable file (with a specific MIME type), or a - * directory containing additional documents (with the - * {@link Documents#MIME_TYPE_DIR} MIME type). Each document can have different - * capabilities, as described by {@link DocumentColumns#FLAGS}. The same - * {@link DocumentColumns#DOC_ID} can be included in multiple directories. - * <p> - * Document providers must be protected with the - * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can - * only be requested by the system. The system-provided UI then issues narrow - * Uri permission grants for individual documents when the user explicitly picks - * documents. + * To create a document provider, extend {@link DocumentsProvider}, which + * provides a foundational implementation of this contract. * - * @see Intent#ACTION_OPEN_DOCUMENT - * @see Intent#ACTION_CREATE_DOCUMENT + * @see DocumentsProvider */ public final class DocumentsContract { private static final String TAG = "Documents"; - // content://com.example/roots/ - // content://com.example/roots/sdcard/ - // content://com.example/roots/sdcard/docs/0/ - // content://com.example/roots/sdcard/docs/0/contents/ - // content://com.example/roots/sdcard/docs/0/search/?query=pony + // content://com.example/root/ + // content://com.example/root/sdcard/ + // content://com.example/root/sdcard/recent/ + // content://com.example/document/12/ + // content://com.example/document/12/children/ + // content://com.example/document/12/search/?query=pony + + private DocumentsContract() { + } /** {@hide} */ public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER"; /** {@hide} */ - public static final String ACTION_DOCUMENT_CHANGED = "android.provider.action.DOCUMENT_CHANGED"; + public static final String ACTION_MANAGE_DOCUMENTS = "android.provider.action.MANAGE_DOCUMENTS"; /** - * Constants for individual documents. + * Constants related to a document, including {@link Cursor} columns names + * and flags. + * <p> + * A document can be either an openable file (with a specific MIME type), or + * a directory containing additional documents (with the + * {@link #MIME_TYPE_DIR} MIME type). + * <p> + * All columns are <em>read-only</em> to client applications. */ - public static class Documents { - private Documents() { + public final static class Document { + private Document() { } /** - * MIME type of a document which is a directory that may contain additional - * documents. + * Unique ID of a document. This ID is both provided by and interpreted + * by a {@link DocumentsProvider}, and should be treated as an opaque + * value by client applications. + * <p> + * Each document must have a unique ID within a provider, but that + * single document may be included as a child of multiple directories. + * <p> + * A provider must always return durable IDs, since they will be used to + * issue long-term Uri permission grants when an application interacts + * with {@link Intent#ACTION_OPEN_DOCUMENT} and + * {@link Intent#ACTION_CREATE_DOCUMENT}. + * <p> + * Type: STRING + */ + public static final String COLUMN_DOCUMENT_ID = "document_id"; + + /** + * Concrete MIME type of a document. For example, "image/png" or + * "application/pdf" for openable files. A document can also be a + * directory containing additional documents, which is represented with + * the {@link #MIME_TYPE_DIR} MIME type. + * <p> + * Type: STRING * - * @see #buildContentsUri(String, String, String) + * @see #MIME_TYPE_DIR */ - public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/doc"; + public static final String COLUMN_MIME_TYPE = "mime_type"; /** - * {@link DocumentColumns#DOC_ID} value representing the root directory of a - * documents root. + * Display name of a document, used as the primary title displayed to a + * user. + * <p> + * Type: STRING */ - public static final String DOC_ID_ROOT = "0"; + public static final String COLUMN_DISPLAY_NAME = OpenableColumns.DISPLAY_NAME; /** - * Flag indicating that a document is a directory that supports creation of - * new files within it. + * Summary of a document, which may be shown to a user. The summary may + * be {@code null}. + * <p> + * Type: STRING + */ + public static final String COLUMN_SUMMARY = "summary"; + + /** + * Timestamp when a document was last modified, in milliseconds since + * January 1, 1970 00:00:00.0 UTC, or {@code null} if unknown. A + * {@link DocumentsProvider} can update this field using events from + * {@link OnCloseListener} or other reliable + * {@link ParcelFileDescriptor} transports. + * <p> + * Type: INTEGER (long) * - * @see DocumentColumns#FLAGS - * @see #createDocument(ContentResolver, Uri, String, String) + * @see System#currentTimeMillis() */ - public static final int FLAG_SUPPORTS_CREATE = 1; + public static final String COLUMN_LAST_MODIFIED = "last_modified"; + + /** + * Specific icon resource ID for a document, or {@code null} to use + * platform default icon based on {@link #COLUMN_MIME_TYPE}. + * <p> + * Type: INTEGER (int) + */ + public static final String COLUMN_ICON = "icon"; /** - * Flag indicating that a document is renamable. + * Flags that apply to a document. + * <p> + * Type: INTEGER (int) * - * @see DocumentColumns#FLAGS - * @see #renameDocument(ContentResolver, Uri, String) + * @see #FLAG_SUPPORTS_WRITE + * @see #FLAG_SUPPORTS_DELETE + * @see #FLAG_SUPPORTS_THUMBNAIL + * @see #FLAG_DIR_PREFERS_GRID + * @see #FLAG_DIR_SUPPORTS_CREATE + * @see #FLAG_DIR_SUPPORTS_SEARCH */ - public static final int FLAG_SUPPORTS_RENAME = 1 << 1; + public static final String COLUMN_FLAGS = "flags"; /** - * Flag indicating that a document is deletable. + * Size of a document, in bytes, or {@code null} if unknown. + * <p> + * Type: INTEGER (long) + */ + public static final String COLUMN_SIZE = OpenableColumns.SIZE; + + /** + * MIME type of a document which is a directory that may contain + * additional documents. * - * @see DocumentColumns#FLAGS + * @see #COLUMN_MIME_TYPE */ - public static final int FLAG_SUPPORTS_DELETE = 1 << 2; + public static final String MIME_TYPE_DIR = "vnd.android.document/directory"; /** * Flag indicating that a document can be represented as a thumbnail. * - * @see DocumentColumns#FLAGS - * @see #getThumbnail(ContentResolver, Uri, Point) + * @see #COLUMN_FLAGS + * @see DocumentsContract#getDocumentThumbnail(ContentResolver, Uri, + * Point, CancellationSignal) + * @see DocumentsProvider#openDocumentThumbnail(String, Point, + * android.os.CancellationSignal) */ - public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3; + public static final int FLAG_SUPPORTS_THUMBNAIL = 1; /** - * Flag indicating that a document is a directory that supports search. + * Flag indicating that a document supports writing. + * <p> + * When a document is opened with {@link Intent#ACTION_OPEN_DOCUMENT}, + * the calling application is granted both + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. However, the actual + * writability of a document may change over time, for example due to + * remote access changes. This flag indicates that a document client can + * expect {@link ContentResolver#openOutputStream(Uri)} to succeed. * - * @see DocumentColumns#FLAGS + * @see #COLUMN_FLAGS */ - public static final int FLAG_SUPPORTS_SEARCH = 1 << 4; + public static final int FLAG_SUPPORTS_WRITE = 1 << 1; /** - * Flag indicating that a document is writable. + * Flag indicating that a document is deletable. * - * @see DocumentColumns#FLAGS + * @see #COLUMN_FLAGS + * @see DocumentsContract#deleteDocument(ContentResolver, Uri) + * @see DocumentsProvider#deleteDocument(String) */ - public static final int FLAG_SUPPORTS_WRITE = 1 << 5; + public static final int FLAG_SUPPORTS_DELETE = 1 << 2; /** - * Flag indicating that a document is a directory that prefers its contents - * be shown in a larger format grid. Usually suitable when a directory - * contains mostly pictures. + * Flag indicating that a document is a directory that supports creation + * of new files within it. Only valid when {@link #COLUMN_MIME_TYPE} is + * {@link #MIME_TYPE_DIR}. * - * @see DocumentColumns#FLAGS + * @see #COLUMN_FLAGS + * @see DocumentsContract#createDocument(ContentResolver, Uri, String, + * String) + * @see DocumentsProvider#createDocument(String, String, String) */ - public static final int FLAG_PREFERS_GRID = 1 << 6; - } - - /** - * Optimal dimensions for a document thumbnail request, stored as a - * {@link Point} object. This is only a hint, and the returned thumbnail may - * have different dimensions. - * - * @see ContentProvider#openTypedAssetFile(Uri, String, Bundle) - */ - public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size"; - - /** - * Extra boolean flag included in a directory {@link Cursor#getExtras()} - * indicating that the document provider can provide additional data if - * requested, such as additional search results. - */ - public static final String EXTRA_HAS_MORE = "has_more"; + public static final int FLAG_DIR_SUPPORTS_CREATE = 1 << 3; - /** - * Extra boolean flag included in a {@link Cursor#respond(Bundle)} call to a - * directory to request that additional data should be fetched. When - * requested data is ready, the provider should send a change notification - * to cause a requery. - * - * @see Cursor#respond(Bundle) - * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver, - * boolean) - */ - public static final String EXTRA_REQUEST_MORE = "request_more"; - - private static final String PATH_ROOTS = "roots"; - private static final String PATH_DOCS = "docs"; - private static final String PATH_CONTENTS = "contents"; - private static final String PATH_SEARCH = "search"; - - private static final String PARAM_QUERY = "query"; - private static final String PARAM_LOCAL_ONLY = "localOnly"; - - /** - * Build Uri representing the roots offered by a document provider. - */ - public static Uri buildRootsUri(String authority) { - return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) - .authority(authority).appendPath(PATH_ROOTS).build(); - } - - /** - * Build Uri representing a specific root offered by a document provider. - */ - public static Uri buildRootUri(String authority, String rootId) { - return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) - .authority(authority).appendPath(PATH_ROOTS).appendPath(rootId).build(); - } - - /** - * Build Uri representing the given {@link DocumentColumns#DOC_ID} in a - * document provider. - */ - public static Uri buildDocumentUri(String authority, String rootId, String docId) { - return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) - .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId) - .build(); - } - - /** - * Build Uri representing the contents of the given directory in a document - * provider. The given document must be {@link Documents#MIME_TYPE_DIR}. - */ - public static Uri buildContentsUri(String authority, String rootId, String docId) { - return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) - .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId) - .appendPath(PATH_CONTENTS).build(); - } - - /** - * Build Uri representing a search for matching documents under a specific - * directory in a document provider. The given document must have - * {@link Documents#FLAG_SUPPORTS_SEARCH}. - */ - public static Uri buildSearchUri(String authority, String rootId, String docId, String query) { - return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) - .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId) - .appendPath(PATH_SEARCH).appendQueryParameter(PARAM_QUERY, query).build(); - } - - /** - * Convenience method for {@link #buildDocumentUri(String, String, String)}, - * extracting authority and root from the given Uri. - */ - public static Uri buildDocumentUri(Uri relatedUri, String docId) { - return buildDocumentUri(relatedUri.getAuthority(), getRootId(relatedUri), docId); - } - - /** - * Convenience method for {@link #buildContentsUri(String, String, String)}, - * extracting authority and root from the given Uri. - */ - public static Uri buildContentsUri(Uri relatedUri) { - return buildContentsUri( - relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri)); - } - - /** - * Convenience method for - * {@link #buildSearchUri(String, String, String, String)}, extracting - * authority and root from the given Uri. - */ - public static Uri buildSearchUri(Uri relatedUri, String query) { - return buildSearchUri( - relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri), query); - } + /** + * Flag indicating that a directory supports search. Only valid when + * {@link #COLUMN_MIME_TYPE} is {@link #MIME_TYPE_DIR}. + * + * @see #COLUMN_FLAGS + * @see DocumentsProvider#querySearchDocuments(String, String, + * String[]) + */ + public static final int FLAG_DIR_SUPPORTS_SEARCH = 1 << 4; - /** - * Extract the {@link RootColumns#ROOT_ID} from the given Uri. - */ - public static String getRootId(Uri documentUri) { - final List<String> paths = documentUri.getPathSegments(); - if (paths.size() < 2) { - throw new IllegalArgumentException("Not a root: " + documentUri); - } - if (!PATH_ROOTS.equals(paths.get(0))) { - throw new IllegalArgumentException("Not a root: " + documentUri); - } - return paths.get(1); + /** + * Flag indicating that a directory prefers its contents be shown in a + * larger format grid. Usually suitable when a directory contains mostly + * pictures. Only valid when {@link #COLUMN_MIME_TYPE} is + * {@link #MIME_TYPE_DIR}. + * + * @see #COLUMN_FLAGS + */ + public static final int FLAG_DIR_PREFERS_GRID = 1 << 5; } /** - * Extract the {@link DocumentColumns#DOC_ID} from the given Uri. + * Constants related to a root of documents, including {@link Cursor} + * columns names and flags. + * <p> + * All columns are <em>read-only</em> to client applications. */ - public static String getDocId(Uri documentUri) { - final List<String> paths = documentUri.getPathSegments(); - if (paths.size() < 4) { - throw new IllegalArgumentException("Not a document: " + documentUri); - } - if (!PATH_ROOTS.equals(paths.get(0))) { - throw new IllegalArgumentException("Not a document: " + documentUri); + public final static class Root { + private Root() { } - if (!PATH_DOCS.equals(paths.get(2))) { - throw new IllegalArgumentException("Not a document: " + documentUri); - } - return paths.get(3); - } - /** - * Return requested search query from the given Uri, as constructed by - * {@link #buildSearchUri(String, String, String, String)}. - */ - public static String getSearchQuery(Uri documentUri) { - return documentUri.getQueryParameter(PARAM_QUERY); - } - - /** - * Mark the given Uri to indicate that only locally-available data should be - * returned. That is, no network connections should be initiated to provide - * the metadata or content. - */ - public static Uri setLocalOnly(Uri documentUri) { - return documentUri.buildUpon() - .appendQueryParameter(PARAM_LOCAL_ONLY, String.valueOf(true)).build(); - } - - /** - * Return if the given Uri is requesting that only locally-available data be - * returned. That is, no network connections should be initiated to provide - * the metadata or content. - */ - public static boolean isLocalOnly(Uri documentUri) { - return documentUri.getBooleanQueryParameter(PARAM_LOCAL_ONLY, false); - } - - /** - * Standard columns for document queries. Document providers <em>must</em> - * support at least these columns when queried. - * - * @see DocumentsContract#buildDocumentUri(String, String, String) - * @see DocumentsContract#buildContentsUri(String, String, String) - * @see DocumentsContract#buildSearchUri(String, String, String, String) - */ - public interface DocumentColumns extends OpenableColumns { /** - * The ID for a document under a storage backend root. Values - * <em>must</em> never change once returned. This field is read-only to - * document clients. + * Unique ID of a root. This ID is both provided by and interpreted by a + * {@link DocumentsProvider}, and should be treated as an opaque value + * by client applications. * <p> * Type: STRING */ - public static final String DOC_ID = "doc_id"; + public static final String COLUMN_ROOT_ID = "root_id"; /** - * MIME type of a document, matching the value returned by - * {@link ContentResolver#getType(android.net.Uri)}. This field must be - * provided when a new document is created. This field is read-only to - * document clients. + * Type of a root, used for clustering when presenting multiple roots to + * a user. * <p> - * Type: STRING + * Type: INTEGER (int) * - * @see Documents#MIME_TYPE_DIR + * @see #ROOT_TYPE_SERVICE + * @see #ROOT_TYPE_SHORTCUT + * @see #ROOT_TYPE_DEVICE */ - public static final String MIME_TYPE = "mime_type"; + public static final String COLUMN_ROOT_TYPE = "root_type"; /** - * Timestamp when a document was last modified, in milliseconds since - * January 1, 1970 00:00:00.0 UTC. This field is read-only to document - * clients. Document providers can update this field using events from - * {@link OnCloseListener} or other reliable - * {@link ParcelFileDescriptor} transport. + * Flags that apply to a root. * <p> - * Type: INTEGER (long) + * Type: INTEGER (int) * - * @see System#currentTimeMillis() + * @see #FLAG_LOCAL_ONLY + * @see #FLAG_SUPPORTS_CREATE + * @see #FLAG_ADVANCED + * @see #FLAG_PROVIDES_AUDIO + * @see #FLAG_PROVIDES_IMAGES + * @see #FLAG_PROVIDES_VIDEO */ - public static final String LAST_MODIFIED = "last_modified"; + public static final String COLUMN_FLAGS = "flags"; /** - * Flags that apply to a specific document. This field is read-only to - * document clients. + * Icon resource ID for a root. * <p> * Type: INTEGER (int) */ - public static final String FLAGS = "flags"; + public static final String COLUMN_ICON = "icon"; /** - * Summary for this document, or {@code null} to omit. This field is - * read-only to document clients. + * Title for a root, which will be shown to a user. * <p> * Type: STRING */ - public static final String SUMMARY = "summary"; - } + public static final String COLUMN_TITLE = "title"; - /** - * Constants for individual document roots. - */ - public static class Roots { - private Roots() { - } + /** + * Summary for this root, which may be shown to a user. The summary may + * be {@code null}. + * <p> + * Type: STRING + */ + public static final String COLUMN_SUMMARY = "summary"; + + /** + * Document which is a directory that represents the top directory of + * this root. + * <p> + * Type: STRING + * + * @see Document#COLUMN_DOCUMENT_ID + */ + public static final String COLUMN_DOCUMENT_ID = "document_id"; - public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/root"; - public static final String MIME_TYPE_ITEM = "vnd.android.cursor.item/root"; + /** + * Number of bytes available in this root, or {@code null} if unknown or + * unbounded. + * <p> + * Type: INTEGER (long) + */ + public static final String COLUMN_AVAILABLE_BYTES = "available_bytes"; /** - * Root that represents a storage service, such as a cloud-based + * Type of root that represents a storage service, such as a cloud-based * service. * - * @see RootColumns#ROOT_TYPE + * @see #COLUMN_ROOT_TYPE */ public static final int ROOT_TYPE_SERVICE = 1; /** - * Root that represents a shortcut to content that may be available - * elsewhere through another storage root. + * Type of root that represents a shortcut to content that may be + * available elsewhere through another storage root. * - * @see RootColumns#ROOT_TYPE + * @see #COLUMN_ROOT_TYPE */ public static final int ROOT_TYPE_SHORTCUT = 2; /** - * Root that represents a physical storage device. + * Type of root that represents a physical storage device. * - * @see RootColumns#ROOT_TYPE + * @see #COLUMN_ROOT_TYPE */ public static final int ROOT_TYPE_DEVICE = 3; /** - * Root that represents a physical storage device that should only be - * displayed to advanced users. + * Flag indicating that at least one directory under this root supports + * creating content. Roots with this flag will be shown when an + * application interacts with {@link Intent#ACTION_CREATE_DOCUMENT}. * - * @see RootColumns#ROOT_TYPE + * @see #COLUMN_FLAGS */ - public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; - } + public static final int FLAG_SUPPORTS_CREATE = 1; - /** - * Standard columns for document root queries. - * - * @see DocumentsContract#buildRootsUri(String) - * @see DocumentsContract#buildRootUri(String, String) - */ - public interface RootColumns { - public static final String ROOT_ID = "root_id"; + /** + * Flag indicating that this root offers content that is strictly local + * on the device. That is, no network requests are made for the content. + * + * @see #COLUMN_FLAGS + * @see Intent#EXTRA_LOCAL_ONLY + */ + public static final int FLAG_LOCAL_ONLY = 1 << 1; /** - * Storage root type, use for clustering. This field is read-only to - * document clients. - * <p> - * Type: INTEGER (int) + * Flag indicating that this root should only be visible to advanced + * users. * - * @see Roots#ROOT_TYPE_SERVICE - * @see Roots#ROOT_TYPE_DEVICE + * @see #COLUMN_FLAGS */ - public static final String ROOT_TYPE = "root_type"; + public static final int FLAG_ADVANCED = 1 << 2; /** - * Icon resource ID for this storage root, or {@code null} to use the - * default {@link ProviderInfo#icon}. This field is read-only to - * document clients. - * <p> - * Type: INTEGER (int) + * Flag indicating that a root offers audio documents. When a user is + * selecting audio, roots not providing audio may be excluded. + * + * @see #COLUMN_FLAGS + * @see Intent#EXTRA_MIME_TYPES */ - public static final String ICON = "icon"; + public static final int FLAG_PROVIDES_AUDIO = 1 << 3; /** - * Title for this storage root, or {@code null} to use the default - * {@link ProviderInfo#labelRes}. This field is read-only to document - * clients. - * <p> - * Type: STRING + * Flag indicating that a root offers video documents. When a user is + * selecting video, roots not providing video may be excluded. + * + * @see #COLUMN_FLAGS + * @see Intent#EXTRA_MIME_TYPES */ - public static final String TITLE = "title"; + public static final int FLAG_PROVIDES_VIDEO = 1 << 4; /** - * Summary for this storage root, or {@code null} to omit. This field is - * read-only to document clients. - * <p> - * Type: STRING + * Flag indicating that a root offers image documents. When a user is + * selecting images, roots not providing images may be excluded. + * + * @see #COLUMN_FLAGS + * @see Intent#EXTRA_MIME_TYPES */ - public static final String SUMMARY = "summary"; + public static final int FLAG_PROVIDES_IMAGES = 1 << 5; /** - * Number of free bytes of available in this storage root, or - * {@code null} if unknown or unbounded. This field is read-only to - * document clients. - * <p> - * Type: INTEGER (long) + * Flag indicating that this root can report recently modified + * documents. + * + * @see #COLUMN_FLAGS + * @see DocumentsContract#buildRecentDocumentsUri(String, String) */ - public static final String AVAILABLE_BYTES = "available_bytes"; + public static final int FLAG_SUPPORTS_RECENTS = 1 << 6; + } + + /** + * Optional boolean flag included in a directory {@link Cursor#getExtras()} + * indicating that a document provider is still loading data. For example, a + * provider has returned some results, but is still waiting on an + * outstanding network request. The provider must send a content changed + * notification when loading is finished. + * + * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver, + * boolean) + */ + public static final String EXTRA_LOADING = "loading"; + + /** + * Optional string included in a directory {@link Cursor#getExtras()} + * providing an informational message that should be shown to a user. For + * example, a provider may wish to indicate that not all documents are + * available. + */ + public static final String EXTRA_INFO = "info"; + + /** + * Optional string included in a directory {@link Cursor#getExtras()} + * providing an error message that should be shown to a user. For example, a + * provider may wish to indicate that a network error occurred. The user may + * choose to retry, resulting in a new query. + */ + public static final String EXTRA_ERROR = "error"; + + /** {@hide} */ + public static final String METHOD_CREATE_DOCUMENT = "android:createDocument"; + /** {@hide} */ + public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument"; + + /** {@hide} */ + public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size"; + + private static final String PATH_ROOT = "root"; + private static final String PATH_RECENT = "recent"; + private static final String PATH_DOCUMENT = "document"; + private static final String PATH_CHILDREN = "children"; + private static final String PATH_SEARCH = "search"; + + private static final String PARAM_QUERY = "query"; + + /** + * Build Uri representing the roots of a document provider. When queried, a + * provider will return one or more rows with columns defined by + * {@link Root}. + * + * @see DocumentsProvider#queryRoots(String[]) + */ + public static Uri buildRootsUri(String authority) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority).appendPath(PATH_ROOT).build(); + } + + /** + * Build Uri representing the recently modified documents of a specific + * root. When queried, a provider will return zero or more rows with columns + * defined by {@link Document}. + * + * @see DocumentsProvider#queryRecentDocuments(String, String[]) + * @see #getRootId(Uri) + */ + public static Uri buildRecentDocumentsUri(String authority, String rootId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority).appendPath(PATH_ROOT).appendPath(rootId) + .appendPath(PATH_RECENT).build(); + } + + /** + * Build Uri representing the given {@link Document#COLUMN_DOCUMENT_ID} in a + * document provider. When queried, a provider will return a single row with + * columns defined by {@link Document}. + * + * @see DocumentsProvider#queryDocument(String, String[]) + * @see #getDocumentId(Uri) + */ + public static Uri buildDocumentUri(String authority, String documentId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority).appendPath(PATH_DOCUMENT).appendPath(documentId).build(); + } + + /** + * Build Uri representing the children of the given directory in a document + * provider. When queried, a provider will return zero or more rows with + * columns defined by {@link Document}. + * + * @param parentDocumentId the document to return children for, which must + * be a directory with MIME type of + * {@link Document#MIME_TYPE_DIR}. + * @see DocumentsProvider#queryChildDocuments(String, String[], String) + * @see #getDocumentId(Uri) + */ + public static Uri buildChildDocumentsUri(String authority, String parentDocumentId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) + .appendPath(PATH_DOCUMENT).appendPath(parentDocumentId).appendPath(PATH_CHILDREN) + .build(); + } + + /** + * Build Uri representing a search for matching documents under a specific + * directory in a document provider. When queried, a provider will return + * zero or more rows with columns defined by {@link Document}. + * + * @param parentDocumentId the document to return children for, which must + * be both a directory with MIME type of + * {@link Document#MIME_TYPE_DIR} and have + * {@link Document#FLAG_DIR_SUPPORTS_SEARCH} set. + * @see DocumentsProvider#querySearchDocuments(String, String, String[]) + * @see #getDocumentId(Uri) + * @see #getSearchDocumentsQuery(Uri) + */ + public static Uri buildSearchDocumentsUri( + String authority, String parentDocumentId, String query) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) + .appendPath(PATH_DOCUMENT).appendPath(parentDocumentId).appendPath(PATH_SEARCH) + .appendQueryParameter(PARAM_QUERY, query).build(); + } + + /** + * Extract the {@link Root#COLUMN_ROOT_ID} from the given Uri. + */ + public static String getRootId(Uri rootUri) { + final List<String> paths = rootUri.getPathSegments(); + if (paths.size() < 2) { + throw new IllegalArgumentException("Not a root: " + rootUri); + } + if (!PATH_ROOT.equals(paths.get(0))) { + throw new IllegalArgumentException("Not a root: " + rootUri); + } + return paths.get(1); + } + + /** + * Extract the {@link Document#COLUMN_DOCUMENT_ID} from the given Uri. + */ + public static String getDocumentId(Uri documentUri) { + final List<String> paths = documentUri.getPathSegments(); + if (paths.size() < 2) { + throw new IllegalArgumentException("Not a document: " + documentUri); + } + if (!PATH_DOCUMENT.equals(paths.get(0))) { + throw new IllegalArgumentException("Not a document: " + documentUri); + } + return paths.get(1); + } + + /** + * Extract the search query from a Uri built by + * {@link #buildSearchDocumentsUri(String, String, String)}. + */ + public static String getSearchDocumentsQuery(Uri searchDocumentsUri) { + return searchDocumentsUri.getQueryParameter(PARAM_QUERY); } /** @@ -503,6 +586,7 @@ public final class DocumentsContract { * {@link Intent#ACTION_CREATE_DOCUMENT}. * * @see Context#grantUriPermission(String, Uri, int) + * @see Context#revokeUriPermission(Uri, int) * @see ContentResolver#getIncomingUriPermissionGrants(int, int) */ public static Uri[] getOpenDocuments(Context context) { @@ -526,19 +610,28 @@ public final class DocumentsContract { } /** - * Return thumbnail representing the document at the given URI. Callers are - * responsible for their own in-memory caching. Given document must have - * {@link Documents#FLAG_SUPPORTS_THUMBNAIL} set. + * Return thumbnail representing the document at the given Uri. Callers are + * responsible for their own in-memory caching. * + * @param documentUri document to return thumbnail for, which must have + * {@link Document#FLAG_SUPPORTS_THUMBNAIL} set. + * @param size optimal thumbnail size desired. A provider may return a + * thumbnail of a different size, but never more than double the + * requested size. + * @param signal signal used to indicate that caller is no longer interested + * in the thumbnail. * @return decoded thumbnail, or {@code null} if problem was encountered. + * @see DocumentsProvider#openDocumentThumbnail(String, Point, + * android.os.CancellationSignal) */ - public static Bitmap getThumbnail(ContentResolver resolver, Uri documentUri, Point size) { + public static Bitmap getDocumentThumbnail( + ContentResolver resolver, Uri documentUri, Point size, CancellationSignal signal) { final Bundle openOpts = new Bundle(); openOpts.putParcelable(DocumentsContract.EXTRA_THUMBNAIL_SIZE, size); AssetFileDescriptor afd = null; try { - afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts); + afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts, signal); final FileDescriptor fd = afd.getFileDescriptor(); final long offset = afd.getStartOffset(); @@ -589,43 +682,46 @@ public final class DocumentsContract { } /** - * Create a new document under a specific parent document with the given - * display name and MIME type. + * Create a new document with given MIME type and display name. * - * @param parentDocumentUri document with - * {@link Documents#FLAG_SUPPORTS_CREATE} - * @param displayName name for new document - * @param mimeType MIME type for new document, which cannot be changed - * @return newly created document Uri, or {@code null} if failed + * @param parentDocumentUri directory with + * {@link Document#FLAG_DIR_SUPPORTS_CREATE} + * @param mimeType MIME type of new document + * @param displayName name of new document + * @return newly created document, or {@code null} if failed */ - public static Uri createDocument( - ContentResolver resolver, Uri parentDocumentUri, String displayName, String mimeType) { - final ContentValues values = new ContentValues(); - values.put(DocumentColumns.MIME_TYPE, mimeType); - values.put(DocumentColumns.DISPLAY_NAME, displayName); - return resolver.insert(parentDocumentUri, values); + public static Uri createDocument(ContentResolver resolver, Uri parentDocumentUri, + String mimeType, String displayName) { + final Bundle in = new Bundle(); + in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(parentDocumentUri)); + in.putString(Document.COLUMN_MIME_TYPE, mimeType); + in.putString(Document.COLUMN_DISPLAY_NAME, displayName); + + try { + final Bundle out = resolver.call(parentDocumentUri, METHOD_CREATE_DOCUMENT, null, in); + return buildDocumentUri( + parentDocumentUri.getAuthority(), out.getString(Document.COLUMN_DOCUMENT_ID)); + } catch (Exception e) { + Log.w(TAG, "Failed to create document", e); + return null; + } } /** - * Rename the document at the given URI. Given document must have - * {@link Documents#FLAG_SUPPORTS_RENAME} set. + * Delete the given document. * - * @return if rename was successful. + * @param documentUri document with {@link Document#FLAG_SUPPORTS_DELETE} */ - public static boolean renameDocument( - ContentResolver resolver, Uri documentUri, String displayName) { - final ContentValues values = new ContentValues(); - values.put(DocumentColumns.DISPLAY_NAME, displayName); - return (resolver.update(documentUri, values, null, null) == 1); - } + public static boolean deleteDocument(ContentResolver resolver, Uri documentUri) { + final Bundle in = new Bundle(); + in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(documentUri)); - /** - * Notify the system that roots have changed for the given storage provider. - * This signal is used to invalidate internal caches. - */ - public static void notifyRootsChanged(Context context, String authority) { - final Intent intent = new Intent(ACTION_DOCUMENT_CHANGED); - intent.setData(buildRootsUri(authority)); - context.sendBroadcast(intent); + try { + final Bundle out = resolver.call(documentUri, METHOD_DELETE_DOCUMENT, null, in); + return true; + } catch (Exception e) { + Log.w(TAG, "Failed to delete document", e); + return false; + } } } diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java new file mode 100644 index 0000000..09f4866 --- /dev/null +++ b/core/java/android/provider/DocumentsProvider.java @@ -0,0 +1,419 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.provider; + +import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE; +import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT; +import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT; +import static android.provider.DocumentsContract.getDocumentId; +import static android.provider.DocumentsContract.getRootId; +import static android.provider.DocumentsContract.getSearchDocumentsQuery; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.UriMatcher; +import android.content.pm.ProviderInfo; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.graphics.Point; +import android.net.Uri; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.ParcelFileDescriptor.OnCloseListener; +import android.provider.DocumentsContract.Document; +import android.util.Log; + +import libcore.io.IoUtils; + +import java.io.FileNotFoundException; + +/** + * Base class for a document provider. A document provider should extend this + * class and implement the abstract methods. + * <p> + * Each document provider expresses one or more "roots" which each serve as the + * top-level of a tree. For example, a root could represent an account, or a + * physical storage device. Under each root, documents are referenced by + * {@link Document#COLUMN_DOCUMENT_ID}, which must not change once returned. + * <p> + * Documents can be either an openable file (with a specific MIME type), or a + * directory containing additional documents (with the + * {@link Document#MIME_TYPE_DIR} MIME type). Each document can have different + * capabilities, as described by {@link Document#COLUMN_FLAGS}. The same + * {@link Document#COLUMN_DOCUMENT_ID} can be included in multiple directories. + * <p> + * Document providers must be protected with the + * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can + * only be requested by the system. The system-provided UI then issues narrow + * Uri permission grants for individual documents when the user explicitly picks + * documents. + * + * @see Intent#ACTION_OPEN_DOCUMENT + * @see Intent#ACTION_CREATE_DOCUMENT + */ +public abstract class DocumentsProvider extends ContentProvider { + private static final String TAG = "DocumentsProvider"; + + private static final int MATCH_ROOT = 1; + private static final int MATCH_RECENT = 2; + private static final int MATCH_DOCUMENT = 3; + private static final int MATCH_CHILDREN = 4; + private static final int MATCH_SEARCH = 5; + + private String mAuthority; + + private UriMatcher mMatcher; + + /** + * Implementation is provided by the parent class. + */ + @Override + public void attachInfo(Context context, ProviderInfo info) { + mAuthority = info.authority; + + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mMatcher.addURI(mAuthority, "root", MATCH_ROOT); + mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT); + mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT); + mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN); + mMatcher.addURI(mAuthority, "document/*/search", MATCH_SEARCH); + + // Sanity check our setup + if (!info.exported) { + throw new SecurityException("Provider must be exported"); + } + if (!info.grantUriPermissions) { + throw new SecurityException("Provider must grantUriPermissions"); + } + if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission) + || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) { + throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS"); + } + + super.attachInfo(context, info); + } + + /** + * Create a new document and return its {@link Document#COLUMN_DOCUMENT_ID}. + * A provider must allocate a new {@link Document#COLUMN_DOCUMENT_ID} to + * represent the document, which must not change once returned. + * + * @param documentId the parent directory to create the new document under. + * @param mimeType the MIME type associated with the new document. + * @param displayName the display name of the new document. + */ + @SuppressWarnings("unused") + public String createDocument(String documentId, String mimeType, String displayName) + throws FileNotFoundException { + throw new UnsupportedOperationException("Create not supported"); + } + + /** + * Delete the given document. Upon returning, any Uri permission grants for + * the given document will be revoked. If additional documents were deleted + * as a side effect of this call, such as documents inside a directory, the + * implementor is responsible for revoking those permissions. + * + * @param documentId the document to delete. + */ + @SuppressWarnings("unused") + public void deleteDocument(String documentId) throws FileNotFoundException { + throw new UnsupportedOperationException("Delete not supported"); + } + + public abstract Cursor queryRoots(String[] projection) throws FileNotFoundException; + + @SuppressWarnings("unused") + public Cursor queryRecentDocuments(String rootId, String[] projection) + throws FileNotFoundException { + throw new UnsupportedOperationException("Recent not supported"); + } + + /** + * Return metadata for the given document. A provider should avoid making + * network requests to keep this request fast. + * + * @param documentId the document to return. + */ + public abstract Cursor queryDocument(String documentId, String[] projection) + throws FileNotFoundException; + + /** + * Return the children of the given document which is a directory. + * + * @param parentDocumentId the directory to return children for. + */ + public abstract Cursor queryChildDocuments( + String parentDocumentId, String[] projection, String sortOrder) + throws FileNotFoundException; + + /** + * Return documents that that match the given query, starting the search at + * the given directory. + * + * @param parentDocumentId the directory to start search at. + */ + @SuppressWarnings("unused") + public Cursor querySearchDocuments(String parentDocumentId, String query, String[] projection) + throws FileNotFoundException { + throw new UnsupportedOperationException("Search not supported"); + } + + /** + * Return MIME type for the given document. Must match the value of + * {@link Document#COLUMN_MIME_TYPE} for this document. + */ + public String getDocumentType(String documentId) throws FileNotFoundException { + final Cursor cursor = queryDocument(documentId, null); + try { + if (cursor.moveToFirst()) { + return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); + } else { + return null; + } + } finally { + IoUtils.closeQuietly(cursor); + } + } + + /** + * Open and return the requested document. A provider should return a + * reliable {@link ParcelFileDescriptor} to detect when the remote caller + * has finished reading or writing the document. A provider may return a + * pipe or socket pair if the mode is exclusively + * {@link ParcelFileDescriptor#MODE_READ_ONLY} or + * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like + * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on + * disk. If a provider blocks while downloading content, it should + * periodically check {@link CancellationSignal#isCanceled()} to abort + * abandoned open requests. + * + * @param docId the document to return. + * @param mode the mode to open with, such as 'r', 'w', or 'rw'. + * @param signal used by the caller to signal if the request should be + * cancelled. + * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler, + * OnCloseListener) + * @see ParcelFileDescriptor#createReliablePipe() + * @see ParcelFileDescriptor#createReliableSocketPair() + */ + public abstract ParcelFileDescriptor openDocument( + String docId, String mode, CancellationSignal signal) throws FileNotFoundException; + + /** + * Open and return a thumbnail of the requested document. A provider should + * return a thumbnail closely matching the hinted size, attempting to serve + * from a local cache if possible. A provider should never return images + * more than double the hinted size. If a provider performs expensive + * operations to download or generate a thumbnail, it should periodically + * check {@link CancellationSignal#isCanceled()} to abort abandoned + * thumbnail requests. + * + * @param docId the document to return. + * @param sizeHint hint of the optimal thumbnail dimensions. + * @param signal used by the caller to signal if the request should be + * cancelled. + * @see Document#FLAG_SUPPORTS_THUMBNAIL + */ + @SuppressWarnings("unused") + public AssetFileDescriptor openDocumentThumbnail( + String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { + throw new UnsupportedOperationException("Thumbnails not supported"); + } + + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #queryRoots(String[]) + * @see #queryRecentDocuments(String, String[]) + * @see #queryDocument(String, String[]) + * @see #queryChildDocuments(String, String[], String) + * @see #querySearchDocuments(String, String, String[]) + */ + @Override + public final Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + try { + switch (mMatcher.match(uri)) { + case MATCH_ROOT: + return queryRoots(projection); + case MATCH_RECENT: + return queryRecentDocuments(getRootId(uri), projection); + case MATCH_DOCUMENT: + return queryDocument(getDocumentId(uri), projection); + case MATCH_CHILDREN: + return queryChildDocuments(getDocumentId(uri), projection, sortOrder); + case MATCH_SEARCH: + return querySearchDocuments( + getDocumentId(uri), getSearchDocumentsQuery(uri), projection); + default: + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + } catch (FileNotFoundException e) { + Log.w(TAG, "Failed during query", e); + return null; + } + } + + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #getDocumentType(String) + */ + @Override + public final String getType(Uri uri) { + try { + switch (mMatcher.match(uri)) { + case MATCH_DOCUMENT: + return getDocumentType(getDocumentId(uri)); + default: + return null; + } + } catch (FileNotFoundException e) { + Log.w(TAG, "Failed during getType", e); + return null; + } + } + + /** + * Implementation is provided by the parent class. Throws by default, and + * cannot be overriden. + * + * @see #createDocument(String, String, String) + */ + @Override + public final Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("Insert not supported"); + } + + /** + * Implementation is provided by the parent class. Throws by default, and + * cannot be overriden. + * + * @see #deleteDocument(String) + */ + @Override + public final int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Delete not supported"); + } + + /** + * Implementation is provided by the parent class. Throws by default, and + * cannot be overriden. + */ + @Override + public final int update( + Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Update not supported"); + } + + /** {@hide} */ + @Override + public final Bundle callFromPackage( + String callingPackage, String method, String arg, Bundle extras) { + if (!method.startsWith("android:")) { + // Let non-platform methods pass through + return super.callFromPackage(callingPackage, method, arg, extras); + } + + // Require that caller can manage given document + final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID); + final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId); + getContext().enforceCallingOrSelfUriPermission( + documentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, method); + + final Bundle out = new Bundle(); + try { + if (METHOD_CREATE_DOCUMENT.equals(method)) { + final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); + final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); + + // TODO: issue Uri grant towards calling package + // TODO: enforce that package belongs to caller + final String newDocumentId = createDocument(documentId, mimeType, displayName); + out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId); + + } else if (METHOD_DELETE_DOCUMENT.equals(method)) { + final String docId = extras.getString(Document.COLUMN_DOCUMENT_ID); + deleteDocument(docId); + + } else { + throw new UnsupportedOperationException("Method not supported " + method); + } + } catch (FileNotFoundException e) { + throw new IllegalStateException("Failed call " + method, e); + } + return out; + } + + /** + * Implementation is provided by the parent class. + * + * @see #openDocument(String, String, CancellationSignal) + */ + @Override + public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + return openDocument(getDocumentId(uri), mode, null); + } + + /** + * Implementation is provided by the parent class. + * + * @see #openDocument(String, String, CancellationSignal) + */ + @Override + public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) + throws FileNotFoundException { + return openDocument(getDocumentId(uri), mode, signal); + } + + /** + * Implementation is provided by the parent class. + * + * @see #openDocumentThumbnail(String, Point, CancellationSignal) + */ + @Override + public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) + throws FileNotFoundException { + if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { + final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); + return openDocumentThumbnail(getDocumentId(uri), sizeHint, null); + } else { + return super.openTypedAssetFile(uri, mimeTypeFilter, opts); + } + } + + /** + * Implementation is provided by the parent class. + * + * @see #openDocumentThumbnail(String, Point, CancellationSignal) + */ + @Override + public final AssetFileDescriptor openTypedAssetFile( + Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) + throws FileNotFoundException { + if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { + final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); + return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal); + } else { + return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal); + } + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 0b51b8a..83e1544 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -690,6 +690,19 @@ public final class Settings { public static final String ACTION_NOTIFICATION_LISTENER_SETTINGS = "android.settings.NOTIFICATION_LISTENER_SETTINGS"; + /** + * Activity Action: Show settings for video captioning. + * <p> + * In some cases, a matching Activity may not exist, so ensure you safeguard + * against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CAPTIONING_SETTINGS = "android.settings.CAPTIONING_SETTINGS"; + // End of Intent actions for Settings /** @@ -2895,6 +2908,11 @@ public final class Settings { /** @hide */ public static int getIntForUser(ContentResolver cr, String name, int def, int userHandle) { + if (LOCATION_MODE.equals(name)) { + // HACK ALERT: temporary hack to work around b/10491283. + // TODO: once b/10491283 fixed, remove this hack + return getLocationModeForUser(cr, userHandle); + } String v = getStringForUser(cr, name, userHandle); try { return v != null ? Integer.parseInt(v) : def; @@ -2929,6 +2947,11 @@ public final class Settings { /** @hide */ public static int getIntForUser(ContentResolver cr, String name, int userHandle) throws SettingNotFoundException { + if (LOCATION_MODE.equals(name)) { + // HACK ALERT: temporary hack to work around b/10491283. + // TODO: once b/10491283 fixed, remove this hack + return getLocationModeForUser(cr, userHandle); + } String v = getStringForUser(cr, name, userHandle); try { return Integer.parseInt(v); @@ -2957,6 +2980,11 @@ public final class Settings { /** @hide */ public static boolean putIntForUser(ContentResolver cr, String name, int value, int userHandle) { + if (LOCATION_MODE.equals(name)) { + // HACK ALERT: temporary hack to work around b/10491283. + // TODO: once b/10491283 fixed, remove this hack + return setLocationModeForUser(cr, value, userHandle); + } return putStringForUser(cr, name, Integer.toString(value), userHandle); } @@ -3265,11 +3293,25 @@ public final class Settings { /** * Comma-separated list of location providers that activities may access. + * + * @deprecated use {@link #LOCATION_MODE} */ + @Deprecated public static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed"; /** - * Location access disabled + * The degree of location access enabled by the user. + * <p/> + * When used with {@link #putInt(ContentResolver, String, int)}, must be one of {@link + * #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY}, {@link + * #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}. When used with {@link + * #getInt(ContentResolver, String)}, the caller must gracefully handle additional location + * modes that might be added in the future. + */ + public static final String LOCATION_MODE = "location_mode"; + + /** + * Location access disabled. */ public static final int LOCATION_MODE_OFF = 0; /** @@ -3559,7 +3601,7 @@ public final class Settings { * <li>{@link #ACCESSIBILITY_CAPTIONING_EDGE_COLOR} * <li>{@link #ACCESSIBILITY_CAPTIONING_EDGE_TYPE} * <li>{@link #ACCESSIBILITY_CAPTIONING_TYPEFACE} - * <li>{@link #ACCESSIBILITY_CAPTIONING_FONT_SIZE} + * <li>{@link #ACCESSIBILITY_CAPTIONING_FONT_SCALE} * </ul> * * @hide @@ -3581,9 +3623,8 @@ public final class Settings { * Integer property that specifies the preset style for captions, one * of: * <ul> - * <li>{@link android.view.accessibility.CaptioningManager#PRESET_WHITE_ON_BLACK} - * <li>{@link android.view.accessibility.CaptioningManager#PRESET_BLACK_ON_WHITE} - * <li>{@link android.view.accessibility.CaptioningManager#PRESET_CUSTOM} + * <li>{@link android.view.accessibility.CaptioningManager.CaptionStyle#PRESET_CUSTOM} + * <li>a valid index of {@link android.view.accessibility.CaptioningManager.CaptionStyle#PRESETS} * </ul> * * @see java.util.Locale#toString @@ -3615,9 +3656,9 @@ public final class Settings { /** * Integer property that specifes the edge type for captions, one of: * <ul> - * <li>{@link android.view.accessibility.CaptioningManager#EDGE_TYPE_NONE} - * <li>{@link android.view.accessibility.CaptioningManager#EDGE_TYPE_OUTLINE} - * <li>{@link android.view.accessibility.CaptioningManager#EDGE_TYPE_DROP_SHADOWED} + * <li>{@link android.view.accessibility.CaptioningManager.CaptionStyle#EDGE_TYPE_NONE} + * <li>{@link android.view.accessibility.CaptioningManager.CaptionStyle#EDGE_TYPE_OUTLINE} + * <li>{@link android.view.accessibility.CaptioningManager.CaptionStyle#EDGE_TYPE_DROP_SHADOW} * </ul> * * @see #ACCESSIBILITY_CAPTIONING_EDGE_COLOR @@ -3653,13 +3694,12 @@ public final class Settings { "accessibility_captioning_typeface"; /** - * Integer point property that specifies font size for captions in - * scaled pixels (sp). + * Floating point property that specifies font scaling for captions. * * @hide */ - public static final String ACCESSIBILITY_CAPTIONING_FONT_SIZE = - "accessibility_captioning_font_size"; + public static final String ACCESSIBILITY_CAPTIONING_FONT_SCALE = + "accessibility_captioning_font_scale"; /** * The timout for considering a press to be a long press in milliseconds. @@ -3668,13 +3708,22 @@ public final class Settings { public static final String LONG_PRESS_TIMEOUT = "long_press_timeout"; /** - * List of the enabled print providers. + * List of the enabled print services. * @hide */ public static final String ENABLED_PRINT_SERVICES = "enabled_print_services"; /** + * List of the system print services we enabled on first boot. On + * first boot we enable all system, i.e. bundled print services, + * once, so they work out-of-the-box. + * @hide + */ + public static final String ENABLED_ON_FIRST_BOOT_SYSTEM_PRINT_SERVICES = + "enabled_on_first_boot_system_print_services"; + + /** * Setting to always use the default text-to-speech settings regardless * of the application settings. * 1 = override application settings, @@ -4256,12 +4305,6 @@ public final class Settings { public static final String NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component"; /** - * Whether to automatically invoke NFC payment app or manually select on tap. - * @hide - */ - public static final String NFC_PAYMENT_MODE = "nfc_payment_mode"; - - /** * Name of a package that the current user has explicitly allowed to see all of that * user's notifications. * @@ -4304,7 +4347,7 @@ public final class Settings { ACCESSIBILITY_CAPTIONING_EDGE_TYPE, ACCESSIBILITY_CAPTIONING_EDGE_COLOR, ACCESSIBILITY_CAPTIONING_TYPEFACE, - ACCESSIBILITY_CAPTIONING_FONT_SIZE, + ACCESSIBILITY_CAPTIONING_FONT_SCALE, TTS_USE_DEFAULTS, TTS_DEFAULT_RATE, TTS_DEFAULT_PITCH, @@ -4328,7 +4371,7 @@ public final class Settings { * @param cr the content resolver to use * @param provider the location provider to query * @return true if the provider is enabled - * @deprecated use {@link #getLocationMode(ContentResolver)} + * @deprecated use {@link #getInt(ContentResolver, String)} and {@link #LOCATION_MODE} */ @Deprecated public static final boolean isLocationProviderEnabled(ContentResolver cr, String provider) { @@ -4341,7 +4384,8 @@ public final class Settings { * @param provider the location provider to query * @param userId the userId to query * @return true if the provider is enabled - * @deprecated use {@link #getLocationModeForUser(ContentResolver, int)} + * @deprecated use {@link #getIntForUser(ContentResolver, String, int, int)} and + * {@link #LOCATION_MODE} * @hide */ @Deprecated @@ -4356,7 +4400,7 @@ public final class Settings { * @param cr the content resolver to use * @param provider the location provider to enable or disable * @param enabled true if the provider should be enabled - * @deprecated use {@link #setLocationMode(ContentResolver, int)} + * @deprecated use {@link #putInt(ContentResolver, String, int)} and {@link #LOCATION_MODE} */ @Deprecated public static final void setLocationProviderEnabled(ContentResolver cr, @@ -4366,15 +4410,18 @@ public final class Settings { /** * Thread-safe method for enabling or disabling a single location provider. + * * @param cr the content resolver to use * @param provider the location provider to enable or disable * @param enabled true if the provider should be enabled * @param userId the userId for which to enable/disable providers - * @deprecated use {@link #setLocationModeForUser(ContentResolver, int, int)} + * @return true if the value was set, false on database errors + * @deprecated use {@link #putIntForUser(ContentResolver, String, int, int)} and + * {@link #LOCATION_MODE} * @hide */ @Deprecated - public static final void setLocationProviderEnabledForUser(ContentResolver cr, + public static final boolean setLocationProviderEnabledForUser(ContentResolver cr, String provider, boolean enabled, int userId) { synchronized (mLocationSettingsLock) { // to ensure thread safety, we write the provider name with a '+' or '-' @@ -4385,7 +4432,7 @@ public final class Settings { } else { provider = "-" + provider; } - putStringForUser(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, provider, + return putStringForUser(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, provider, userId); } } @@ -4398,10 +4445,12 @@ public final class Settings { * @param cr the content resolver to use * @param mode such as {@link #LOCATION_MODE_HIGH_ACCURACY} * @param userId the userId for which to change mode + * @return true if the value was set, false on database errors * * @throws IllegalArgumentException if mode is not one of the supported values */ - public static final void setLocationModeForUser(ContentResolver cr, int mode, int userId) { + private static final boolean setLocationModeForUser(ContentResolver cr, int mode, + int userId) { synchronized (mLocationSettingsLock) { boolean gps = false; boolean network = false; @@ -4421,28 +4470,15 @@ public final class Settings { default: throw new IllegalArgumentException("Invalid location mode: " + mode); } - Settings.Secure.setLocationProviderEnabledForUser( + boolean gpsSuccess = Settings.Secure.setLocationProviderEnabledForUser( cr, LocationManager.GPS_PROVIDER, gps, userId); - Settings.Secure.setLocationProviderEnabledForUser( + boolean nlpSuccess = Settings.Secure.setLocationProviderEnabledForUser( cr, LocationManager.NETWORK_PROVIDER, network, userId); + return gpsSuccess && nlpSuccess; } } /** - * Thread-safe method for setting the location mode to one of - * {@link #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY}, - * {@link #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}. - * - * @param cr the content resolver to use - * @param mode such as {@link #LOCATION_MODE_HIGH_ACCURACY} - * - * @throws IllegalArgumentException if mode is not one of the supported values - */ - public static final void setLocationMode(ContentResolver cr, int mode) { - setLocationModeForUser(cr, mode, UserHandle.myUserId()); - } - - /** * Thread-safe method for reading the location mode, returns one of * {@link #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY}, * {@link #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}. @@ -4451,7 +4487,7 @@ public final class Settings { * @param userId the userId for which to read the mode * @return the location mode */ - public static final int getLocationModeForUser(ContentResolver cr, int userId) { + private static final int getLocationModeForUser(ContentResolver cr, int userId) { synchronized (mLocationSettingsLock) { boolean gpsEnabled = Settings.Secure.isLocationProviderEnabledForUser( cr, LocationManager.GPS_PROVIDER, userId); @@ -4468,18 +4504,6 @@ public final class Settings { } } } - - /** - * Thread-safe method for reading the location mode, returns one of - * {@link #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY}, - * {@link #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}. - * - * @param cr the content resolver to use - * @return the location mode - */ - public static final int getLocationMode(ContentResolver cr) { - return getLocationModeForUser(cr, UserHandle.myUserId()); - } } /** diff --git a/core/java/android/security/IKeystoreService.java b/core/java/android/security/IKeystoreService.java index 3d75dc8..bf8d4e5 100644 --- a/core/java/android/security/IKeystoreService.java +++ b/core/java/android/security/IKeystoreService.java @@ -244,7 +244,8 @@ public interface IKeystoreService extends IInterface { return _result; } - public int generate(String name, int uid, int flags) throws RemoteException { + public int generate(String name, int uid, int keyType, int keySize, int flags, + byte[][] args) throws RemoteException { Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); int _result; @@ -252,7 +253,17 @@ public interface IKeystoreService extends IInterface { _data.writeInterfaceToken(DESCRIPTOR); _data.writeString(name); _data.writeInt(uid); + _data.writeInt(keyType); + _data.writeInt(keySize); _data.writeInt(flags); + if (args == null) { + _data.writeInt(0); + } else { + _data.writeInt(args.length); + for (int i = 0; i < args.length; i++) { + _data.writeByteArray(args[i]); + } + } mRemote.transact(Stub.TRANSACTION_generate, _data, _reply, 0); _reply.readException(); _result = _reply.readInt(); @@ -560,7 +571,8 @@ public interface IKeystoreService extends IInterface { public int zero() throws RemoteException; - public int generate(String name, int uid, int flags) throws RemoteException; + public int generate(String name, int uid, int keyType, int keySize, int flags, byte[][] args) + throws RemoteException; public int import_key(String name, byte[] data, int uid, int flags) throws RemoteException; diff --git a/core/java/android/speech/hotword/HotwordRecognitionListener.java b/core/java/android/speech/hotword/HotwordRecognitionListener.java new file mode 100644 index 0000000..8e62373 --- /dev/null +++ b/core/java/android/speech/hotword/HotwordRecognitionListener.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.speech.hotword; + +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; + +/** + * Used for receiving notifications from the HotwordRecognitionService when the + * hotword recognition related events occur. + * All the callbacks are executed on the application main thread. + * {@hide} + */ +public interface HotwordRecognitionListener { + /** + * Called when the service starts listening for hotword. + */ + void onHotwordRecognitionStarted(); + + /** + * Called when the service stops listening for hotword. + */ + void onHotwordRecognitionStopped(); + + /** + * Called on an event of interest to the client. + * + * @param eventType the event type. + * @param eventBundle a Bundle containing the hotword event(s). + */ + void onHotwordEvent(int eventType, Bundle eventBundle); + + /** + * Called back when hotword is detected. + * The action tells the client what action to take, post hotword-detection. + */ + void onHotwordRecognized(PendingIntent intent); + + /** + * Called when the HotwordRecognitionService encounters an error. + * + * @param errorCode the error code describing the error that was encountered. + */ + void onHotwordError(int errorCode); +}
\ No newline at end of file diff --git a/core/java/android/speech/hotword/HotwordRecognitionService.java b/core/java/android/speech/hotword/HotwordRecognitionService.java new file mode 100644 index 0000000..521d06d --- /dev/null +++ b/core/java/android/speech/hotword/HotwordRecognitionService.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.speech.hotword; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +/** + * This class provides a base class for hotword detection service implementations. + * This class should be extended only if you wish to implement a new hotword recognizer. + * {@hide} + */ +public abstract class HotwordRecognitionService extends Service { + /** + * The {@link Intent} that must be declared as handled by the service. + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.speech.hotword.HotwordRecognitionService"; + + /** Log messages identifier */ + private static final String TAG = "HotwordRecognitionService"; + + /** Debugging flag */ + // TODO: Turn off. + private static final boolean DBG = true; + + private static final int MSG_START_RECOGNITION = 1; + private static final int MSG_STOP_RECOGNITION = 2; + + /** + * The current callback of an application that invoked the + * {@link HotwordRecognitionService#onStartHotwordRecognition(Callback)} method + */ + private Callback mCurrentCallback = null; + + // Handle the client dying. + private final IBinder.DeathRecipient mCallbackDeathRecipient = new IBinder.DeathRecipient() { + @Override + public void binderDied() { + if (DBG) Log.i(TAG, "HotwordRecognitionService listener died"); + mCurrentCallback = null; + } + }; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_START_RECOGNITION: + dispatchStartRecognition((IHotwordRecognitionListener) msg.obj); + break; + case MSG_STOP_RECOGNITION: + dispatchStopRecognition((IHotwordRecognitionListener) msg.obj); + break; + } + } + }; + + /** Binder of the hotword recognition service */ + private RecognitionServiceBinder mBinder = new RecognitionServiceBinder(this); + + private void dispatchStartRecognition(IHotwordRecognitionListener listener) { + if (mCurrentCallback == null) { + if (DBG) Log.d(TAG, "created new mCurrentCallback, listener = " + listener.asBinder()); + try { + listener.asBinder().linkToDeath(mCallbackDeathRecipient, 0); + } catch (RemoteException e) { + if (DBG) Log.d(TAG, "listener died before linkToDeath()"); + } + mCurrentCallback = new Callback(listener); + HotwordRecognitionService.this.onStartHotwordRecognition(mCurrentCallback); + } else { + try { + listener.onHotwordError(HotwordRecognizer.ERROR_RECOGNIZER_BUSY); + } catch (RemoteException e) { + if (DBG) Log.d(TAG, "onError call from startRecognition failed"); + } + if (DBG) Log.d(TAG, "concurrent startRecognition received - ignoring this call"); + } + } + + private void dispatchStopRecognition(IHotwordRecognitionListener listener) { + try { + if (mCurrentCallback == null) { + listener.onHotwordError(HotwordRecognizer.ERROR_CLIENT); + Log.w(TAG, "stopRecognition called with no preceding startRecognition - ignoring"); + } else if (mCurrentCallback.mListener.asBinder() != listener.asBinder()) { + listener.onHotwordError(HotwordRecognizer.ERROR_RECOGNIZER_BUSY); + Log.w(TAG, "stopRecognition called by a different caller - ignoring"); + } else { // the correct state + mCurrentCallback.onHotwordRecognitionStopped(); + mCurrentCallback = null; + HotwordRecognitionService.this.onStopHotwordRecognition(); + } + } catch (RemoteException e) { // occurs if onError fails + if (DBG) Log.d(TAG, "onError call from stopRecognition failed"); + } + } + + @Override + public IBinder onBind(final Intent intent) { + if (DBG) Log.d(TAG, "onBind, intent=" + intent); + return mBinder; + } + + @Override + public void onDestroy() { + if (DBG) Log.d(TAG, "onDestroy"); + if (mCurrentCallback != null) { + mCurrentCallback.mListener.asBinder().unlinkToDeath(mCallbackDeathRecipient, 0); + mCurrentCallback = null; + } + mBinder.clearReference(); + super.onDestroy(); + } + + /** + * Notifies the service to start a recognition. + * + * @param callback that receives the callbacks from the service. + */ + public abstract void onStartHotwordRecognition(Callback callback); + + /** + * Notifies the service to stop recognition. + */ + public abstract void onStopHotwordRecognition(); + + /** Binder of the hotword recognition service */ + private static class RecognitionServiceBinder extends IHotwordRecognitionService.Stub { + private HotwordRecognitionService mInternalService; + + public RecognitionServiceBinder(HotwordRecognitionService service) { + mInternalService = service; + } + + public void startHotwordRecognition(IHotwordRecognitionListener listener) { + if (DBG) Log.d(TAG, "startRecognition called by: " + listener.asBinder()); + if (mInternalService != null) { + mInternalService.mHandler.sendMessage( + Message.obtain(mInternalService.mHandler, MSG_START_RECOGNITION, listener)); + } + } + + public void stopHotwordRecognition(IHotwordRecognitionListener listener) { + if (DBG) Log.d(TAG, "stopRecognition called by: " + listener.asBinder()); + if (mInternalService != null) { + mInternalService.mHandler.sendMessage( + Message.obtain(mInternalService.mHandler, MSG_STOP_RECOGNITION, listener)); + } + } + + private void clearReference() { + mInternalService = null; + } + } + + /** + * This class acts passes on the callbacks received from the Hotword service + * to the listener. + */ + public static class Callback { + private final IHotwordRecognitionListener mListener; + + private Callback(IHotwordRecognitionListener listener) { + mListener = listener; + } + + /** + * Called when the service starts listening for hotword. + */ + public void onHotwordRecognitionStarted() throws RemoteException { + mListener.onHotwordRecognitionStarted(); + } + + /** + * Called when the service starts listening for hotword. + */ + public void onHotwordRecognitionStopped() throws RemoteException { + mListener.onHotwordRecognitionStopped(); + } + + /** + * Called on an event of interest to the client. + * + * @param eventType the event type. Event types are defined in {@link HotwordRecognizer}. + * @param eventBundle a Bundle containing the hotword event(s). + */ + public void onHotwordEvent(int eventType, Bundle eventBundle) throws RemoteException { + mListener.onHotwordEvent(eventType, eventBundle); + } + + /** + * Called back when hotword is detected. + * The action tells the client what action to take, post hotword-detection. + */ + public void onHotwordRecognized(PendingIntent intent) throws RemoteException { + mListener.onHotwordRecognized(intent); + } + + /** + * Called when the HotwordRecognitionService encounters an error. + * + * @param errorCode the error code describing the error that was encountered. + * Error codes are defined in {@link HotwordRecognizer}. + */ + public void onError(int errorCode) throws RemoteException { + mListener.onHotwordError(errorCode); + } + } +} diff --git a/core/java/android/speech/hotword/HotwordRecognizer.java b/core/java/android/speech/hotword/HotwordRecognizer.java new file mode 100644 index 0000000..82cec10 --- /dev/null +++ b/core/java/android/speech/hotword/HotwordRecognizer.java @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.speech.hotword; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +/** + * This class provides access to the Hotword recognition service. + * This class's methods must be invoked on the main application thread. + * {@hide} + */ +public class HotwordRecognizer { + /** DEBUG value to enable verbose debug prints */ + // TODO: Turn off. + private final static boolean DBG = true; + + /** Log messages identifier */ + private static final String TAG = "HotwordRecognizer"; + + /** + * Key used to retrieve a string to be displayed to the user passed to the + * {@link android.speech.hotword.HotwordRecognitionListener#onHotwordEvent(int, Bundle)} method. + */ + public static final String PROMPT_TEXT = "prompt_text"; + + /** + * Event type used to indicate to the user that the hotword service has changed + * its state. + */ + public static final int EVENT_TYPE_STATE_CHANGED = 1; + + /** Audio recording error. */ + public static final int ERROR_AUDIO = 1; + + /** RecognitionService busy. */ + public static final int ERROR_RECOGNIZER_BUSY = 2; + + /** This indicates a permanent failure and the clients shouldn't retry on this */ + public static final int ERROR_FAILED = 3; + + /** Client-side errors */ + public static final int ERROR_CLIENT = 4; + + /** The service timed out */ + public static final int ERROR_TIMEOUT = 5; + + /** The service received concurrent start calls */ + public static final int ERROR_SERVICE_ALREADY_STARTED = 6; + + /** action codes */ + private static final int MSG_START = 1; + private static final int MSG_STOP = 2; + + /** The underlying HotwordRecognitionService endpoint */ + private IHotwordRecognitionService mService; + + /** The connection to the actual service */ + private Connection mConnection; + + /** Context with which the manager was created */ + private final Context mContext; + + /** Component to direct service intent to */ + private final ComponentName mServiceComponent; + + /** Handler that will execute the main tasks */ + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_START: + handleStartRecognition(); + break; + case MSG_STOP: + handleStopRecognition(); + break; + } + } + }; + + /** + * Temporary queue, saving the messages until the connection will be established, afterwards, + * only mHandler will receive the messages + */ + private final Queue<Message> mPendingTasks = new LinkedList<Message>(); + + /** The Listener that will receive all the callbacks */ + private final InternalListener mListener = new InternalListener(); + + /** + * Checks whether a hotword recognition service is available on the system. If this method + * returns {@code false}, {@link HotwordRecognizer#createHotwordRecognizer(Context)} will + * fail. + * + * @param context with which {@code HotwordRecognizer} will be created + * @return {@code true} if recognition is available, {@code false} otherwise + */ + public static boolean isHotwordRecognitionAvailable(final Context context) { + final List<ResolveInfo> list = context.getPackageManager().queryIntentServices( + new Intent(HotwordRecognitionService.SERVICE_INTERFACE), 0); + return list != null && list.size() != 0; + } + + /** + * Factory method to create a new {@code HotwordRecognizer}. + * + * @param context in which to create {@code HotwordRecognizer} + * @return a new {@code HotwordRecognizer} + */ + public static HotwordRecognizer createHotwordRecognizer(final Context context) { + ComponentName serviceComponent = null; + // Resolve to a default ComponentName. + final List<ResolveInfo> list = context.getPackageManager().queryIntentServices( + new Intent(HotwordRecognitionService.SERVICE_INTERFACE), 0); + for (int i = 0; i < list.size(); i++) { + final ResolveInfo ri = list.get(i); + if (!ri.serviceInfo.enabled) { + continue; + } + if ((ri.serviceInfo.applicationInfo.flags&ApplicationInfo.FLAG_SYSTEM) + != PackageManager.MATCH_DEFAULT_ONLY) { + serviceComponent = new ComponentName( + ri.serviceInfo.packageName, ri.serviceInfo.name); + break; + } + } + // If all else fails, pick the first one. + if (serviceComponent == null && !list.isEmpty()) { + serviceComponent = new ComponentName( + list.get(0).serviceInfo.packageName, list.get(0).serviceInfo.name); + } + return createHotwordRecognizer(context, serviceComponent); + } + + /** + * Factory method to create a new {@code HotwordRecognizer}. + * + * Use this version of the method to specify a specific service to direct this + * {@link HotwordRecognizer} to. Normally you would not use this; use + * {@link #createHotwordRecognizer(Context)} instead to use the system default recognition + * service. + * + * @param context in which to create {@code HotwordRecognizer} + * @param serviceComponent the {@link ComponentName} of a specific service to direct this + * {@code HotwordRecognizer} to + * @return a new {@code HotwordRecognizer} + */ + public static HotwordRecognizer createHotwordRecognizer( + final Context context, final ComponentName serviceComponent) { + if (context == null) { + throw new IllegalArgumentException("Context cannot be null)"); + } + checkIsCalledFromMainThread(); + return new HotwordRecognizer(context, serviceComponent); + } + + /** + * Starts recognizing hotword and sets the listener that will receive the callbacks. + * + * @param listener listener that will receive all the callbacks from the created + * {@link HotwordRecognizer}, this must not be null. + */ + public void startRecognition(HotwordRecognitionListener listener) { + checkIsCalledFromMainThread(); + if (mConnection == null) { // first time connection + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + mConnection = new Connection(); + Intent serviceIntent = new Intent(HotwordRecognitionService.SERVICE_INTERFACE); + mListener.mInternalListener = listener; + + if (mServiceComponent == null) { + Log.e(TAG, "no selected voice recognition service"); + mListener.onHotwordError(ERROR_CLIENT); + return; + } else { + serviceIntent.setComponent(mServiceComponent); + } + + if (!mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE)) { + Log.e(TAG, "bind to recognition service failed"); + mConnection = null; + mService = null; + mListener.onHotwordError(ERROR_CLIENT); + return; + } + putMessage(Message.obtain(mHandler, MSG_START)); + } else { + mListener.onHotwordError(ERROR_SERVICE_ALREADY_STARTED); + return; + } + } + + /** + * Stops recognizing hotword. + */ + public void stopRecognition() { + checkIsCalledFromMainThread(); + putMessage(Message.obtain(mHandler, MSG_STOP)); + } + + // Private constructor. + private HotwordRecognizer(Context context, ComponentName serviceComponent) { + mContext = context; + mServiceComponent = serviceComponent; + } + + private void handleStartRecognition() { + if (!checkOpenConnection()) { + return; + } + try { + mService.startHotwordRecognition(mListener); + if (DBG) Log.d(TAG, "service startRecognition command succeeded"); + } catch (final RemoteException e) { + Log.e(TAG, "startRecognition() failed", e); + mListener.onHotwordError(ERROR_CLIENT); + } + } + + private void handleStopRecognition() { + if (!checkOpenConnection()) { + return; + } + try { + mService.stopHotwordRecognition(mListener); + if (mConnection != null) { + mContext.unbindService(mConnection); + } + if (DBG) Log.d(TAG, "service stopRecognition command succeeded"); + } catch (final RemoteException e) { + Log.e(TAG, "stopRecognition() failed", e); + mListener.onHotwordError(ERROR_CLIENT); + } finally { + mPendingTasks.clear(); + mService = null; + mConnection = null; + mListener.mInternalListener = null; + } + } + + private boolean checkOpenConnection() { + if (mService != null) { + return true; + } + mListener.onHotwordError(ERROR_CLIENT); + Log.e(TAG, "not connected to the recognition service"); + return false; + } + + private static void checkIsCalledFromMainThread() { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new RuntimeException( + "HotwordRecognizer should be used only from the application's main thread"); + } + } + + private void putMessage(Message msg) { + if (mService == null) { + mPendingTasks.offer(msg); + } else { + mHandler.sendMessage(msg); + } + } + + /** + * Basic ServiceConnection that records the mService variable. + * Additionally, on creation it invokes + * {@link IHotwordRecognitionService#startHotwordRecognition(IHotwordRecognitionListener)}. + */ + private class Connection implements ServiceConnection { + + public void onServiceConnected(final ComponentName name, final IBinder service) { + // always done on the application main thread, so no need to send message to mHandler + mService = IHotwordRecognitionService.Stub.asInterface(service); + if (DBG) Log.d(TAG, "onServiceConnected - Success"); + while (!mPendingTasks.isEmpty()) { + mHandler.sendMessage(mPendingTasks.poll()); + } + } + + public void onServiceDisconnected(final ComponentName name) { + // always done on the application main thread, so no need to send message to mHandler + mService = null; + mConnection = null; + mPendingTasks.clear(); + if (DBG) Log.d(TAG, "onServiceDisconnected - Success"); + } + } + + /** + * Internal wrapper of IHotwordRecognitionListener which will propagate the results to + * HotwordRecognitionListener. + */ + private class InternalListener extends IHotwordRecognitionListener.Stub { + private HotwordRecognitionListener mInternalListener; + + private final static int MSG_ON_START = 1; + private final static int MSG_ON_STOP = 2; + private final static int MSG_ON_EVENT = 3; + private final static int MSG_ON_RECOGNIZED = 4; + private final static int MSG_ON_ERROR = 5; + + private final Handler mInternalHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (mInternalListener == null) { + return; + } + switch (msg.what) { + case MSG_ON_START: + mInternalListener.onHotwordRecognitionStarted(); + break; + case MSG_ON_STOP: + mInternalListener.onHotwordRecognitionStopped(); + break; + case MSG_ON_EVENT: + mInternalListener.onHotwordEvent(msg.arg1, (Bundle) msg.obj); + break; + case MSG_ON_RECOGNIZED: + mInternalListener.onHotwordRecognized((PendingIntent) msg.obj); + break; + case MSG_ON_ERROR: + mInternalListener.onHotwordError((Integer) msg.obj); + break; + } + } + }; + + @Override + public void onHotwordRecognitionStarted() throws RemoteException { + Message.obtain(mInternalHandler, MSG_ON_START).sendToTarget(); + } + + @Override + public void onHotwordRecognitionStopped() throws RemoteException { + Message.obtain(mInternalHandler, MSG_ON_STOP).sendToTarget(); + } + + @Override + public void onHotwordEvent(final int eventType, final Bundle params) { + Message.obtain(mInternalHandler, MSG_ON_EVENT, eventType, eventType, params) + .sendToTarget(); + } + + @Override + public void onHotwordRecognized(PendingIntent intent) throws RemoteException { + Message.obtain(mInternalHandler, MSG_ON_RECOGNIZED, intent) + .sendToTarget(); + } + + @Override + public void onHotwordError(final int error) { + Message.obtain(mInternalHandler, MSG_ON_ERROR, error).sendToTarget(); + } + } +} diff --git a/core/java/android/speech/hotword/IHotwordRecognitionListener.aidl b/core/java/android/speech/hotword/IHotwordRecognitionListener.aidl new file mode 100644 index 0000000..49c5233 --- /dev/null +++ b/core/java/android/speech/hotword/IHotwordRecognitionListener.aidl @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.speech.hotword; + +import android.app.PendingIntent; +import android.os.Bundle; + +/** + * Listener for hotword detection events. + * This indicates when the hotword was detected, and also notifies the + * client of the intermediate events that may be used to show visual feedback + * to the user. + * {@hide} + */ +oneway interface IHotwordRecognitionListener { + /** + * Called when the service starts listening for hotword. + */ + void onHotwordRecognitionStarted(); + + /** + * Called when the service starts listening for hotword. + */ + void onHotwordRecognitionStopped(); + + /** + * Called on an event of interest to the client. + * + * @param eventType the event type. + * @param eventBundle a Bundle containing the hotword event(s). + */ + void onHotwordEvent(in int eventType, in Bundle eventBundle); + + /** + * Called back when hotword is detected. + * The action tells the client what action to take, post hotword-detection. + */ + void onHotwordRecognized(in PendingIntent intent); + + /** + * Called when the HotwordRecognitionService encounters an error. + * + * @param errorCode the error code describing the error that was encountered. + */ + void onHotwordError(in int errorCode); +} diff --git a/core/java/android/speech/hotword/IHotwordRecognitionService.aidl b/core/java/android/speech/hotword/IHotwordRecognitionService.aidl new file mode 100644 index 0000000..331d81c --- /dev/null +++ b/core/java/android/speech/hotword/IHotwordRecognitionService.aidl @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.speech.hotword; + +import android.speech.hotword.IHotwordRecognitionListener; + +/** + * A service interface to Hotword recognition. + * Call startHotwordDetection with a listener when you want to begin detecting + * hotword; + * The service would automatically stop detection when hotword is detected; + * So it's a create-once use-once service. + * The service doesn't support nested calls to start detection and disallows them. + * {@hide} + */ +oneway interface IHotwordRecognitionService { + /** + * Start hotword recognition. + * The clients should rely on the callback to figure out if the detection was + * started. + * + * @param listener a listener to notify of hotword events. + */ + void startHotwordRecognition(in IHotwordRecognitionListener listener); + + /** + * Stop hotword recognition. + * Stops the recognition only if it was started by the same caller. + * + * @param listener a listener to notify of hotword events. + */ + void stopHotwordRecognition(in IHotwordRecognitionListener listener); +} diff --git a/core/java/android/speech/tts/SynthesisRequest.java b/core/java/android/speech/tts/SynthesisRequest.java index 917a109..12a026b 100644 --- a/core/java/android/speech/tts/SynthesisRequest.java +++ b/core/java/android/speech/tts/SynthesisRequest.java @@ -30,7 +30,7 @@ import android.os.Bundle; * </ul> * * Any additional parameters sent to the text to speech service are passed in - * uninterpreted, see the @code{params} argument in {@link TextToSpeech#speak} + * uninterpreted, see the {@code params} argument in {@link TextToSpeech#speak} * and {@link TextToSpeech#synthesizeToFile}. */ public final class SynthesisRequest { diff --git a/core/java/android/util/LayoutDirection.java b/core/java/android/util/LayoutDirection.java index e37d2f2..20af20b 100644 --- a/core/java/android/util/LayoutDirection.java +++ b/core/java/android/util/LayoutDirection.java @@ -17,11 +17,15 @@ package android.util; /** - * An interface for defining layout directions. A layout direction can be left-to-right (LTR) + * A class for defining layout directions. A layout direction can be left-to-right (LTR) * or right-to-left (RTL). It can also be inherited (from a parent) or deduced from the default * language script of a locale. */ -public interface LayoutDirection { +public final class LayoutDirection { + + // No instantiation + private LayoutDirection() {} + /** * Horizontal layout direction is from Left to Right. */ diff --git a/core/java/android/view/ScaleGestureDetector.java b/core/java/android/view/ScaleGestureDetector.java index 51c5c7b..0bebc04 100644 --- a/core/java/android/view/ScaleGestureDetector.java +++ b/core/java/android/view/ScaleGestureDetector.java @@ -18,6 +18,8 @@ package android.view; import android.content.Context; import android.content.res.Resources; +import android.os.Build; +import android.os.Handler; import android.os.SystemClock; import android.util.FloatMath; @@ -128,6 +130,8 @@ public class ScaleGestureDetector { private float mFocusX; private float mFocusY; + private boolean mDoubleTapScales; + private float mCurrSpan; private float mPrevSpan; private float mInitialSpan; @@ -148,9 +152,14 @@ public class ScaleGestureDetector { private int mTouchHistoryDirection; private long mTouchHistoryLastAcceptedTime; private int mTouchMinMajor; + private MotionEvent mDoubleTapEvent; + private int mDoubleTapMode = DOUBLE_TAP_MODE_NONE; + private final Handler mHandler; private static final long TOUCH_STABILIZE_TIME = 128; // ms - private static final int TOUCH_MIN_MAJOR = 48; // dp + private static final int DOUBLE_TAP_MODE_NONE = 0; + private static final int DOUBLE_TAP_MODE_IN_PROGRESS = 1; + /** * Consistency verifier for debugging purposes. @@ -158,8 +167,37 @@ public class ScaleGestureDetector { private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = InputEventConsistencyVerifier.isInstrumentationEnabled() ? new InputEventConsistencyVerifier(this, 0) : null; + private GestureDetector mGestureDetector; + + private boolean mEventBeforeOrAboveStartingGestureEvent; + /** + * Creates a ScaleGestureDetector with the supplied listener. + * You may only use this constructor from a {@link android.os.Looper Looper} thread. + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * + * @throws NullPointerException if {@code listener} is null. + */ public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { + this(context, listener, null); + } + + /** + * Creates a ScaleGestureDetector with the supplied listener. + * @see android.os.Handler#Handler() + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @param handler the handler to use for running deferred listener events. + * + * @throws NullPointerException if {@code listener} is null. + */ + public ScaleGestureDetector(Context context, OnScaleGestureListener listener, + Handler handler) { mContext = context; mListener = listener; mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2; @@ -167,8 +205,12 @@ public class ScaleGestureDetector { final Resources res = context.getResources(); mTouchMinMajor = res.getDimensionPixelSize( com.android.internal.R.dimen.config_minScalingTouchMajor); - mMinSpan = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_minScalingSpan); + mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan); + mHandler = handler; + // Quick scale is enabled by default after JB_MR2 + if (context.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) { + setQuickScaleEnabled(true); + } } /** @@ -263,8 +305,14 @@ public class ScaleGestureDetector { final int action = event.getActionMasked(); + // Forward the event to check for double tap gesture + if (mDoubleTapScales) { + mGestureDetector.onTouchEvent(event); + } + final boolean streamComplete = action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL; + if (action == MotionEvent.ACTION_DOWN || streamComplete) { // Reset any scale in progress with the listener. // If it's an ACTION_DOWN we're beginning a new event stream. @@ -273,6 +321,7 @@ public class ScaleGestureDetector { mListener.onScaleEnd(this); mInProgress = false; mInitialSpan = 0; + mDoubleTapMode = DOUBLE_TAP_MODE_NONE; } if (streamComplete) { @@ -284,21 +333,37 @@ public class ScaleGestureDetector { final boolean configChanged = action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN; + + final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; final int skipIndex = pointerUp ? event.getActionIndex() : -1; // Determine focal point float sumX = 0, sumY = 0; final int count = event.getPointerCount(); - for (int i = 0; i < count; i++) { - if (skipIndex == i) continue; - sumX += event.getX(i); - sumY += event.getY(i); - } final int div = pointerUp ? count - 1 : count; - final float focusX = sumX / div; - final float focusY = sumY / div; + final float focusX; + final float focusY; + if (mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS) { + // In double tap mode, the focal pt is always where the double tap + // gesture started + focusX = mDoubleTapEvent.getX(); + focusY = mDoubleTapEvent.getY(); + if (event.getY() < focusY) { + mEventBeforeOrAboveStartingGestureEvent = true; + } else { + mEventBeforeOrAboveStartingGestureEvent = false; + } + } else { + for (int i = 0; i < count; i++) { + if (skipIndex == i) continue; + sumX += event.getX(i); + sumY += event.getY(i); + } + focusX = sumX / div; + focusY = sumY / div; + } addTouchHistory(event); @@ -320,7 +385,12 @@ public class ScaleGestureDetector { // the focal point. final float spanX = devX * 2; final float spanY = devY * 2; - final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY); + final float span; + if (inDoubleTapMode()) { + span = spanY; + } else { + span = FloatMath.sqrt(spanX * spanX + spanY * spanY); + } // Dispatch begin/end events as needed. // If the configuration changes, notify the app to reset its current state by beginning @@ -328,10 +398,11 @@ public class ScaleGestureDetector { final boolean wasInProgress = mInProgress; mFocusX = focusX; mFocusY = focusY; - if (mInProgress && (span < mMinSpan || configChanged)) { + if (!inDoubleTapMode() && mInProgress && (span < mMinSpan || configChanged)) { mListener.onScaleEnd(this); mInProgress = false; mInitialSpan = span; + mDoubleTapMode = DOUBLE_TAP_MODE_NONE; } if (configChanged) { mPrevSpanX = mCurrSpanX = spanX; @@ -354,6 +425,7 @@ public class ScaleGestureDetector { mCurrSpan = span; boolean updatePrev = true; + if (mInProgress) { updatePrev = mListener.onScale(this); } @@ -369,6 +441,34 @@ public class ScaleGestureDetector { return true; } + + private boolean inDoubleTapMode() { + return mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS; + } + + /** + * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks + * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default + * if the app targets API 19 and newer. + * @param scales true to enable quick scaling, false to disable + */ + public void setQuickScaleEnabled(boolean scales) { + mDoubleTapScales = scales; + if (mDoubleTapScales && mGestureDetector == null) { + GestureDetector.SimpleOnGestureListener gestureListener = + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onDoubleTap(MotionEvent e) { + // Double tap: start watching for a swipe + mDoubleTapEvent = e; + mDoubleTapMode = DOUBLE_TAP_MODE_IN_PROGRESS; + return true; + } + }; + mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler); + } + } + /** * Returns {@code true} if a scale gesture is in progress. */ @@ -472,6 +572,12 @@ public class ScaleGestureDetector { * @return The current scaling factor. */ public float getScaleFactor() { + if (inDoubleTapMode() && mEventBeforeOrAboveStartingGestureEvent) { + // Drag is moving up; the further away from the gesture + // start, the smaller the span should be, the closer, + // the larger the span, and therefore the larger the scale + return (1 / mCurrSpan) / (1 / mPrevSpan); + } return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; } @@ -493,4 +599,4 @@ public class ScaleGestureDetector { public long getEventTime() { return mCurrTime; } -} +}
\ No newline at end of file diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 5269ee3..907290b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -2375,20 +2375,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public static final int SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 0x00000400; /** - * Flag for {@link #setSystemUiVisibility(int)}: View would like to receive touch events - * when hiding the status bar with {@link #SYSTEM_UI_FLAG_FULLSCREEN} and/or hiding the - * navigation bar with {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION} instead of having the system - * clear these flags upon interaction. The system may compensate by temporarily overlaying - * semi-transparent system bars while also delivering the event. - */ - public static final int SYSTEM_UI_FLAG_ALLOW_TRANSIENT = 0x00000800; + * Flag for {@link #setSystemUiVisibility(int)}: View would like to remain interactive when + * hiding the status bar with {@link #SYSTEM_UI_FLAG_FULLSCREEN} and/or hiding the navigation + * bar with {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}. Use this flag to create an immersive + * experience while also hiding the system bars. If this flag is not set, + * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION} will be force cleared by the system on any user + * interaction, and {@link #SYSTEM_UI_FLAG_FULLSCREEN} will be force-cleared by the system + * if the user swipes from the top of the screen. + * <p>When system bars are hidden in immersive mode, they can be revealed temporarily with + * system gestures, such as swiping from the top of the screen. These transient system bars + * will overlay app’s content, may have some degree of transparency, and will automatically + * hide after a short timeout. + * </p><p>Since this flag is a modifier for {@link #SYSTEM_UI_FLAG_FULLSCREEN} and + * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, it only has an effect when used in combination + * with one or both of those flags.</p> + */ + public static final int SYSTEM_UI_FLAG_IMMERSIVE = 0x00000800; /** * Flag for {@link #setSystemUiVisibility(int)}: View would like the status bar to have * transparency. * * <p>The transparency request may be denied if the bar is in another mode with a specific - * style, like {@link #SYSTEM_UI_FLAG_ALLOW_TRANSIENT transient mode}. + * style, like {@link #SYSTEM_UI_FLAG_IMMERSIVE immersive mode}. */ public static final int SYSTEM_UI_FLAG_TRANSPARENT_STATUS = 0x00001000; @@ -2397,7 +2406,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * transparency. * * <p>The transparency request may be denied if the bar is in another mode with a specific - * style, like {@link #SYSTEM_UI_FLAG_ALLOW_TRANSIENT transient mode}. + * style, like {@link #SYSTEM_UI_FLAG_IMMERSIVE immersive mode}. */ public static final int SYSTEM_UI_FLAG_TRANSPARENT_NAVIGATION = 0x00002000; @@ -3390,17 +3399,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. - * @param defStyle The default style to apply to this view. If 0, no style - * will be applied (beyond what is included in the theme). This may - * either be an attribute resource, whose value will be retrieved - * from the current theme, or an explicit style resource. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource to apply to this view. If 0, no + * default style will be applied. * @see #View(Context, AttributeSet) */ - public View(Context context, AttributeSet attrs, int defStyle) { + public View(Context context, AttributeSet attrs, int defStyleAttr) { this(context); TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, - defStyle, 0); + defStyleAttr, 0); Drawable background = null; @@ -6680,8 +6688,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Returns whether this View is accessibility focused. * * @return True if this View is accessibility focused. + * @hide */ - boolean isAccessibilityFocused() { + public boolean isAccessibilityFocused() { return (mPrivateFlags2 & PFLAG2_ACCESSIBILITY_FOCUSED) != 0; } @@ -7389,7 +7398,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ public void dispatchStartTemporaryDetach() { - clearAccessibilityFocus(); clearDisplayList(); onStartTemporaryDetach(); @@ -8419,6 +8427,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Implement this method to handle touch screen motion events. + * <p> + * If this method is used to detect click actions, it is recommended that + * the actions be performed by implementing and calling + * {@link #performClick()}. This will ensure consistent system behavior, + * including: + * <ul> + * <li>obeying click sound preferences + * <li>dispatching OnClickListener calls + * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when + * accessibility features are enabled + * </ul> * * @param event The motion event. * @return True if the event was handled, false otherwise. @@ -11860,7 +11879,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, jumpDrawablesToCurrentState(); - clearAccessibilityFocus(); resetSubtreeAccessibilityStateChanged(); if (isFocused()) { @@ -13238,14 +13256,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Keep the DRAWING_CACHE_QUALITY_LOW flag just in case switch (mViewFlags & DRAWING_CACHE_QUALITY_MASK) { case DRAWING_CACHE_QUALITY_AUTO: - quality = Bitmap.Config.ARGB_8888; - break; case DRAWING_CACHE_QUALITY_LOW: - quality = Bitmap.Config.ARGB_8888; - break; case DRAWING_CACHE_QUALITY_HIGH: - quality = Bitmap.Config.ARGB_8888; - break; default: quality = Bitmap.Config.ARGB_8888; break; @@ -16654,7 +16666,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param visibility Bitwise-or of flags {@link #SYSTEM_UI_FLAG_LOW_PROFILE}, * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, {@link #SYSTEM_UI_FLAG_FULLSCREEN}, * {@link #SYSTEM_UI_FLAG_LAYOUT_STABLE}, {@link #SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}, - * {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}, {@link #SYSTEM_UI_FLAG_ALLOW_TRANSIENT}, + * {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}, {@link #SYSTEM_UI_FLAG_IMMERSIVE}, * {@link #SYSTEM_UI_FLAG_TRANSPARENT_STATUS}, * and {@link #SYSTEM_UI_FLAG_TRANSPARENT_NAVIGATION}. */ @@ -16672,7 +16684,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return Bitwise-or of flags {@link #SYSTEM_UI_FLAG_LOW_PROFILE}, * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, {@link #SYSTEM_UI_FLAG_FULLSCREEN}, * {@link #SYSTEM_UI_FLAG_LAYOUT_STABLE}, {@link #SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}, - * {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}, {@link #SYSTEM_UI_FLAG_ALLOW_TRANSIENT}, + * {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}, {@link #SYSTEM_UI_FLAG_IMMERSIVE}, * {@link #SYSTEM_UI_FLAG_TRANSPARENT_STATUS}, * and {@link #SYSTEM_UI_FLAG_TRANSPARENT_NAVIGATION}. */ diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index d68c410..03a9c37 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -3865,8 +3865,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ public void setLayoutTransition(LayoutTransition transition) { if (mTransition != null) { - mTransition.cancel(); - mTransition.removeTransitionListener(mLayoutTransitionListener); + LayoutTransition previousTransition = mTransition; + previousTransition.cancel(); + previousTransition.removeTransitionListener(mLayoutTransitionListener); } mTransition = transition; if (mTransition != null) { diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 8ae6996..26596d9 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -269,6 +269,23 @@ public interface ViewParent { /** * Called when a child view now has or no longer is tracking transient state. * + * <p>"Transient state" is any state that a View might hold that is not expected to + * be reflected in the data model that the View currently presents. This state only + * affects the presentation to the user within the View itself, such as the current + * state of animations in progress or the state of a text selection operation.</p> + * + * <p>Transient state is useful for hinting to other components of the View system + * that a particular view is tracking something complex but encapsulated. + * A <code>ListView</code> for example may acknowledge that list item Views + * with transient state should be preserved within their position or stable item ID + * instead of treating that view as trivially replaceable by the backing adapter. + * This allows adapter implementations to be simpler instead of needing to track + * the state of item view animations in progress such that they could be restored + * in the event of an unexpected recycling and rebinding of attached item views.</p> + * + * <p>This method is called on a parent view when a child view or a view within + * its subtree begins or ends tracking of internal transient state.</p> + * * @param child Child view whose state has changed * @param hasTransientState true if this child has transient state */ diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 6d54094..ba63421 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -518,7 +518,7 @@ public class AccessibilityNodeInfo implements Parcelable { private int mTextSelectionEnd = UNDEFINED; private int mInputType = InputType.TYPE_NULL; - private Bundle mBundle; + private Bundle mExtras; private int mConnectionId = UNDEFINED; @@ -1471,9 +1471,18 @@ public class AccessibilityNodeInfo implements Parcelable { } /** - * Gets if the node is a live region for whose changes the user - * should be notified. It is the responsibility of the accessibility + * Gets if the node is a live region. + * <p> + * A live region is a node that contains information that is important + * for the user and when it changes the user has to be notified. For + * example, if the user plays a video and the application shows a + * progress indicator with the percentage of buffering, then the progress + * indicator should be marked as a live region. + * </p> + * <p> + * It is the responsibility of the accessibility * service to monitor this region and notify the user if it changes. + * </p> * * @return If the node is a live region. */ @@ -1525,7 +1534,7 @@ public class AccessibilityNodeInfo implements Parcelable { * * @return If the the node opens a popup. */ - public boolean getOpensPopup() { + public boolean canOpenPopup() { return getBooleanProperty(BOOLEAN_PROPERTY_OPENS_POPUP); } @@ -1539,7 +1548,8 @@ public class AccessibilityNodeInfo implements Parcelable { * * @param opensPopup If the the node opens a popup. */ - public void setOpensPopup(boolean opensPopup) { + public void setCanOpenPopup(boolean opensPopup) { + enforceNotSealed(); setBooleanProperty(BOOLEAN_PROPERTY_OPENS_POPUP, opensPopup); } @@ -1927,7 +1937,7 @@ public class AccessibilityNodeInfo implements Parcelable { } /** - * Gets an optional bundle with additional data. The bundle + * Gets an optional bundle with extra data. The bundle * is lazily created and never <code>null</code>. * <p> * <strong>Note:</strong> It is recommended to use the package @@ -1939,11 +1949,11 @@ public class AccessibilityNodeInfo implements Parcelable { * * @return The bundle. */ - public Bundle getBundle() { - if (mBundle == null) { - mBundle = new Bundle(); + public Bundle getExtras() { + if (mExtras == null) { + mExtras = new Bundle(); } - return mBundle; + return mExtras; } /** @@ -2194,9 +2204,9 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.writeInt(mTextSelectionEnd); parcel.writeInt(mInputType); - if (mBundle != null) { + if (mExtras != null) { parcel.writeInt(1); - parcel.writeBundle(mBundle); + parcel.writeBundle(mExtras); } else { parcel.writeInt(0); } @@ -2213,8 +2223,8 @@ public class AccessibilityNodeInfo implements Parcelable { if (mCollectionInfo != null) { parcel.writeInt(1); - parcel.writeInt(mCollectionInfo.getHorizontalSize()); - parcel.writeInt(mCollectionInfo.getVerticalSize()); + parcel.writeInt(mCollectionInfo.getRowCount()); + parcel.writeInt(mCollectionInfo.getColumnCount()); parcel.writeInt(mCollectionInfo.isHierarchical() ? 1 : 0); } else { parcel.writeInt(0); @@ -2222,10 +2232,10 @@ public class AccessibilityNodeInfo implements Parcelable { if (mCollectionItemInfo != null) { parcel.writeInt(1); - parcel.writeInt(mCollectionItemInfo.getHorizontalPosition()); - parcel.writeInt(mCollectionItemInfo.getHorizontalSpan()); - parcel.writeInt(mCollectionItemInfo.getVerticalPosition()); - parcel.writeInt(mCollectionItemInfo.getVerticalSpan()); + parcel.writeInt(mCollectionItemInfo.getColumnIndex()); + parcel.writeInt(mCollectionItemInfo.getColumnSpan()); + parcel.writeInt(mCollectionItemInfo.getRowIndex()); + parcel.writeInt(mCollectionItemInfo.getRowSpan()); parcel.writeInt(mCollectionItemInfo.isHeading() ? 1 : 0); } else { parcel.writeInt(0); @@ -2266,8 +2276,8 @@ public class AccessibilityNodeInfo implements Parcelable { mTextSelectionStart = other.mTextSelectionStart; mTextSelectionEnd = other.mTextSelectionEnd; mInputType = other.mInputType; - if (other.mBundle != null && !other.mBundle.isEmpty()) { - getBundle().putAll(other.mBundle); + if (other.mExtras != null && !other.mExtras.isEmpty()) { + getExtras().putAll(other.mExtras); } mRangeInfo = other.mRangeInfo; mCollectionInfo = other.mCollectionInfo; @@ -2323,7 +2333,7 @@ public class AccessibilityNodeInfo implements Parcelable { mInputType = parcel.readInt(); if (parcel.readInt() == 1) { - getBundle().putAll(parcel.readBundle()); + getExtras().putAll(parcel.readBundle()); } if (parcel.readInt() == 1) { @@ -2376,8 +2386,8 @@ public class AccessibilityNodeInfo implements Parcelable { mTextSelectionStart = UNDEFINED; mTextSelectionEnd = UNDEFINED; mInputType = InputType.TYPE_NULL; - if (mBundle != null) { - mBundle.clear(); + if (mExtras != null) { + mExtras.clear(); } if (mRangeInfo != null) { mRangeInfo.recycle(); @@ -2677,7 +2687,15 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Class with information if a node is a collection. Use - * {@link CollectionInfo#obtain(int, int, boolean) to an instance. + * {@link CollectionInfo#obtain(int, int, boolean)} to get an instance. + * <p> + * A collection of items has rows and columns and may be hierarchical. + * For example, a horizontal list is a collection with one column, as + * many rows as the list items, and is not hierarchical; A table is a + * collection with several rows, several columns, and is not hierarchical; + * A vertical tree is a hierarchical collection with one column and + * as many rows as the first level children. + * </p> */ public static final class CollectionInfo { private static final int MAX_POOL_SIZE = 20; @@ -2685,54 +2703,54 @@ public class AccessibilityNodeInfo implements Parcelable { private static final SynchronizedPool<CollectionInfo> sPool = new SynchronizedPool<CollectionInfo>(MAX_POOL_SIZE); - private int mHorizontalSize; - private int mVerticalSize; + private int mRowCount; + private int mColumnCount; private boolean mHierarchical; /** * Obtains a pooled instance. * - * @param horizontalSize The horizontal size. - * @param verticalSize The vertical size. + * @param rowCount The number of rows. + * @param columnCount The number of columns. * @param hierarchical Whether the collection is hierarchical. */ - public static CollectionInfo obtain(int horizontalSize, int verticalSize, + public static CollectionInfo obtain(int rowCount, int columnCount, boolean hierarchical) { CollectionInfo info = sPool.acquire(); - return (info != null) ? info : new CollectionInfo(horizontalSize, - verticalSize, hierarchical); + return (info != null) ? info : new CollectionInfo(rowCount, + columnCount, hierarchical); } /** * Creates a new instance. * - * @param horizontalSize The horizontal size. - * @param verticalSize The vertical size. + * @param rowCount The number of rows. + * @param columnCount The number of columns. * @param hierarchical Whether the collection is hierarchical. */ - private CollectionInfo(int horizontalSize, int verticalSize, + private CollectionInfo(int rowCount, int columnCount, boolean hierarchical) { - mHorizontalSize = horizontalSize; - mVerticalSize = verticalSize; + mRowCount = rowCount; + mColumnCount = columnCount; mHierarchical = hierarchical; } /** - * Gets the horizontal size in terms of item positions. + * Gets the number of rows. * - * @return The size. + * @return The row count. */ - public int getHorizontalSize() { - return mHorizontalSize; + public int getRowCount() { + return mRowCount; } /** - * Gets the vertical size in terms of item positions. + * Gets the number of columns. * - * @return The size. + * @return The column count. */ - public int getVerticalSize() { - return mVerticalSize; + public int getColumnCount() { + return mColumnCount; } /** @@ -2753,15 +2771,23 @@ public class AccessibilityNodeInfo implements Parcelable { } private void clear() { - mHorizontalSize = 0; - mVerticalSize = 0; + mRowCount = 0; + mColumnCount = 0; mHierarchical = false; } } /** * Class with information if a node is a collection item. Use - * {@link CollectionItemInfo#obtain(int, int, int, int, boolean) to get an instance. + * {@link CollectionItemInfo#obtain(int, int, int, int, boolean)} + * to get an instance. + * <p> + * A collection item is contained in a collection, it starts at + * a given row and column in the collection, and spans one or + * more rows and columns. For example, a header of two related + * table columns starts at the first row and the first column, + * spans one row and two columns. + * </p> */ public static final class CollectionItemInfo { private static final int MAX_POOL_SIZE = 20; @@ -2772,79 +2798,77 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Obtains a pooled instance. * - * @param horizontalPosition The horizontal item position. - * @param horizontalSpan The horizontal item span. - * @param verticalPosition The vertical item position. - * @param verticalSpan The vertical item span. + * @param rowIndex The row index at which the item is located. + * @param rowSpan The number of rows the item spans. + * @param columnIndex The column index at which the item is located. + * @param columnSpan The number of columns the item spans. * @param heading Whether the item is a heading. */ - public static CollectionItemInfo obtain(int horizontalPosition, int horizontalSpan, - int verticalPosition, int verticalSpan, boolean heading) { + public static CollectionItemInfo obtain(int rowIndex, int rowSpan, + int columnIndex, int columnSpan, boolean heading) { CollectionItemInfo info = sPool.acquire(); - return (info != null) ? info : new CollectionItemInfo(horizontalPosition, - horizontalSpan, verticalPosition, verticalSpan, heading); + return (info != null) ? info : new CollectionItemInfo(rowIndex, + rowSpan, columnIndex, columnSpan, heading); } private boolean mHeading; - private int mHorizontalPosition; - private int mVerticalPosition; - private int mHorizontalSpan; - private int mVerticalSpan; + private int mColumnIndex; + private int mRowIndex; + private int mColumnSpan; + private int mRowSpan; /** * Creates a new instance. * - * @param horizontalPosition The horizontal item position. - * @param horizontalSpan The horizontal item span. - * @param verticalPosition The vertical item position. - * @param verticalSpan The vertical item span. + * @param rowIndex The row index at which the item is located. + * @param rowSpan The number of rows the item spans. + * @param columnIndex The column index at which the item is located. + * @param columnSpan The number of columns the item spans. * @param heading Whether the item is a heading. */ - private CollectionItemInfo(int horizontalPosition, int horizontalSpan, - int verticalPosition, int verticalSpan, boolean heading) { - mHorizontalPosition = horizontalPosition; - mHorizontalSpan = horizontalSpan; - mVerticalPosition = verticalPosition; - mVerticalSpan = verticalSpan; + private CollectionItemInfo(int rowIndex, int rowSpan, + int columnIndex, int columnSpan, boolean heading) { + mRowIndex = rowIndex; + mRowSpan = rowSpan; + mColumnIndex = columnIndex; + mColumnSpan = columnSpan; mHeading = heading; } /** - * Gets the horizontal item position in the parent collection. + * Gets the column index at which the item is located. * - * @return The position. + * @return The column index. */ - public int getHorizontalPosition() { - return mHorizontalPosition; + public int getColumnIndex() { + return mColumnIndex; } /** - * Gets the vertical item position in the parent collection. + * Gets the row index at which the item is located. * - * @return The position. + * @return The row index. */ - public int getVerticalPosition() { - return mVerticalPosition; + public int getRowIndex() { + return mRowIndex; } /** - * Gets the horizontal span in terms of item positions - * of the parent collection. + * Gets the number of columns the item spans. * - * @return The span. + * @return The column span. */ - public int getHorizontalSpan() { - return mHorizontalSpan; + public int getColumnSpan() { + return mColumnSpan; } /** - * Gets the vertical span in terms of item positions - * of the parent collection. + * Gets the number of rows the item spans. * - * @return The span. + * @return The row span. */ - public int getVerticalSpan() { - return mVerticalSpan; + public int getRowSpan() { + return mRowSpan; } /** @@ -2866,10 +2890,10 @@ public class AccessibilityNodeInfo implements Parcelable { } private void clear() { - mHorizontalPosition = 0; - mHorizontalSpan = 0; - mVerticalPosition = 0; - mVerticalSpan = 0; + mColumnIndex = 0; + mColumnSpan = 0; + mRowIndex = 0; + mRowSpan = 0; mHeading = false; } } diff --git a/core/java/android/view/accessibility/CaptioningManager.java b/core/java/android/view/accessibility/CaptioningManager.java index b1be24c..d4c6abe 100644 --- a/core/java/android/view/accessibility/CaptioningManager.java +++ b/core/java/android/view/accessibility/CaptioningManager.java @@ -17,58 +17,77 @@ package android.view.accessibility; import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; import android.graphics.Color; import android.graphics.Typeface; +import android.net.Uri; +import android.os.Handler; import android.provider.Settings.Secure; import android.text.TextUtils; +import java.util.ArrayList; import java.util.Locale; /** - * Contains methods for accessing preferred video captioning state and + * Contains methods for accessing and monitoring preferred video captioning state and visual * properties. + * <p> + * To obtain a handle to the captioning manager, do the following: + * <p> + * <code> + * <pre>CaptioningManager captioningManager = + * (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);</pre> + * </code> */ public class CaptioningManager { - /** - * Activity Action: Show settings for video captioning. - * <p> - * In some cases, a matching Activity may not exist, so ensure you safeguard - * against this. - * <p> - * Input: Nothing. - * <p> - * Output: Nothing. - */ - public static final String ACTION_CAPTIONING_SETTINGS = "android.settings.CAPTIONING_SETTINGS"; + /** Default captioning enabled value. */ + private static final int DEFAULT_ENABLED = 0; + /** Default style preset as an index into {@link CaptionStyle#PRESETS}. */ private static final int DEFAULT_PRESET = 0; - private static final int DEFAULT_ENABLED = 0; - private static final float DEFAULT_FONT_SIZE = 24; + + /** Default scaling value for caption fonts. */ + private static final float DEFAULT_FONT_SCALE = 1; + + private final ArrayList<CaptioningChangeListener> + mListeners = new ArrayList<CaptioningChangeListener>(); + private final Handler mHandler = new Handler(); + + private final ContentResolver mContentResolver; + + /** + * Creates a new captioning manager for the specified context. + * + * @hide + */ + public CaptioningManager(Context context) { + mContentResolver = context.getContentResolver(); + } /** - * @param cr Resolver to access the database with. - * @return The user's preferred caption enabled state. + * @return the user's preferred captioning enabled state */ - public static final boolean isEnabled(ContentResolver cr) { - return Secure.getInt(cr, Secure.ACCESSIBILITY_CAPTIONING_ENABLED, DEFAULT_ENABLED) == 1; + public final boolean isEnabled() { + return Secure.getInt( + mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_ENABLED, DEFAULT_ENABLED) == 1; } /** - * @param cr Resolver to access the database with. - * @return The raw locale string for the user's preferred caption language. + * @return the raw locale string for the user's preferred captioning + * language * @hide */ - public static final String getRawLocale(ContentResolver cr) { - return Secure.getString(cr, Secure.ACCESSIBILITY_CAPTIONING_LOCALE); + public final String getRawLocale() { + return Secure.getString(mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_LOCALE); } /** - * @param cr Resolver to access the database with. - * @return The locale for the user's preferred caption language, or null if - * not specified. + * @return the locale for the user's preferred captioning language, or null + * if not specified */ - public static final Locale getLocale(ContentResolver cr) { - final String rawLocale = getRawLocale(cr); + public final Locale getLocale() { + final String rawLocale = getRawLocale(); if (!TextUtils.isEmpty(rawLocale)) { final String[] splitLocale = rawLocale.split("_"); switch (splitLocale.length) { @@ -85,14 +104,151 @@ public class CaptioningManager { } /** - * @param cr Resolver to access the database with. - * @return The user's preferred font size for video captions, or 0 if not - * specified. + * @return the user's preferred font scaling factor for video captions, or 1 if not + * specified */ - public static final float getFontSize(ContentResolver cr) { - return Secure.getFloat(cr, Secure.ACCESSIBILITY_CAPTIONING_FONT_SIZE, DEFAULT_FONT_SIZE); + public final float getFontScale() { + return Secure.getFloat( + mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, DEFAULT_FONT_SCALE); + } + + /** + * @return the raw preset number, or the first preset if not specified + * @hide + */ + public int getRawUserStyle() { + return Secure.getInt( + mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_PRESET, DEFAULT_PRESET); + } + + /** + * @return the user's preferred visual properties for captions as a + * {@link CaptionStyle}, or the default style if not specified + */ + public CaptionStyle getUserStyle() { + final int preset = getRawUserStyle(); + if (preset == CaptionStyle.PRESET_CUSTOM) { + return CaptionStyle.getCustomStyle(mContentResolver); + } + + return CaptionStyle.PRESETS[preset]; + } + + /** + * Adds a listener for changes in the user's preferred captioning enabled + * state and visual properties. + * + * @param listener the listener to add + */ + public void addCaptioningStateChangeListener(CaptioningChangeListener listener) { + synchronized (mListeners) { + if (mListeners.isEmpty()) { + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_ENABLED); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_LOCALE); + } + + mListeners.add(listener); + } + } + + private void registerObserver(String key) { + mContentResolver.registerContentObserver(Secure.getUriFor(key), false, mContentObserver); + } + + /** + * Removes a listener previously added using + * {@link #addCaptioningStateChangeListener}. + * + * @param listener the listener to remove + */ + public void removeCaptioningStateChangeListener(CaptioningChangeListener listener) { + synchronized (mListeners) { + mListeners.remove(listener); + + if (mListeners.isEmpty()) { + mContentResolver.unregisterContentObserver(mContentObserver); + } + } + } + + private void notifyEnabledChanged() { + final boolean enabled = isEnabled(); + synchronized (mListeners) { + for (CaptioningChangeListener listener : mListeners) { + listener.onEnabledChanged(enabled); + } + } + } + + private void notifyUserStyleChanged() { + final CaptionStyle userStyle = getUserStyle(); + synchronized (mListeners) { + for (CaptioningChangeListener listener : mListeners) { + listener.onUserStyleChanged(userStyle); + } + } } + private void notifyLocaleChanged() { + final Locale locale = getLocale(); + synchronized (mListeners) { + for (CaptioningChangeListener listener : mListeners) { + listener.onLocaleChanged(locale); + } + } + } + + private void notifyFontScaleChanged() { + final float fontScale = getFontScale(); + synchronized (mListeners) { + for (CaptioningChangeListener listener : mListeners) { + listener.onFontScaleChanged(fontScale); + } + } + } + + private final ContentObserver mContentObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + final String uriPath = uri.getPath(); + final String name = uriPath.substring(uriPath.lastIndexOf('/') + 1); + if (Secure.ACCESSIBILITY_CAPTIONING_ENABLED.equals(name)) { + notifyEnabledChanged(); + } else if (Secure.ACCESSIBILITY_CAPTIONING_LOCALE.equals(name)) { + notifyLocaleChanged(); + } else if (Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE.equals(name)) { + notifyFontScaleChanged(); + } else { + // We only need a single callback when multiple style properties + // change in rapid succession. + mHandler.removeCallbacks(mStyleChangedRunnable); + mHandler.post(mStyleChangedRunnable); + } + } + }; + + /** + * Runnable posted when user style properties change. This is used to + * prevent unnecessary change notifications when multiple properties change + * in rapid succession. + */ + private final Runnable mStyleChangedRunnable = new Runnable() { + @Override + public void run() { + notifyUserStyleChanged(); + } + }; + + /** + * Specifies visual properties for video captions, including foreground and + * background colors, edge properties, and typeface. + */ public static final class CaptionStyle { private static final CaptionStyle WHITE_ON_BLACK; private static final CaptionStyle BLACK_ON_WHITE; @@ -155,8 +311,8 @@ public class CaptioningManager { } /** - * @return The preferred {@link Typeface} for video captions, or null if - * not specified. + * @return the preferred {@link Typeface} for video captions, or null if + * not specified */ public Typeface getTypeface() { if (mParsedTypeface == null && !TextUtils.isEmpty(mRawTypeface)) { @@ -168,41 +324,20 @@ public class CaptioningManager { /** * @hide */ - public static int getRawPreset(ContentResolver cr) { - return Secure.getInt(cr, Secure.ACCESSIBILITY_CAPTIONING_PRESET, DEFAULT_PRESET); - } - - /** - * @param cr Resolver to access the database with. - * @return The user's preferred caption style. - */ - public static CaptionStyle defaultUserStyle(ContentResolver cr) { - final int preset = getRawPreset(cr); - if (preset == PRESET_CUSTOM) { - return getCustomStyle(cr); - } - - return PRESETS[preset]; - } - - /** - * @hide - */ public static CaptionStyle getCustomStyle(ContentResolver cr) { + final CaptionStyle defStyle = CaptionStyle.DEFAULT_CUSTOM; final int foregroundColor = Secure.getInt( - cr, Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR, - DEFAULT_CUSTOM.foregroundColor); - final int backgroundColor = Secure.getInt(cr, - Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR, - DEFAULT_CUSTOM.backgroundColor); + cr, Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR, defStyle.foregroundColor); + final int backgroundColor = Secure.getInt( + cr, Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR, defStyle.backgroundColor); final int edgeType = Secure.getInt( - cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE, DEFAULT_CUSTOM.edgeType); + cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE, defStyle.edgeType); final int edgeColor = Secure.getInt( - cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR, DEFAULT_CUSTOM.edgeColor); + cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR, defStyle.edgeColor); String rawTypeface = Secure.getString(cr, Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE); if (rawTypeface == null) { - rawTypeface = DEFAULT_CUSTOM.mRawTypeface; + rawTypeface = defStyle.mRawTypeface; } return new CaptionStyle( @@ -226,4 +361,45 @@ public class CaptioningManager { DEFAULT_CUSTOM = WHITE_ON_BLACK; } } + + /** + * Listener for changes in captioning properties, including enabled state + * and user style preferences. + */ + public abstract class CaptioningChangeListener { + /** + * Called when the captioning enabled state changes. + * + * @param enabled the user's new preferred captioning enabled state + */ + public void onEnabledChanged(boolean enabled) { + } + + /** + * Called when the captioning user style changes. + * + * @param userStyle the user's new preferred style + * @see CaptioningManager#getUserStyle() + */ + public void onUserStyleChanged(CaptionStyle userStyle) { + } + + /** + * Called when the captioning locale changes. + * + * @param locale the preferred captioning locale + * @see CaptioningManager#getLocale() + */ + public void onLocaleChanged(Locale locale) { + } + + /** + * Called when the captioning font scaling factor changes. + * + * @param fontScale the preferred font scaling factor + * @see CaptioningManager#getFontScale() + */ + public void onFontScaleChanged(float fontScale) { + } + } } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index f97e3dd..54b87de 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1876,9 +1876,13 @@ public final class InputMethodManager { } /** - * Returns true if the current IME needs to offer the users a way to switch to a next input - * method. When the user triggers it, the IME has to call {@link #switchToNextInputMethod} to - * switch to a next input method which is selected by the system. + * Returns true if the current IME needs to offer the users ways to switch to a next input + * method (e.g. a globe key.). + * When an IME sets supportsSwitchingToNextInputMethod and this method returns true, + * the IME has to offer ways to to invoke {@link #switchToNextInputMethod} accordingly. + * <p> Note that the system determines the most appropriate next input method + * and subtype in order to provide the consistent user experience in switching + * between IMEs and subtypes. * @param imeToken Supplies the identifying token given to an input method when it was started, * which allows it to perform this operation on itself. */ diff --git a/core/java/android/view/transition/Move.java b/core/java/android/view/transition/Move.java index ae7d759..fda0cd2 100644 --- a/core/java/android/view/transition/Move.java +++ b/core/java/android/view/transition/Move.java @@ -40,7 +40,7 @@ public class Move extends Transition { private static final String PROPNAME_PARENT = "android:move:parent"; private static final String PROPNAME_WINDOW_X = "android:move:windowX"; private static final String PROPNAME_WINDOW_Y = "android:move:windowY"; - private static String[] sTransitionProperties = { + private static final String[] sTransitionProperties = { PROPNAME_BOUNDS, PROPNAME_PARENT, PROPNAME_WINDOW_X, diff --git a/core/java/android/view/transition/TextChange.java b/core/java/android/view/transition/TextChange.java index 04ff707..7973c97 100644 --- a/core/java/android/view/transition/TextChange.java +++ b/core/java/android/view/transition/TextChange.java @@ -78,6 +78,10 @@ public class TextChange extends Transition { */ public static final int CHANGE_BEHAVIOR_OUT_IN = 3; + private static final String[] sTransitionProperties = { + PROPNAME_TEXT + }; + /** * Sets the type of changing animation that will be run, one of * {@link #CHANGE_BEHAVIOR_KEEP} and {@link #CHANGE_BEHAVIOR_OUT_IN}. @@ -92,6 +96,11 @@ public class TextChange extends Transition { } @Override + public String[] getTransitionProperties() { + return sTransitionProperties; + } + + @Override protected void captureValues(TransitionValues values, boolean start) { if (values.view instanceof TextView) { TextView textview = (TextView) values.view; @@ -111,7 +120,7 @@ public class TextChange extends Transition { final TextView view = (TextView) endValues.view; Map<String, Object> startVals = startValues.values; Map<String, Object> endVals = endValues.values; - String startText = (String) startVals.get(PROPNAME_TEXT); + final String startText = (String) startVals.get(PROPNAME_TEXT); final String endText = (String) endVals.get(PROPNAME_TEXT); if (!startText.equals(endText)) { view.setText(startText); @@ -121,7 +130,10 @@ public class TextChange extends Transition { anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - view.setText(endText); + if (startText.equals(view.getText())) { + // Only set if it hasn't been changed since anim started + view.setText(endText); + } } }); } else { @@ -143,7 +155,10 @@ public class TextChange extends Transition { outAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - view.setText(endText); + if (startText.equals(view.getText())) { + // Only set if it hasn't been changed since anim started + view.setText(endText); + } } }); } @@ -169,6 +184,20 @@ public class TextChange extends Transition { anim = inAnim; } } + TransitionListener transitionListener = new TransitionListenerAdapter() { + boolean mCanceled = false; + + @Override + public void onTransitionPause(Transition transition) { + view.setText(endText); + } + + @Override + public void onTransitionResume(Transition transition) { + view.setText(startText); + } + }; + addListener(transitionListener); return anim; } return null; diff --git a/core/java/android/view/transition/Transition.java b/core/java/android/view/transition/Transition.java index 0444843..a66fa52 100644 --- a/core/java/android/view/transition/Transition.java +++ b/core/java/android/view/transition/Transition.java @@ -843,7 +843,6 @@ public abstract class Transition implements Cloneable { for (int i = numOldAnims - 1; i >= 0; i--) { Animator anim = runningAnimators.keyAt(i); if (anim != null) { - anim.resume(); AnimationInfo oldInfo = runningAnimators.get(anim); if (oldInfo != null) { boolean cancel = false; @@ -851,22 +850,25 @@ public abstract class Transition implements Cloneable { View oldView = oldInfo.view; TransitionValues newValues = mEndValues.viewValues != null ? mEndValues.viewValues.get(oldView) : null; - if (oldValues == null || newValues == null) { - if (oldValues != null || newValues != null) { + if (oldValues != null) { + // if oldValues null, then transition didn't care to stash values, + // and won't get canceled + if (newValues == null) { cancel = true; - } - } else { - for (String key : oldValues.values.keySet()) { - Object oldValue = oldValues.values.get(key); - Object newValue = newValues.values.get(key); - if ((oldValue == null && newValue != null) || - (oldValue != null && !oldValue.equals(newValue))) { - cancel = true; - if (DBG) { - Log.d(LOG_TAG, "Transition.play: oldValue != newValue for " + - key + ": old, new = " + oldValue + ", " + newValue); + } else { + for (String key : oldValues.values.keySet()) { + Object oldValue = oldValues.values.get(key); + Object newValue = newValues.values.get(key); + if (oldValue != null && newValue != null && + !oldValue.equals(newValue)) { + cancel = true; + if (DBG) { + Log.d(LOG_TAG, "Transition.playTransition: " + + "oldValue != newValue for " + key + + ": old, new = " + oldValue + ", " + newValue); + } + break; } - break; } } } diff --git a/core/java/android/view/transition/TransitionManager.java b/core/java/android/view/transition/TransitionManager.java index 3cb6f68..bde891d 100644 --- a/core/java/android/view/transition/TransitionManager.java +++ b/core/java/android/view/transition/TransitionManager.java @@ -183,9 +183,12 @@ public class TransitionManager { final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions = getRunningTransitions(); ArrayList<Transition> currentTransitions = runningTransitions.get(sceneRoot); + ArrayList<Transition> previousRunningTransitions = null; if (currentTransitions == null) { currentTransitions = new ArrayList<Transition>(); runningTransitions.put(sceneRoot, currentTransitions); + } else if (currentTransitions.size() > 0) { + previousRunningTransitions = new ArrayList<Transition>(currentTransitions); } currentTransitions.add(transition); transition.addListener(new Transition.TransitionListenerAdapter() { @@ -197,6 +200,11 @@ public class TransitionManager { } }); transition.captureValues(sceneRoot, false); + if (previousRunningTransitions != null) { + for (Transition runningTransition : previousRunningTransitions) { + runningTransition.resume(); + } + } transition.playTransition(sceneRoot); // Returning false from onPreDraw() skips the current frame. This is diff --git a/core/java/android/view/transition/Visibility.java b/core/java/android/view/transition/Visibility.java index 96ea044..348dcfb 100644 --- a/core/java/android/view/transition/Visibility.java +++ b/core/java/android/view/transition/Visibility.java @@ -29,17 +29,31 @@ import android.view.ViewParent; * views exist in the current view hierarchy. The class is intended to be a * utility for subclasses such as {@link Fade}, which use this visibility * information to determine the specific animations to run when visibility - * changes occur. Subclasses should implement one or more of the methods - * {@link #appear(ViewGroup, TransitionValues, int, TransitionValues, int)}, - * {@link #disappear(ViewGroup, TransitionValues, int, TransitionValues, int)}, - * {@link #appear(ViewGroup, TransitionValues, int, TransitionValues, int)}, and + * changes occur. Subclasses should implement one or both of the methods + * {@link #appear(ViewGroup, TransitionValues, int, TransitionValues, int), and * {@link #disappear(ViewGroup, TransitionValues, int, TransitionValues, int)}. + * + * <p>Note that a view's visibility change is determined by both whether the view + * itself is changing and whether its parent hierarchy's visibility is changing. + * That is, a view that appears in the end scene will only trigger a call to + * {@link #appear(android.view.ViewGroup, TransitionValues, int, TransitionValues, int) + * appear()} if its parent hierarchy was stable between the start and end scenes. + * This is done to avoid causing a visibility transition on every node in a hierarchy + * when only the top-most node is the one that should be transitioned in/out. + * Stability is determined by either the parent hierarchy views being the same + * between scenes or, if scenes are inflated from layout resource files and thus + * have result in different view instances, if the views represented by + * the ids of those parents are stable. This means that visibility determination + * is more effective with inflated view hierarchies if ids are used. + * The exception to this is when the visibility subclass transition is + * targeted at specific views, in which case the visibility of parent views + * is ignored.</p> */ public abstract class Visibility extends Transition { private static final String PROPNAME_VISIBILITY = "android:visibility:visibility"; private static final String PROPNAME_PARENT = "android:visibility:parent"; - private static String[] sTransitionProperties = { + private static final String[] sTransitionProperties = { PROPNAME_VISIBILITY, PROPNAME_PARENT, }; @@ -49,8 +63,8 @@ public abstract class Visibility extends Transition { boolean fadeIn; int startVisibility; int endVisibility; - View startParent; - View endParent; + ViewGroup startParent; + ViewGroup endParent; } // Temporary structure, used in calculating state in setup() and play() @@ -93,28 +107,47 @@ public abstract class Visibility extends Transition { return visibility == View.VISIBLE && parent != null; } - private boolean isHierarchyVisibilityChanging(ViewGroup sceneRoot, ViewGroup view) { + /** + * Tests whether the hierarchy, up to the scene root, changes visibility between + * start and end scenes. This is done to ensure that a view that changes visibility + * is only animated if that view's parent was stable between scenes; we should not + * fade an entire hierarchy, but rather just the top-most node in the hierarchy that + * changed visibility. Note that both the start and end parents are passed in + * because the instances may differ for the same view due to layout inflation + * between scenes. + * + * @param sceneRoot The root of the scene hierarchy + * @param startView The container view in the start scene + * @param endView The container view in the end scene + * @return true if the parent hierarchy experienced a visibility change, false + * otherwise + */ + private boolean isHierarchyVisibilityChanging(ViewGroup sceneRoot, ViewGroup startView, + ViewGroup endView) { - if (view == sceneRoot) { + if (startView == sceneRoot || endView == sceneRoot) { return false; } - TransitionValues startValues = getTransitionValues(view, true); - TransitionValues endValues = getTransitionValues(view, false); + TransitionValues startValues = startView != null ? + getTransitionValues(startView, true) : getTransitionValues(endView, true); + TransitionValues endValues = endView != null ? + getTransitionValues(endView, false) : getTransitionValues(startView, false); if (startValues == null || endValues == null) { return true; } - int startVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY); - View startParent = (View) startValues.values.get(PROPNAME_PARENT); - int endVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY); - View endParent = (View) endValues.values.get(PROPNAME_PARENT); + Integer visibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY); + int startVisibility = (visibility != null) ? visibility : -1; + ViewGroup startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT); + visibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY); + int endVisibility = (visibility != null) ? visibility : -1; + ViewGroup endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT); if (startVisibility != endVisibility || startParent != endParent) { return true; } - ViewParent parent = view.getParent(); - if (parent instanceof ViewGroup && parent != sceneRoot) { - return isHierarchyVisibilityChanging(sceneRoot, (ViewGroup) parent); + if (startParent != null || endParent != null) { + return isHierarchyVisibilityChanging(sceneRoot, startParent, endParent); } return false; } @@ -126,14 +159,14 @@ public abstract class Visibility extends Transition { visInfo.fadeIn = false; if (startValues != null) { visInfo.startVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY); - visInfo.startParent = (View) startValues.values.get(PROPNAME_PARENT); + visInfo.startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT); } else { visInfo.startVisibility = -1; visInfo.startParent = null; } if (endValues != null) { visInfo.endVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY); - visInfo.endParent = (View) endValues.values.get(PROPNAME_PARENT); + visInfo.endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT); } else { visInfo.endVisibility = -1; visInfo.endParent = null; @@ -177,20 +210,27 @@ public abstract class Visibility extends Transition { protected Animator play(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) { VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues); - // Ensure not in parent hierarchy that's also becoming visible/invisible if (visInfo.visibilityChange) { - ViewGroup parent = (ViewGroup) ((visInfo.endParent != null) ? - visInfo.endParent : visInfo.startParent); - if (parent != null) { - if (!isHierarchyVisibilityChanging(sceneRoot, parent)) { - if (visInfo.fadeIn) { - return appear(sceneRoot, startValues, visInfo.startVisibility, - endValues, visInfo.endVisibility); - } else { - return disappear(sceneRoot, startValues, visInfo.startVisibility, - endValues, visInfo.endVisibility - ); - } + // Only transition views that are either targets of this transition + // or whose parent hierarchies remain stable between scenes + boolean isTarget = false; + if (mTargets != null || mTargetIds != null) { + View startView = startValues != null ? startValues.view : null; + View endView = endValues != null ? endValues.view : null; + int startId = startView != null ? startView.getId() : -1; + int endId = endView != null ? endView.getId() : -1; + isTarget = isValidTarget(startView, startId) || isValidTarget(endView, endId); + } + if (isTarget || ((visInfo.startParent != null || visInfo.endParent != null) && + !isHierarchyVisibilityChanging(sceneRoot, + visInfo.startParent, visInfo.endParent))) { + if (visInfo.fadeIn) { + return appear(sceneRoot, startValues, visInfo.startVisibility, + endValues, visInfo.endVisibility); + } else { + return disappear(sceneRoot, startValues, visInfo.startVisibility, + endValues, visInfo.endVisibility + ); } } } diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index fea6be6..7707392 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -50,7 +50,9 @@ import java.util.Map; */ class CallbackProxy extends Handler { // Logging tag - private static final String LOGTAG = "CallbackProxy"; + static final String LOGTAG = "WebViewCallback"; + // Enables API callback tracing + private static final boolean TRACE = DebugFlags.TRACE_CALLBACK; // Instance of WebViewClient that is the client callback. private volatile WebViewClient mWebViewClient; // Instance of WebChromeClient for handling all chrome functions. @@ -258,6 +260,7 @@ class CallbackProxy extends Handler { } boolean override = false; if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "shouldOverrideUrlLoading=" + overrideUrl); override = mWebViewClient.shouldOverrideUrlLoading(mWebView.getWebView(), overrideUrl); } else { @@ -307,6 +310,7 @@ class CallbackProxy extends Handler { String startedUrl = msg.getData().getString("url"); mWebView.onPageStarted(startedUrl); if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "onPageStarted=" + startedUrl); mWebViewClient.onPageStarted(mWebView.getWebView(), startedUrl, (Bitmap) msg.obj); } @@ -316,18 +320,21 @@ class CallbackProxy extends Handler { String finishedUrl = (String) msg.obj; mWebView.onPageFinished(finishedUrl); if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "onPageFinished=" + finishedUrl); mWebViewClient.onPageFinished(mWebView.getWebView(), finishedUrl); } break; case RECEIVED_ICON: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onReceivedIcon"); mWebChromeClient.onReceivedIcon(mWebView.getWebView(), (Bitmap) msg.obj); } break; case RECEIVED_TOUCH_ICON_URL: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onReceivedTouchIconUrl"); mWebChromeClient.onReceivedTouchIconUrl(mWebView.getWebView(), (String) msg.obj, msg.arg1 == 1); } @@ -335,6 +342,7 @@ class CallbackProxy extends Handler { case RECEIVED_TITLE: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onReceivedTitle"); mWebChromeClient.onReceivedTitle(mWebView.getWebView(), (String) msg.obj); } @@ -345,6 +353,7 @@ class CallbackProxy extends Handler { int reasonCode = msg.arg1; final String description = msg.getData().getString("description"); final String failUrl = msg.getData().getString("failingUrl"); + if (TRACE) Log.d(LOGTAG, "onReceivedError=" + failUrl); mWebViewClient.onReceivedError(mWebView.getWebView(), reasonCode, description, failUrl); } @@ -356,6 +365,7 @@ class CallbackProxy extends Handler { Message dontResend = (Message) msg.getData().getParcelable("dontResend"); if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "onFormResubmission"); mWebViewClient.onFormResubmission(mWebView.getWebView(), dontResend, resend); } else { @@ -379,6 +389,7 @@ class CallbackProxy extends Handler { HttpAuthHandler handler = (HttpAuthHandler) msg.obj; String host = msg.getData().getString("host"); String realm = msg.getData().getString("realm"); + if (TRACE) Log.d(LOGTAG, "onReceivedHttpAuthRequest"); mWebViewClient.onReceivedHttpAuthRequest(mWebView.getWebView(), handler, host, realm); } @@ -388,6 +399,7 @@ class CallbackProxy extends Handler { if (mWebViewClient != null) { HashMap<String, Object> map = (HashMap<String, Object>) msg.obj; + if (TRACE) Log.d(LOGTAG, "onReceivedSslError"); mWebViewClient.onReceivedSslError(mWebView.getWebView(), (SslErrorHandler) map.get("handler"), (SslError) map.get("error")); @@ -396,6 +408,7 @@ class CallbackProxy extends Handler { case PROCEEDED_AFTER_SSL_ERROR: if (mWebViewClient != null && mWebViewClient instanceof WebViewClientClassicExt) { + if (TRACE) Log.d(LOGTAG, "onProceededAfterSslError"); ((WebViewClientClassicExt) mWebViewClient).onProceededAfterSslError( mWebView.getWebView(), (SslError) msg.obj); @@ -404,6 +417,7 @@ class CallbackProxy extends Handler { case CLIENT_CERT_REQUEST: if (mWebViewClient != null && mWebViewClient instanceof WebViewClientClassicExt) { + if (TRACE) Log.d(LOGTAG, "onReceivedClientCertRequest"); HashMap<String, Object> map = (HashMap<String, Object>) msg.obj; ((WebViewClientClassicExt) mWebViewClient).onReceivedClientCertRequest( mWebView.getWebView(), @@ -418,6 +432,7 @@ class CallbackProxy extends Handler { // changed. synchronized (this) { if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onProgressChanged=" + mLatestProgress); mWebChromeClient.onProgressChanged(mWebView.getWebView(), mLatestProgress); } @@ -427,14 +442,18 @@ class CallbackProxy extends Handler { case UPDATE_VISITED: if (mWebViewClient != null) { + String url = (String) msg.obj; + if (TRACE) Log.d(LOGTAG, "doUpdateVisitedHistory=" + url); mWebViewClient.doUpdateVisitedHistory(mWebView.getWebView(), - (String) msg.obj, msg.arg1 != 0); + url, msg.arg1 != 0); } break; case LOAD_RESOURCE: if (mWebViewClient != null) { - mWebViewClient.onLoadResource(mWebView.getWebView(), (String) msg.obj); + String url = (String) msg.obj; + if (TRACE) Log.d(LOGTAG, "onLoadResource=" + url); + mWebViewClient.onLoadResource(mWebView.getWebView(), url); } break; @@ -448,6 +467,7 @@ class CallbackProxy extends Handler { String referer = msg.getData().getString("referer"); Long contentLength = msg.getData().getLong("contentLength"); + if (TRACE) Log.d(LOGTAG, "onDownloadStart"); if (mDownloadListener instanceof BrowserDownloadListener) { ((BrowserDownloadListener) mDownloadListener).onDownloadStart(url, userAgent, contentDisposition, mimetype, referer, contentLength); @@ -460,6 +480,7 @@ class CallbackProxy extends Handler { case CREATE_WINDOW: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onCreateWindow"); if (!mWebChromeClient.onCreateWindow(mWebView.getWebView(), msg.arg1 == 1, msg.arg2 == 1, (Message) msg.obj)) { @@ -473,12 +494,14 @@ class CallbackProxy extends Handler { case REQUEST_FOCUS: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onRequestFocus"); mWebChromeClient.onRequestFocus(mWebView.getWebView()); } break; case CLOSE_WINDOW: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onCloseWindow"); mWebChromeClient.onCloseWindow(((WebViewClassic) msg.obj).getWebView()); } break; @@ -500,6 +523,7 @@ class CallbackProxy extends Handler { case ASYNC_KEYEVENTS: if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "onUnhandledKeyEvent"); mWebViewClient.onUnhandledKeyEvent(mWebView.getWebView(), (KeyEvent) msg.obj); } @@ -521,6 +545,7 @@ class CallbackProxy extends Handler { WebStorage.QuotaUpdater quotaUpdater = (WebStorage.QuotaUpdater) map.get("quotaUpdater"); + if (TRACE) Log.d(LOGTAG, "onExceededDatabaseQuota"); mWebChromeClient.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater); @@ -538,6 +563,7 @@ class CallbackProxy extends Handler { WebStorage.QuotaUpdater quotaUpdater = (WebStorage.QuotaUpdater) map.get("quotaUpdater"); + if (TRACE) Log.d(LOGTAG, "onReachedMaxAppCacheSize"); mWebChromeClient.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); } @@ -551,6 +577,7 @@ class CallbackProxy extends Handler { GeolocationPermissions.Callback callback = (GeolocationPermissions.Callback) map.get("callback"); + if (TRACE) Log.d(LOGTAG, "onGeolocationPermissionsShowPrompt"); mWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback); } @@ -558,6 +585,7 @@ class CallbackProxy extends Handler { case GEOLOCATION_PERMISSIONS_HIDE_PROMPT: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onGeolocationPermissionsHidePrompt"); mWebChromeClient.onGeolocationPermissionsHidePrompt(); } break; @@ -566,6 +594,7 @@ class CallbackProxy extends Handler { if (mWebChromeClient != null) { final JsResultReceiver receiver = (JsResultReceiver) msg.obj; JsDialogHelper helper = new JsDialogHelper(receiver.mJsResult, msg); + if (TRACE) Log.d(LOGTAG, "onJsAlert"); if (!helper.invokeCallback(mWebChromeClient, mWebView.getWebView())) { helper.showDialog(mContext); } @@ -577,6 +606,7 @@ class CallbackProxy extends Handler { if(mWebChromeClient != null) { final JsResultReceiver receiver = (JsResultReceiver) msg.obj; final JsResult res = receiver.mJsResult; + if (TRACE) Log.d(LOGTAG, "onJsTimeout"); if (mWebChromeClient.onJsTimeout()) { res.confirm(); } else { @@ -598,6 +628,7 @@ class CallbackProxy extends Handler { case SCALE_CHANGED: if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "onScaleChanged"); mWebViewClient.onScaleChanged(mWebView.getWebView(), msg.getData() .getFloat("old"), msg.getData().getFloat("new")); } @@ -624,6 +655,7 @@ class CallbackProxy extends Handler { ConsoleMessage.MessageLevel messageLevel = ConsoleMessage.MessageLevel.values()[msgLevel]; + if (TRACE) Log.d(LOGTAG, "onConsoleMessage"); if (!mWebChromeClient.onConsoleMessage(new ConsoleMessage(message, sourceID, lineNumber, messageLevel))) { // If false was returned the user did not provide their own console function so @@ -654,12 +686,14 @@ class CallbackProxy extends Handler { case GET_VISITED_HISTORY: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "getVisitedHistory"); mWebChromeClient.getVisitedHistory((ValueCallback<String[]>)msg.obj); } break; case OPEN_FILE_CHOOSER: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "openFileChooser"); UploadFileMessageData data = (UploadFileMessageData)msg.obj; mWebChromeClient.openFileChooser(data.getUploadFile(), data.getAcceptType(), data.getCapture()); @@ -668,6 +702,7 @@ class CallbackProxy extends Handler { case ADD_HISTORY_ITEM: if (mWebBackForwardListClient != null) { + if (TRACE) Log.d(LOGTAG, "onNewHistoryItem"); mWebBackForwardListClient.onNewHistoryItem( (WebHistoryItem) msg.obj); } @@ -693,6 +728,7 @@ class CallbackProxy extends Handler { String realm = msg.getData().getString("realm"); String account = msg.getData().getString("account"); String args = msg.getData().getString("args"); + if (TRACE) Log.d(LOGTAG, "onReceivedLoginRequest"); mWebViewClient.onReceivedLoginRequest(mWebView.getWebView(), realm, account, args); } @@ -910,6 +946,7 @@ class CallbackProxy extends Handler { return null; } // Note: This method does _not_ send a message. + if (TRACE) Log.d(LOGTAG, "shouldInterceptRequest=" + url); WebResourceResponse r = mWebViewClient.shouldInterceptRequest(mWebView.getWebView(), url); if (r == null) { diff --git a/core/java/android/webkit/DebugFlags.java b/core/java/android/webkit/DebugFlags.java index 349113e..524f610 100644 --- a/core/java/android/webkit/DebugFlags.java +++ b/core/java/android/webkit/DebugFlags.java @@ -24,25 +24,33 @@ package android.webkit; * The name of each flags maps directly to the name of the class in which that * flag is used. * + * @hide Only used by WebView implementations. */ -class DebugFlags { +public class DebugFlags { + public static final boolean COOKIE_SYNC_MANAGER = false; + public static final boolean TRACE_API = false; + public static final boolean TRACE_CALLBACK = false; + public static final boolean TRACE_JAVASCRIPT_BRIDGE = false; + public static final boolean URL_UTIL = false; + public static final boolean WEB_SYNC_MANAGER = false; + + // TODO: Delete these when WebViewClassic is moved public static final boolean BROWSER_FRAME = false; public static final boolean CACHE_MANAGER = false; public static final boolean CALLBACK_PROXY = false; public static final boolean COOKIE_MANAGER = false; - public static final boolean COOKIE_SYNC_MANAGER = false; public static final boolean FRAME_LOADER = false; public static final boolean J_WEB_CORE_JAVA_BRIDGE = false;// HIGHLY VERBOSE public static final boolean LOAD_LISTENER = false; + public static final boolean MEASURE_PAGE_SWAP_FPS = false; public static final boolean NETWORK = false; public static final boolean SSL_ERROR_HANDLER = false; public static final boolean STREAM_LOADER = false; - public static final boolean URL_UTIL = false; public static final boolean WEB_BACK_FORWARD_LIST = false; public static final boolean WEB_SETTINGS = false; - public static final boolean WEB_SYNC_MANAGER = false; public static final boolean WEB_VIEW = false; public static final boolean WEB_VIEW_CORE = false; - public static final boolean MEASURE_PAGE_SWAP_FPS = false; + + } diff --git a/core/java/android/webkit/HTML5VideoFullScreen.java b/core/java/android/webkit/HTML5VideoFullScreen.java index b52218d..6fb32c8 100644 --- a/core/java/android/webkit/HTML5VideoFullScreen.java +++ b/core/java/android/webkit/HTML5VideoFullScreen.java @@ -19,6 +19,7 @@ package android.webkit; import android.content.Context; import android.media.MediaPlayer; import android.media.Metadata; +import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.SurfaceHolder; @@ -293,12 +294,16 @@ public class HTML5VideoFullScreen extends HTML5VideoView mLayout.setVisibility(View.VISIBLE); WebChromeClient client = webView.getWebChromeClient(); if (client != null) { + if (DebugFlags.TRACE_CALLBACK) Log.d(CallbackProxy.LOGTAG, "onShowCustomView"); client.onShowCustomView(mLayout, mCallback); // Plugins like Flash will draw over the video so hide // them while we're playing. if (webView.getViewManager() != null) webView.getViewManager().hideAll(); + if (DebugFlags.TRACE_CALLBACK) { + Log.d(CallbackProxy.LOGTAG, "getVideoLoadingProgressView"); + } mProgressView = client.getVideoLoadingProgressView(); if (mProgressView != null) { mLayout.addView(mProgressView, layoutParams); diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java index a3d62ae..e8538f6 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -180,6 +180,7 @@ class HTML5VideoViewProxy extends Handler if (!mHTML5VideoView.fullScreenExited() && mHTML5VideoView.isFullScreenMode()) { WebChromeClient client = webView.getWebChromeClient(); if (client != null) { + if (DebugFlags.TRACE_CALLBACK) Log.d(CallbackProxy.LOGTAG, "onHideCustomView"); client.onHideCustomView(); } } @@ -405,6 +406,7 @@ class HTML5VideoViewProxy extends Handler case ERROR: { WebChromeClient client = mWebView.getWebChromeClient(); if (client != null) { + if (DebugFlags.TRACE_CALLBACK) Log.d(CallbackProxy.LOGTAG, "onHideCustomView"); client.onHideCustomView(); } break; @@ -412,6 +414,9 @@ class HTML5VideoViewProxy extends Handler case LOAD_DEFAULT_POSTER: { WebChromeClient client = mWebView.getWebChromeClient(); if (client != null) { + if (DebugFlags.TRACE_CALLBACK) { + Log.d(CallbackProxy.LOGTAG, "getDefaultVideoPoster"); + } doSetPoster(client.getDefaultVideoPoster()); } break; diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 1b57d50..f0e8c4f 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -31,7 +31,9 @@ import android.os.Bundle; import android.os.CancellationSignal; import android.os.Looper; import android.os.Message; +import android.os.ParcelFileDescriptor; import android.os.StrictMode; +import android.print.PrintAttributes; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; @@ -49,7 +51,6 @@ import android.widget.AbsoluteLayout; import java.io.BufferedWriter; import java.io.File; -import java.io.OutputStream; import java.util.Map; /** @@ -243,7 +244,7 @@ public class WebView extends AbsoluteLayout implements ViewTreeObserver.OnGlobalFocusChangeListener, ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler { - private static final String LOGTAG = "webview_proxy"; + private static final String LOGTAG = "WebView"; // Throwing an exception for incorrect thread usage if the // build target is JB MR2 or newer. Defaults to false, and is @@ -495,9 +496,12 @@ public class WebView extends AbsoluteLayout sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2; checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "WebView<init>"); ensureProviderCreated(); mProvider.init(javaScriptInterfaces, privateBrowsing); + // Post condition of creating a webview is the CookieSyncManager instance exists. + CookieSyncManager.createInstance(getContext()); } /** @@ -507,6 +511,7 @@ public class WebView extends AbsoluteLayout */ public void setHorizontalScrollbarOverlay(boolean overlay) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setHorizontalScrollbarOverlay=" + overlay); mProvider.setHorizontalScrollbarOverlay(overlay); } @@ -517,6 +522,7 @@ public class WebView extends AbsoluteLayout */ public void setVerticalScrollbarOverlay(boolean overlay) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setVerticalScrollbarOverlay=" + overlay); mProvider.setVerticalScrollbarOverlay(overlay); } @@ -571,6 +577,7 @@ public class WebView extends AbsoluteLayout @Deprecated public void setCertificate(SslCertificate certificate) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setCertificate=" + certificate); mProvider.setCertificate(certificate); } @@ -594,6 +601,7 @@ public class WebView extends AbsoluteLayout @Deprecated public void savePassword(String host, String username, String password) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "savePassword=" + host); mProvider.savePassword(host, username, password); } @@ -613,6 +621,7 @@ public class WebView extends AbsoluteLayout public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setHttpAuthUsernamePassword=" + host); mProvider.setHttpAuthUsernamePassword(host, realm, username, password); } @@ -642,6 +651,7 @@ public class WebView extends AbsoluteLayout */ public void destroy() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "destroy"); mProvider.destroy(); } @@ -680,6 +690,7 @@ public class WebView extends AbsoluteLayout */ public void setNetworkAvailable(boolean networkUp) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setNetworkAvailable=" + networkUp); mProvider.setNetworkAvailable(networkUp); } @@ -696,6 +707,7 @@ public class WebView extends AbsoluteLayout */ public WebBackForwardList saveState(Bundle outState) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "saveState"); return mProvider.saveState(outState); } @@ -712,6 +724,7 @@ public class WebView extends AbsoluteLayout @Deprecated public boolean savePicture(Bundle b, final File dest) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "savePicture=" + dest.getName()); return mProvider.savePicture(b, dest); } @@ -729,6 +742,7 @@ public class WebView extends AbsoluteLayout @Deprecated public boolean restorePicture(Bundle b, File src) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "restorePicture=" + src.getName()); return mProvider.restorePicture(b, src); } @@ -746,6 +760,7 @@ public class WebView extends AbsoluteLayout */ public WebBackForwardList restoreState(Bundle inState) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "restoreState"); return mProvider.restoreState(inState); } @@ -762,6 +777,7 @@ public class WebView extends AbsoluteLayout */ public void loadUrl(String url, Map<String, String> additionalHttpHeaders) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadUrl(extra headers)=" + url); mProvider.loadUrl(url, additionalHttpHeaders); } @@ -772,6 +788,7 @@ public class WebView extends AbsoluteLayout */ public void loadUrl(String url) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadUrl=" + url); mProvider.loadUrl(url); } @@ -786,6 +803,7 @@ public class WebView extends AbsoluteLayout */ public void postUrl(String url, byte[] postData) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "postUrl=" + url); mProvider.postUrl(url, postData); } @@ -820,6 +838,7 @@ public class WebView extends AbsoluteLayout */ public void loadData(String data, String mimeType, String encoding) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadData"); mProvider.loadData(data, mimeType, encoding); } @@ -852,6 +871,7 @@ public class WebView extends AbsoluteLayout public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadDataWithBaseURL=" + baseUrl); mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); } @@ -868,6 +888,7 @@ public class WebView extends AbsoluteLayout */ public void evaluateJavascript(String script, ValueCallback<String> resultCallback) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "evaluateJavascript=" + script); mProvider.evaluateJavaScript(script, resultCallback); } @@ -878,6 +899,7 @@ public class WebView extends AbsoluteLayout */ public void saveWebArchive(String filename) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "saveWebArchive=" + filename); mProvider.saveWebArchive(filename); } @@ -895,6 +917,7 @@ public class WebView extends AbsoluteLayout */ public void saveWebArchive(String basename, boolean autoname, ValueCallback<String> callback) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "saveWebArchive(auto)=" + basename); mProvider.saveWebArchive(basename, autoname, callback); } @@ -903,6 +926,7 @@ public class WebView extends AbsoluteLayout */ public void stopLoading() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "stopLoading"); mProvider.stopLoading(); } @@ -911,6 +935,7 @@ public class WebView extends AbsoluteLayout */ public void reload() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "reload"); mProvider.reload(); } @@ -929,6 +954,7 @@ public class WebView extends AbsoluteLayout */ public void goBack() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "goBack"); mProvider.goBack(); } @@ -947,6 +973,7 @@ public class WebView extends AbsoluteLayout */ public void goForward() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "goForward"); mProvider.goForward(); } @@ -972,6 +999,7 @@ public class WebView extends AbsoluteLayout */ public void goBackOrForward(int steps) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "goBackOrForwad=" + steps); mProvider.goBackOrForward(steps); } @@ -991,6 +1019,7 @@ public class WebView extends AbsoluteLayout */ public boolean pageUp(boolean top) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "pageUp"); return mProvider.pageUp(top); } @@ -1002,6 +1031,7 @@ public class WebView extends AbsoluteLayout */ public boolean pageDown(boolean bottom) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "pageDown"); return mProvider.pageDown(bottom); } @@ -1014,6 +1044,7 @@ public class WebView extends AbsoluteLayout @Deprecated public void clearView() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearView"); mProvider.clearView(); } @@ -1033,6 +1064,7 @@ public class WebView extends AbsoluteLayout */ public Picture capturePicture() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "capturePicture"); return mProvider.capturePicture(); } @@ -1040,7 +1072,9 @@ public class WebView extends AbsoluteLayout * Exports the contents of this Webview as PDF. Only supported for API levels * {@link android.os.Build.VERSION_CODES#KEY_LIME_PIE} and above. * - * @param out The stream to export the PDF contents to. Cannot be null. + * TODO(sgurun) the parameter list is stale. Fix it before unhiding. + * + * @param fd The FileDescriptor to export the PDF contents to. Cannot be null. * @param width The page width. Should be larger than 0. * @param height The page height. Should be larger than 0. * @param resultCallback A callback to be invoked when the PDF content is exported. @@ -1049,21 +1083,27 @@ public class WebView extends AbsoluteLayout * be null. * * The PDF conversion is done asynchronously and the PDF output is written to the provided - * outputstream. The caller should not close the outputstream until the resultCallback is - * called, indicating PDF conversion is complete. Webview cannot be drawn during the pdf - * export so the application is recommended to take it offscreen, or putting in a layer - * with an overlaid progress UI / spinner. + * file descriptor. The caller should not close the file descriptor until the resultCallback + * is called, indicating PDF conversion is complete. Webview will never close the file + * descriptor. + * Limitations: Webview cannot be drawn during the PDF export so the application is + * recommended to take it offscreen, or putting in a layer with an overlaid progress + * UI / spinner. * * If the caller cancels the task using the cancellationSignal, the cancellation will be * acked using the resultCallback signal. * + * Throws an exception if an IO error occurs accessing the file descriptor. + * * TODO(sgurun) margins, explain the units, make it public. * @hide */ - public void exportToPdf(OutputStream out, int width, int height, - ValueCallback<Boolean> resultCallback, CancellationSignal cancellationSignal) { + public void exportToPdf(ParcelFileDescriptor fd, PrintAttributes attributes, + ValueCallback<Boolean> resultCallback, CancellationSignal cancellationSignal) + throws java.io.IOException { checkThread(); - mProvider.exportToPdf(out, width, height, resultCallback, cancellationSignal); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "exportToPdf"); + mProvider.exportToPdf(fd, attributes, resultCallback, cancellationSignal); } /** @@ -1094,6 +1134,7 @@ public class WebView extends AbsoluteLayout */ public void setInitialScale(int scaleInPercent) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setInitialScale=" + scaleInPercent); mProvider.setInitialScale(scaleInPercent); } @@ -1104,6 +1145,7 @@ public class WebView extends AbsoluteLayout */ public void invokeZoomPicker() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "invokeZoomPicker"); mProvider.invokeZoomPicker(); } @@ -1127,6 +1169,7 @@ public class WebView extends AbsoluteLayout */ public HitTestResult getHitTestResult() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "getHitTestResult"); return mProvider.getHitTestResult(); } @@ -1145,6 +1188,7 @@ public class WebView extends AbsoluteLayout */ public void requestFocusNodeHref(Message hrefMsg) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "requestFocusNodeHref"); mProvider.requestFocusNodeHref(hrefMsg); } @@ -1157,6 +1201,7 @@ public class WebView extends AbsoluteLayout */ public void requestImageRef(Message msg) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "requestImageRef"); mProvider.requestImageRef(msg); } @@ -1261,6 +1306,7 @@ public class WebView extends AbsoluteLayout */ public void pauseTimers() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "pauseTimers"); mProvider.pauseTimers(); } @@ -1270,6 +1316,7 @@ public class WebView extends AbsoluteLayout */ public void resumeTimers() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "resumeTimers"); mProvider.resumeTimers(); } @@ -1282,6 +1329,7 @@ public class WebView extends AbsoluteLayout */ public void onPause() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "onPause"); mProvider.onPause(); } @@ -1290,6 +1338,7 @@ public class WebView extends AbsoluteLayout */ public void onResume() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "onResume"); mProvider.onResume(); } @@ -1309,6 +1358,7 @@ public class WebView extends AbsoluteLayout */ public void freeMemory() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "freeMemory"); mProvider.freeMemory(); } @@ -1320,6 +1370,7 @@ public class WebView extends AbsoluteLayout */ public void clearCache(boolean includeDiskFiles) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearCache"); mProvider.clearCache(includeDiskFiles); } @@ -1331,6 +1382,7 @@ public class WebView extends AbsoluteLayout */ public void clearFormData() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearFormData"); mProvider.clearFormData(); } @@ -1339,6 +1391,7 @@ public class WebView extends AbsoluteLayout */ public void clearHistory() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearHistory"); mProvider.clearHistory(); } @@ -1348,6 +1401,7 @@ public class WebView extends AbsoluteLayout */ public void clearSslPreferences() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearSslPreferences"); mProvider.clearSslPreferences(); } @@ -1389,6 +1443,7 @@ public class WebView extends AbsoluteLayout */ public void findNext(boolean forward) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "findNext"); mProvider.findNext(forward); } @@ -1404,6 +1459,7 @@ public class WebView extends AbsoluteLayout @Deprecated public int findAll(String find) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "findAll"); StrictMode.noteSlowCall("findAll blocks UI: prefer findAllAsync"); return mProvider.findAll(find); } @@ -1418,6 +1474,7 @@ public class WebView extends AbsoluteLayout */ public void findAllAsync(String find) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "findAllAsync"); mProvider.findAllAsync(find); } @@ -1438,6 +1495,7 @@ public class WebView extends AbsoluteLayout @Deprecated public boolean showFindDialog(String text, boolean showIme) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "showFindDialog"); return mProvider.showFindDialog(text, showIme); } @@ -1473,6 +1531,7 @@ public class WebView extends AbsoluteLayout */ public void clearMatches() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearMatches"); mProvider.clearMatches(); } @@ -1533,6 +1592,7 @@ public class WebView extends AbsoluteLayout @Deprecated public void setPictureListener(PictureListener listener) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setPictureListener=" + listener); mProvider.setPictureListener(listener); } @@ -1582,6 +1642,7 @@ public class WebView extends AbsoluteLayout */ public void addJavascriptInterface(Object object, String name) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "addJavascriptInterface=" + name); mProvider.addJavascriptInterface(object, name); } @@ -1594,6 +1655,7 @@ public class WebView extends AbsoluteLayout */ public void removeJavascriptInterface(String name) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "removeJavascriptInterface=" + name); mProvider.removeJavascriptInterface(name); } @@ -1683,6 +1745,7 @@ public class WebView extends AbsoluteLayout public void flingScroll(int vx, int vy) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "flingScroll"); mProvider.flingScroll(vx, vy); } diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java index db98d30..3f22d53 100644 --- a/core/java/android/webkit/WebViewClassic.java +++ b/core/java/android/webkit/WebViewClassic.java @@ -62,6 +62,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; +import android.print.PrintAttributes; import android.security.KeyChain; import android.text.Editable; import android.text.InputType; @@ -2896,11 +2897,11 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc * See {@link WebView#exportToPdf()} */ @Override - public void exportToPdf(java.io.OutputStream out, int width, int height, - ValueCallback<Boolean> resultCallback, CancellationSignal cancellationSignal) { + public void exportToPdf(android.os.ParcelFileDescriptor fd, PrintAttributes attributes, + ValueCallback<Boolean> resultCallback, CancellationSignal cancellationSignal) + throws java.io.IOException { // K-only API not implemented in WebViewClassic. throw new IllegalStateException("This API not supported on Android 4.3 and earlier"); - } /** @@ -7950,6 +7951,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // triggered in setNewPicture Picture picture = mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2 ? capturePicture() : null; + if (DebugFlags.TRACE_CALLBACK) Log.d(CallbackProxy.LOGTAG, "onNewPicture"); mPictureListener.onNewPicture(getWebView(), picture); } } @@ -8037,6 +8039,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // triggered in pageSwapCallback Picture picture = mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2 ? capturePicture() : null; + if (DebugFlags.TRACE_CALLBACK) Log.d(CallbackProxy.LOGTAG, "onNewPicture"); mPictureListener.onNewPicture(getWebView(), picture); } } diff --git a/core/java/android/webkit/WebViewProvider.java b/core/java/android/webkit/WebViewProvider.java index 8fe6edf..d625d8a 100644 --- a/core/java/android/webkit/WebViewProvider.java +++ b/core/java/android/webkit/WebViewProvider.java @@ -27,6 +27,8 @@ import android.net.http.SslCertificate; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.print.PrintAttributes; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; @@ -41,7 +43,6 @@ import android.webkit.WebView.PictureListener; import java.io.BufferedWriter; import java.io.File; -import java.io.OutputStream; import java.util.Map; /** @@ -148,8 +149,9 @@ public interface WebViewProvider { public Picture capturePicture(); - public void exportToPdf(OutputStream out, int width, int height, - ValueCallback<Boolean> resultCallback, CancellationSignal cancellationSignal); + public void exportToPdf(ParcelFileDescriptor fd, PrintAttributes attributes, + ValueCallback<Boolean> resultCallback, CancellationSignal cancellationSignal) + throws java.io.IOException; public float getScale(); diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 285e6f2..be47bf0 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -2309,33 +2309,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te super.onInitializeAccessibilityNodeInfo(host, info); final int position = getPositionForView(host); - final ListAdapter adapter = getAdapter(); - - if ((position == INVALID_POSITION) || (adapter == null)) { - return; - } - - if (!isEnabled() || !adapter.isEnabled(position)) { - return; - } - - if (position == getSelectedItemPosition()) { - info.setSelected(true); - info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION); - } else { - info.addAction(AccessibilityNodeInfo.ACTION_SELECT); - } - - if (isClickable()) { - info.addAction(AccessibilityNodeInfo.ACTION_CLICK); - info.setClickable(true); - } - - if (isLongClickable()) { - info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK); - info.setLongClickable(true); - } - + onInitializeAccessibilityNodeInfoForItem(host, position, info); } @Override @@ -2388,6 +2362,45 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } + /** + * Initializes an {@link AccessibilityNodeInfo} with information about a + * particular item in the list. + * + * @param view View representing the list item. + * @param position Position of the list item within the adapter. + * @param info Node info to populate. + */ + public void onInitializeAccessibilityNodeInfoForItem( + View view, int position, AccessibilityNodeInfo info) { + final ListAdapter adapter = getAdapter(); + if (position == INVALID_POSITION || adapter == null) { + // The item doesn't exist, so there's not much we can do here. + return; + } + + if (!isEnabled() || !adapter.isEnabled(position)) { + info.setEnabled(false); + return; + } + + if (position == getSelectedItemPosition()) { + info.setSelected(true); + info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION); + } else { + info.addAction(AccessibilityNodeInfo.ACTION_SELECT); + } + + if (isClickable()) { + info.addAction(AccessibilityNodeInfo.ACTION_CLICK); + info.setClickable(true); + } + + if (isLongClickable()) { + info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK); + info.setLongClickable(true); + } + } + void positionSelector(int position, View sel) { if (position != INVALID_POSITION) { mSelectorPosition = position; diff --git a/core/java/android/widget/ActivityChooserView.java b/core/java/android/widget/ActivityChooserView.java index c070ee4..778c8db 100644 --- a/core/java/android/widget/ActivityChooserView.java +++ b/core/java/android/widget/ActivityChooserView.java @@ -234,7 +234,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); - info.setOpensPopup(true); + info.setCanOpenPopup(true); } }); mExpandActivityOverflowButtonImage = diff --git a/core/java/android/widget/CalendarView.java b/core/java/android/widget/CalendarView.java index 6970cde..de2be75 100644 --- a/core/java/android/widget/CalendarView.java +++ b/core/java/android/widget/CalendarView.java @@ -1028,26 +1028,29 @@ public class CalendarView extends FrameLayout { * Sets up the strings to be used by the header. */ private void setUpHeader() { + final String[] tinyWeekdayNames = LocaleData.get(Locale.getDefault()).tinyWeekdayNames; mDayLabels = new String[mDaysPerWeek]; - for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) { - int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY : i; - mDayLabels[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay, - DateUtils.LENGTH_SHORTEST); + for (int i = 0; i < mDaysPerWeek; i++) { + final int j = i + mFirstDayOfWeek; + final int calendarDay = (j > Calendar.SATURDAY) ? j - Calendar.SATURDAY : j; + mDayLabels[i] = tinyWeekdayNames[calendarDay]; } - + // Deal with week number TextView label = (TextView) mDayNamesHeader.getChildAt(0); if (mShowWeekNumber) { label.setVisibility(View.VISIBLE); } else { label.setVisibility(View.GONE); } - for (int i = 1, count = mDayNamesHeader.getChildCount(); i < count; i++) { - label = (TextView) mDayNamesHeader.getChildAt(i); + // Deal with day labels + final int count = mDayNamesHeader.getChildCount(); + for (int i = 0; i < count - 1; i++) { + label = (TextView) mDayNamesHeader.getChildAt(i + 1); if (mWeekDayTextAppearanceResId > -1) { label.setTextAppearance(mContext, mWeekDayTextAppearanceResId); } - if (i < mDaysPerWeek + 1) { - label.setText(mDayLabels[i - 1]); + if (i < mDaysPerWeek) { + label.setText(mDayLabels[i]); label.setVisibility(View.VISIBLE); } else { label.setVisibility(View.GONE); diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 109fcfe..54cc3f4 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -251,14 +251,14 @@ public class GridLayout extends ViewGroup { // Instance variables - final Axis horizontalAxis = new Axis(true); - final Axis verticalAxis = new Axis(false); - int orientation = DEFAULT_ORIENTATION; - boolean useDefaultMargins = DEFAULT_USE_DEFAULT_MARGINS; - int alignmentMode = DEFAULT_ALIGNMENT_MODE; - int defaultGap; - int lastLayoutParamsHashCode = UNINITIALIZED_HASH; - Printer printer = LOG_PRINTER; + final Axis mHorizontalAxis = new Axis(true); + final Axis mVerticalAxis = new Axis(false); + int mOrientation = DEFAULT_ORIENTATION; + boolean mUseDefaultMargins = DEFAULT_USE_DEFAULT_MARGINS; + int mAlignmentMode = DEFAULT_ALIGNMENT_MODE; + int mDefaultGap; + int mLastLayoutParamsHashCode = UNINITIALIZED_HASH; + Printer mPrinter = LOG_PRINTER; // Constructors @@ -267,7 +267,7 @@ public class GridLayout extends ViewGroup { */ public GridLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - defaultGap = context.getResources().getDimensionPixelOffset(R.dimen.default_gap); + mDefaultGap = context.getResources().getDimensionPixelOffset(R.dimen.default_gap); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridLayout); try { setRowCount(a.getInt(ROW_COUNT, DEFAULT_COUNT)); @@ -309,7 +309,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_orientation */ public int getOrientation() { - return orientation; + return mOrientation; } /** @@ -349,8 +349,8 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_orientation */ public void setOrientation(int orientation) { - if (this.orientation != orientation) { - this.orientation = orientation; + if (this.mOrientation != orientation) { + this.mOrientation = orientation; invalidateStructure(); requestLayout(); } @@ -369,7 +369,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_rowCount */ public int getRowCount() { - return verticalAxis.getCount(); + return mVerticalAxis.getCount(); } /** @@ -384,7 +384,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_rowCount */ public void setRowCount(int rowCount) { - verticalAxis.setCount(rowCount); + mVerticalAxis.setCount(rowCount); invalidateStructure(); requestLayout(); } @@ -402,7 +402,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_columnCount */ public int getColumnCount() { - return horizontalAxis.getCount(); + return mHorizontalAxis.getCount(); } /** @@ -417,7 +417,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_columnCount */ public void setColumnCount(int columnCount) { - horizontalAxis.setCount(columnCount); + mHorizontalAxis.setCount(columnCount); invalidateStructure(); requestLayout(); } @@ -433,7 +433,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_useDefaultMargins */ public boolean getUseDefaultMargins() { - return useDefaultMargins; + return mUseDefaultMargins; } /** @@ -463,7 +463,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_useDefaultMargins */ public void setUseDefaultMargins(boolean useDefaultMargins) { - this.useDefaultMargins = useDefaultMargins; + this.mUseDefaultMargins = useDefaultMargins; requestLayout(); } @@ -480,7 +480,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_alignmentMode */ public int getAlignmentMode() { - return alignmentMode; + return mAlignmentMode; } /** @@ -499,7 +499,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_alignmentMode */ public void setAlignmentMode(int alignmentMode) { - this.alignmentMode = alignmentMode; + this.mAlignmentMode = alignmentMode; requestLayout(); } @@ -514,7 +514,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_rowOrderPreserved */ public boolean isRowOrderPreserved() { - return verticalAxis.isOrderPreserved(); + return mVerticalAxis.isOrderPreserved(); } /** @@ -534,7 +534,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_rowOrderPreserved */ public void setRowOrderPreserved(boolean rowOrderPreserved) { - verticalAxis.setOrderPreserved(rowOrderPreserved); + mVerticalAxis.setOrderPreserved(rowOrderPreserved); invalidateStructure(); requestLayout(); } @@ -550,7 +550,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_columnOrderPreserved */ public boolean isColumnOrderPreserved() { - return horizontalAxis.isOrderPreserved(); + return mHorizontalAxis.isOrderPreserved(); } /** @@ -570,7 +570,7 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_columnOrderPreserved */ public void setColumnOrderPreserved(boolean columnOrderPreserved) { - horizontalAxis.setOrderPreserved(columnOrderPreserved); + mHorizontalAxis.setOrderPreserved(columnOrderPreserved); invalidateStructure(); requestLayout(); } @@ -581,9 +581,11 @@ public class GridLayout extends ViewGroup { * @see #setPrinter(android.util.Printer) * * @return the printer associated with this view + * + * @hide */ public Printer getPrinter() { - return printer; + return mPrinter; } /** @@ -593,9 +595,11 @@ public class GridLayout extends ViewGroup { * @param printer the printer associated with this layout * * @see #getPrinter() + * + * @hide */ public void setPrinter(Printer printer) { - this.printer = (printer == null) ? NO_PRINTER : printer; + this.mPrinter = (printer == null) ? NO_PRINTER : printer; } // Static utility methods @@ -643,7 +647,7 @@ public class GridLayout extends ViewGroup { if (c.getClass() == Space.class) { return 0; } - return defaultGap / 2; + return mDefaultGap / 2; } private int getDefaultMargin(View c, boolean isAtEdge, boolean horizontal, boolean leading) { @@ -651,11 +655,11 @@ public class GridLayout extends ViewGroup { } private int getDefaultMargin(View c, LayoutParams p, boolean horizontal, boolean leading) { - if (!useDefaultMargins) { + if (!mUseDefaultMargins) { return 0; } Spec spec = horizontal ? p.columnSpec : p.rowSpec; - Axis axis = horizontal ? horizontalAxis : verticalAxis; + Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; Interval span = spec.span; boolean leading1 = (horizontal && isLayoutRtl()) ? !leading : leading; boolean isAtEdge = leading1 ? (span.min == 0) : (span.max == axis.getCount()); @@ -672,10 +676,10 @@ public class GridLayout extends ViewGroup { } private int getMargin(View view, boolean horizontal, boolean leading) { - if (alignmentMode == ALIGN_MARGINS) { + if (mAlignmentMode == ALIGN_MARGINS) { return getMargin1(view, horizontal, leading); } else { - Axis axis = horizontal ? horizontalAxis : verticalAxis; + Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; int[] margins = leading ? axis.getLeadingMargins() : axis.getTrailingMargins(); LayoutParams lp = getLayoutParams(view); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; @@ -722,8 +726,8 @@ public class GridLayout extends ViewGroup { // install default indices for cells that don't define them private void validateLayoutParams() { - final boolean horizontal = (orientation == HORIZONTAL); - final Axis axis = horizontal ? horizontalAxis : verticalAxis; + final boolean horizontal = (mOrientation == HORIZONTAL); + final Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; final int count = (axis.definedCount != UNDEFINED) ? axis.definedCount : 0; int major = 0; @@ -779,9 +783,9 @@ public class GridLayout extends ViewGroup { } private void invalidateStructure() { - lastLayoutParamsHashCode = UNINITIALIZED_HASH; - horizontalAxis.invalidateStructure(); - verticalAxis.invalidateStructure(); + mLastLayoutParamsHashCode = UNINITIALIZED_HASH; + mHorizontalAxis.invalidateStructure(); + mVerticalAxis.invalidateStructure(); // This can end up being done twice. Better twice than not at all. invalidateValues(); } @@ -789,9 +793,9 @@ public class GridLayout extends ViewGroup { private void invalidateValues() { // Need null check because requestLayout() is called in View's initializer, // before we are set up. - if (horizontalAxis != null && verticalAxis != null) { - horizontalAxis.invalidateValues(); - verticalAxis.invalidateValues(); + if (mHorizontalAxis != null && mVerticalAxis != null) { + mHorizontalAxis.invalidateValues(); + mVerticalAxis.invalidateValues(); } } @@ -822,7 +826,7 @@ public class GridLayout extends ViewGroup { if (span.min != UNDEFINED && span.min < 0) { handleInvalidParams(groupName + " indices must be positive"); } - Axis axis = horizontal ? horizontalAxis : verticalAxis; + Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; int count = axis.definedCount; if (count != UNDEFINED) { if (span.max > count) { @@ -908,7 +912,7 @@ public class GridLayout extends ViewGroup { int right = getWidth() - getPaddingRight() - insets.right; int bottom = getHeight() - getPaddingBottom() - insets.bottom; - int[] xs = horizontalAxis.locations; + int[] xs = mHorizontalAxis.locations; if (xs != null) { for (int i = 0, length = xs.length; i < length; i++) { int x = left + xs[i]; @@ -916,7 +920,7 @@ public class GridLayout extends ViewGroup { } } - int[] ys = verticalAxis.locations; + int[] ys = mVerticalAxis.locations; if (ys != null) { for (int i = 0, length = ys.length; i < length; i++) { int y = top + ys[i]; @@ -973,11 +977,11 @@ public class GridLayout extends ViewGroup { } private void consistencyCheck() { - if (lastLayoutParamsHashCode == UNINITIALIZED_HASH) { + if (mLastLayoutParamsHashCode == UNINITIALIZED_HASH) { validateLayoutParams(); - lastLayoutParamsHashCode = computeLayoutParamsHashCode(); - } else if (lastLayoutParamsHashCode != computeLayoutParamsHashCode()) { - printer.println("The fields of some layout parameters were modified in between " + mLastLayoutParamsHashCode = computeLayoutParamsHashCode(); + } else if (mLastLayoutParamsHashCode != computeLayoutParamsHashCode()) { + mPrinter.println("The fields of some layout parameters were modified in between " + "layout operations. Check the javadoc for GridLayout.LayoutParams#rowSpec."); invalidateStructure(); consistencyCheck(); @@ -1005,11 +1009,11 @@ public class GridLayout extends ViewGroup { if (firstPass) { measureChildWithMargins2(c, widthSpec, heightSpec, lp.width, lp.height); } else { - boolean horizontal = (orientation == HORIZONTAL); + boolean horizontal = (mOrientation == HORIZONTAL); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; if (spec.alignment == FILL) { Interval span = spec.span; - Axis axis = horizontal ? horizontalAxis : verticalAxis; + Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; int[] locations = axis.getLocations(); int cellSize = locations[span.max] - locations[span.min]; int viewSize = cellSize - getTotalMargin(c, horizontal); @@ -1048,14 +1052,14 @@ public class GridLayout extends ViewGroup { int heightSansPadding; // Use the orientation property to decide which axis should be laid out first. - if (orientation == HORIZONTAL) { - widthSansPadding = horizontalAxis.getMeasure(widthSpecSansPadding); + if (mOrientation == HORIZONTAL) { + widthSansPadding = mHorizontalAxis.getMeasure(widthSpecSansPadding); measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, false); - heightSansPadding = verticalAxis.getMeasure(heightSpecSansPadding); + heightSansPadding = mVerticalAxis.getMeasure(heightSpecSansPadding); } else { - heightSansPadding = verticalAxis.getMeasure(heightSpecSansPadding); + heightSansPadding = mVerticalAxis.getMeasure(heightSpecSansPadding); measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, false); - widthSansPadding = horizontalAxis.getMeasure(widthSpecSansPadding); + widthSansPadding = mHorizontalAxis.getMeasure(widthSpecSansPadding); } int measuredWidth = Math.max(widthSansPadding + hPadding, getSuggestedMinimumWidth()); @@ -1114,11 +1118,11 @@ public class GridLayout extends ViewGroup { int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); - horizontalAxis.layout(targetWidth - paddingLeft - paddingRight); - verticalAxis.layout(targetHeight - paddingTop - paddingBottom); + mHorizontalAxis.layout(targetWidth - paddingLeft - paddingRight); + mVerticalAxis.layout(targetHeight - paddingTop - paddingBottom); - int[] hLocations = horizontalAxis.getLocations(); - int[] vLocations = verticalAxis.getLocations(); + int[] hLocations = mHorizontalAxis.getLocations(); + int[] vLocations = mVerticalAxis.getLocations(); for (int i = 0, N = getChildCount(); i < N; i++) { View c = getChildAt(i); @@ -1145,8 +1149,8 @@ public class GridLayout extends ViewGroup { Alignment hAlign = getAlignment(columnSpec.alignment, true); Alignment vAlign = getAlignment(rowSpec.alignment, false); - Bounds boundsX = horizontalAxis.getGroupBounds().getValue(i); - Bounds boundsY = verticalAxis.getGroupBounds().getValue(i); + Bounds boundsX = mHorizontalAxis.getGroupBounds().getValue(i); + Bounds boundsY = mVerticalAxis.getGroupBounds().getValue(i); // Gravity offsets: the location of the alignment group relative to its cell group. int gravityOffsetX = hAlign.getGravityOffset(c, cellWidth - boundsX.size(true)); @@ -1571,7 +1575,7 @@ public class GridLayout extends ViewGroup { removed.add(arc); } } - printer.println(axisName + " constraints: " + arcsToString(culprits) + + mPrinter.println(axisName + " constraints: " + arcsToString(culprits) + " are inconsistent; permanently removing: " + arcsToString(removed) + ". "); } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index a7d546a..15daf83 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -30,7 +30,10 @@ import android.view.ViewDebug; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; +import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; import android.view.animation.GridLayoutAnimationController; +import android.widget.AbsListView.LayoutParams; import android.widget.RemoteViews.RemoteView; @@ -2259,5 +2262,37 @@ public class GridView extends AbsListView { public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(GridView.class.getName()); + + final int columnsCount = getNumColumns(); + final int rowsCount = getCount() / columnsCount; + final CollectionInfo collectionInfo = CollectionInfo.obtain(columnsCount, rowsCount, false); + info.setCollectionInfo(collectionInfo); + } + + @Override + public void onInitializeAccessibilityNodeInfoForItem( + View view, int position, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoForItem(view, position, info); + + final int count = getCount(); + final int columnsCount = getNumColumns(); + final int rowsCount = count / columnsCount; + + final int row; + final int column; + if (!mStackFromBottom) { + column = position % columnsCount; + row = position / columnsCount; + } else { + final int invertedIndex = count - 1 - position; + + column = columnsCount - 1 - (invertedIndex % columnsCount); + row = rowsCount - 1 - invertedIndex / columnsCount; + } + + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final boolean isHeading = lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER; + final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(column, 1, row, 1, isHeading); + info.setCollectionItemInfo(itemInfo); } } diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java index 8919248..f2da765 100644 --- a/core/java/android/widget/ListPopupWindow.java +++ b/core/java/android/widget/ListPopupWindow.java @@ -32,12 +32,15 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.MeasureSpec; +import android.view.View.OnAttachStateChangeListener; import android.view.View.OnTouchListener; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.animation.AccelerateDecelerateInterpolator; +import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller; + import java.util.Locale; /** @@ -962,6 +965,33 @@ public class ListPopupWindow { } /** + * Returns an {@link OnTouchListener} that can be added to the source view + * to implement drag-to-open behavior. Generally, the source view should be + * the same view that was passed to {@link #setAnchorView}. + * <p> + * When the listener is set on a view, touching that view and dragging + * outside of its bounds will open the popup window. Lifting will select the + * currently touched list item. + * <p> + * Example usage: + * <pre>ListPopupWindow myPopup = new ListPopupWindow(context); + * myPopup.setAnchor(myAnchor); + * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor); + * myAnchor.setOnTouchListener(dragListener);</pre> + * + * @param src the view on which the resulting listener will be set + * @return a touch listener that controls drag-to-open behavior + */ + public OnTouchListener createDragToOpenListener(View src) { + return new ForwardingListener(src) { + @Override + public ListPopupWindow getPopup() { + return ListPopupWindow.this; + } + }; + } + + /** * <p>Builds the popup window's content and returns the height the popup * should have. Returns -1 when the content already exists.</p> * @@ -1133,18 +1163,32 @@ public class ListPopupWindow { * * @hide */ - public static abstract class ForwardingListener implements View.OnTouchListener { + public static abstract class ForwardingListener + implements View.OnTouchListener, View.OnAttachStateChangeListener { /** Scaled touch slop, used for detecting movement outside bounds. */ private final float mScaledTouchSlop; + /** Timeout before disallowing intercept on the source's parent. */ + private final int mTapTimeout; + + /** Source view from which events are forwarded. */ + private final View mSrc; + + /** Runnable used to prevent conflicts with scrolling parents. */ + private Runnable mDisallowIntercept; + /** Whether this listener is currently forwarding touch events. */ private boolean mForwarding; /** The id of the first pointer down in the current event stream. */ private int mActivePointerId; - public ForwardingListener(Context context) { - mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + public ForwardingListener(View src) { + mSrc = src; + mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop(); + mTapTimeout = ViewConfiguration.getTapTimeout(); + + src.addOnAttachStateChangeListener(this); } /** @@ -1164,15 +1208,29 @@ public class ListPopupWindow { final boolean wasForwarding = mForwarding; final boolean forwarding; if (wasForwarding) { - forwarding = onTouchForwarded(v, event) || !onForwardingStopped(); + forwarding = onTouchForwarded(event) || !onForwardingStopped(); } else { - forwarding = onTouchObserved(v, event) && onForwardingStarted(); + forwarding = onTouchObserved(event) && onForwardingStarted(); } mForwarding = forwarding; return forwarding || wasForwarding; } + @Override + public void onViewAttachedToWindow(View v) { + } + + @Override + public void onViewDetachedFromWindow(View v) { + mForwarding = false; + mActivePointerId = MotionEvent.INVALID_POINTER_ID; + + if (mDisallowIntercept != null) { + mSrc.removeCallbacks(mDisallowIntercept); + } + } + /** * Called when forwarding would like to start. * <p> @@ -1182,7 +1240,7 @@ public class ListPopupWindow { * * @return true to start forwarding, false otherwise */ - public boolean onForwardingStarted() { + protected boolean onForwardingStarted() { final ListPopupWindow popup = getPopup(); if (popup != null && !popup.isShowing()) { popup.show(); @@ -1199,7 +1257,7 @@ public class ListPopupWindow { * * @return true to stop forwarding, false otherwise */ - public boolean onForwardingStopped() { + protected boolean onForwardingStopped() { final ListPopupWindow popup = getPopup(); if (popup != null && popup.isShowing()) { popup.dismiss(); @@ -1210,29 +1268,45 @@ public class ListPopupWindow { /** * Observes motion events and determines when to start forwarding. * - * @param src view from which the event originated * @param srcEvent motion event in source view coordinates * @return true to start forwarding motion events, false otherwise */ - private boolean onTouchObserved(View src, MotionEvent srcEvent) { + private boolean onTouchObserved(MotionEvent srcEvent) { + final View src = mSrc; if (!src.isEnabled()) { return false; } - // The first pointer down is always the active pointer. final int actionMasked = srcEvent.getActionMasked(); - if (actionMasked == MotionEvent.ACTION_DOWN) { - mActivePointerId = srcEvent.getPointerId(0); - } - - final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId); - if (activePointerIndex >= 0) { - final float x = srcEvent.getX(activePointerIndex); - final float y = srcEvent.getY(activePointerIndex); - if (!src.pointInView(x, y, mScaledTouchSlop)) { - // The pointer has moved outside of the view. - return true; - } + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = srcEvent.getPointerId(0); + if (mDisallowIntercept == null) { + mDisallowIntercept = new DisallowIntercept(); + } + src.postDelayed(mDisallowIntercept, mTapTimeout); + break; + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId); + if (activePointerIndex >= 0) { + final float x = srcEvent.getX(activePointerIndex); + final float y = srcEvent.getY(activePointerIndex); + if (!src.pointInView(x, y, mScaledTouchSlop)) { + // The pointer has moved outside of the view. + if (mDisallowIntercept != null) { + src.removeCallbacks(mDisallowIntercept); + } + src.getParent().requestDisallowInterceptTouchEvent(true); + return true; + } + } + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + if (mDisallowIntercept != null) { + src.removeCallbacks(mDisallowIntercept); + } + break; } return false; @@ -1242,11 +1316,11 @@ public class ListPopupWindow { * Handled forwarded motion events and determines when to stop * forwarding. * - * @param src view from which the event originated * @param srcEvent motion event in source view coordinates * @return true to continue forwarding motion events, false to cancel */ - private boolean onTouchForwarded(View src, MotionEvent srcEvent) { + private boolean onTouchForwarded(MotionEvent srcEvent) { + final View src = mSrc; final ListPopupWindow popup = getPopup(); if (popup == null || !popup.isShowing()) { return false; @@ -1267,6 +1341,14 @@ public class ListPopupWindow { dstEvent.recycle(); return handled; } + + private class DisallowIntercept implements Runnable { + @Override + public void run() { + final ViewParent parent = mSrc.getParent(); + parent.requestDisallowInterceptTouchEvent(true); + } + } } /** @@ -1276,8 +1358,6 @@ public class ListPopupWindow { * passed to the drop down in this mode; the list only looks focused.</p> */ private static class DropDownListView extends ListView { - private static final String TAG = ListPopupWindow.TAG + ".DropDownListView"; - /** Duration in milliseconds of the drag-to-open click animation. */ private static final long CLICK_ANIM_DURATION = 150; @@ -1339,6 +1419,9 @@ public class ListPopupWindow { /** Current drag-to-open click animation, if any. */ private Animator mClickAnimation; + /** Helper for drag-to-open auto scrolling. */ + private AbsListViewAutoScroller mScrollHelper; + /** * <p>Creates a new list view wrapper.</p> * @@ -1399,6 +1482,17 @@ public class ListPopupWindow { clearPressedItem(); } + // Manage automatic scrolling. + if (handledEvent) { + if (mScrollHelper == null) { + mScrollHelper = new AbsListViewAutoScroller(this); + } + mScrollHelper.setEnabled(true); + mScrollHelper.onTouch(this, event); + } else if (mScrollHelper != null) { + mScrollHelper.setEnabled(false); + } + return handledEvent; } diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 2f42ae3..941ddfc 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -42,7 +42,8 @@ import android.view.ViewParent; import android.view.ViewRootImpl; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeProvider; +import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; +import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; import android.widget.RemoteViews.RemoteView; import java.util.ArrayList; @@ -1507,10 +1508,6 @@ public class ListView extends AbsListView { View oldFirst = null; View newSel = null; - AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null; - View accessibilityFocusLayoutRestoreView = null; - int accessibilityFocusPosition = INVALID_POSITION; - // Remember stuff we will need down below switch (mLayoutMode) { case LAYOUT_SET_SELECTION: @@ -1565,31 +1562,14 @@ public class ListView extends AbsListView { setSelectedPositionInt(mNextSelectedPosition); - // Remember which child, if any, had accessibility focus. This must - // occur before recycling any views, since that will clear - // accessibility focus. - // TODO: This should rely on transient state. - final ViewRootImpl viewRootImpl = getViewRootImpl(); - if (viewRootImpl != null) { - final View accessFocusedView = viewRootImpl.getAccessibilityFocusedHost(); - if (accessFocusedView != null) { - final View accessFocusedChild = findAccessibilityFocusedChild( - accessFocusedView); - if (accessFocusedChild != null) { - if (!dataChanged || isDirectChildHeaderOrFooter(accessFocusedChild)) { - // If the views won't be changing, try to maintain - // focus on the current view host and (if - // applicable) its virtual view. - accessibilityFocusLayoutRestoreView = accessFocusedView; - accessibilityFocusLayoutRestoreNode = viewRootImpl - .getAccessibilityFocusedVirtualView(); - } else { - // Otherwise, try to maintain focus at the same - // position. - accessibilityFocusPosition = getPositionForView(accessFocusedChild); - } - } - } + // Remember which child, if any, had accessibility focus. + final int accessibilityFocusPosition; + final View accessFocusedChild = getAccessibilityFocusedChild(); + if (accessFocusedChild != null) { + accessibilityFocusPosition = getPositionForView(accessFocusedChild); + accessFocusedChild.setHasTransientState(true); + } else { + accessibilityFocusPosition = INVALID_POSITION; } // Ensure the child containing focus, if any, has transient state. @@ -1704,25 +1684,20 @@ public class ListView extends AbsListView { } } - // Attempt to restore accessibility focus. - if (accessibilityFocusLayoutRestoreView != null) { - final AccessibilityNodeProvider provider = - accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider(); - if ((accessibilityFocusLayoutRestoreNode != null) && (provider != null)) { - final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId( - accessibilityFocusLayoutRestoreNode.getSourceNodeId()); - provider.performAction(virtualViewId, - AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); - } else { - accessibilityFocusLayoutRestoreView.requestAccessibilityFocus(); - } - } else if (accessibilityFocusPosition != INVALID_POSITION) { - // Bound the position within the visible children. - final int position = MathUtils.constrain( - (accessibilityFocusPosition - mFirstPosition), 0, (getChildCount() - 1)); - final View restoreView = getChildAt(position); - if (restoreView != null) { - restoreView.requestAccessibilityFocus(); + if (accessFocusedChild != null) { + accessFocusedChild.setHasTransientState(false); + + // If we failed to maintain accessibility focus on the previous + // view, attempt to restore it to the previous position. + if (!accessFocusedChild.isAccessibilityFocused() + && accessibilityFocusPosition != INVALID_POSITION) { + // Bound the position within the visible children. + final int position = MathUtils.constrain( + accessibilityFocusPosition - mFirstPosition, 0, getChildCount() - 1); + final View restoreView = getChildAt(position); + if (restoreView != null) { + restoreView.requestAccessibilityFocus(); + } } } @@ -1754,42 +1729,31 @@ public class ListView extends AbsListView { } /** - * @param focusedView the view that has accessibility focus. - * @return the direct child that contains accessibility focus. + * @return the direct child that contains accessibility focus, or null if no + * child contains accessibility focus */ - private View findAccessibilityFocusedChild(View focusedView) { + private View getAccessibilityFocusedChild() { + final ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl == null) { + return null; + } + + View focusedView = viewRootImpl.getAccessibilityFocusedHost(); + if (focusedView == null) { + return null; + } + ViewParent viewParent = focusedView.getParent(); while ((viewParent instanceof View) && (viewParent != this)) { focusedView = (View) viewParent; viewParent = viewParent.getParent(); } + if (!(viewParent instanceof View)) { return null; } - return focusedView; - } - - /** - * @param child a direct child of this list. - * @return Whether child is a header or footer view. - */ - private boolean isDirectChildHeaderOrFooter(View child) { - final ArrayList<FixedViewInfo> headers = mHeaderViewInfos; - final int numHeaders = headers.size(); - for (int i = 0; i < numHeaders; i++) { - if (child == headers.get(i).view) { - return true; - } - } - final ArrayList<FixedViewInfo> footers = mFooterViewInfos; - final int numFooters = footers.size(); - for (int i = 0; i < numFooters; i++) { - if (child == footers.get(i).view) { - return true; - } - } - return false; + return focusedView; } /** @@ -3816,5 +3780,20 @@ public class ListView extends AbsListView { public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(ListView.class.getName()); + + final int count = getCount(); + final CollectionInfo collectionInfo = CollectionInfo.obtain(1, count, false); + info.setCollectionInfo(collectionInfo); + } + + @Override + public void onInitializeAccessibilityNodeInfoForItem( + View view, int position, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoForItem(view, position, info); + + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final boolean isHeading = lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER; + final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(0, 1, position, 1, isHeading); + info.setCollectionItemInfo(itemInfo); } } diff --git a/core/java/android/widget/PopupMenu.java b/core/java/android/widget/PopupMenu.java index 6a6d767..e5344c6 100644 --- a/core/java/android/widget/PopupMenu.java +++ b/core/java/android/widget/PopupMenu.java @@ -26,6 +26,8 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.ListPopupWindow.ForwardingListener; /** * A PopupMenu displays a {@link Menu} in a modal popup window anchored to a {@link View}. @@ -40,6 +42,7 @@ public class PopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback { private MenuPopupHelper mPopup; private OnMenuItemClickListener mMenuItemClickListener; private OnDismissListener mDismissListener; + private OnTouchListener mDragListener; /** * Callback interface used to notify the application that the menu has closed. @@ -71,6 +74,33 @@ public class PopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback { } /** + * Returns an {@link OnTouchListener} that can be added to the anchor view + * to implement drag-to-open behavior. + * <p> + * When the listener is set on a view, touching that view and dragging + * outside of its bounds will open the popup window. Lifting will select the + * currently touched list item. + * <p> + * Example usage: + * <pre>PopupMenu myPopup = new PopupMenu(context, myAnchor); + * myAnchor.setOnTouchListener(myPopup.getDragToOpenListener());</pre> + * + * @return a touch listener that controls drag-to-open behavior + */ + public OnTouchListener getDragToOpenListener() { + if (mDragListener == null) { + mDragListener = new ForwardingListener(mAnchor) { + @Override + public ListPopupWindow getPopup() { + return mPopup.getPopup(); + } + }; + } + + return mDragListener; + } + + /** * @return the {@link Menu} associated with this popup. Populate the returned Menu with * items before calling {@link #show()}. * diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index 7c7df96..b87ed7a 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -198,7 +198,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { } mPopup = popup; - mForwardingListener = new ForwardingListener(context) { + mForwardingListener = new ForwardingListener(this) { @Override public ListPopupWindow getPopup() { return popup; @@ -675,7 +675,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { info.setClassName(Spinner.class.getName()); if (mAdapter != null) { - info.setOpensPopup(true); + info.setCanOpenPopup(true); } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index d5fc21c..a2d48a8 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -4406,6 +4406,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void setError(CharSequence error, Drawable icon) { createEditorIfNeeded(); mEditor.setError(error, icon); + notifyViewAccessibilityStateChangedIfNeeded(); } @Override @@ -8147,6 +8148,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mEditor != null) { info.setInputType(mEditor.mInputType); + + if (mEditor.mError != null) { + info.setContentInvalid(true); + } } if (!TextUtils.isEmpty(mText)) { diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index 1c1d77a..c26cb24 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -443,6 +443,10 @@ public class TimePicker extends FrameLayout { * Set the current hour. */ public void setCurrentHour(Integer currentHour) { + setCurrentHour(currentHour, true); + } + + private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { // why was Integer used in the first place? if (currentHour == null || currentHour == getCurrentHour()) { return; @@ -463,7 +467,9 @@ public class TimePicker extends FrameLayout { updateAmPmControl(); } mHourSpinner.setValue(currentHour); - onTimeChanged(); + if (notifyTimeChanged) { + onTimeChanged(); + } } /** @@ -481,8 +487,10 @@ public class TimePicker extends FrameLayout { mIs24HourView = is24HourView; getHourFormatData(); updateHourControl(); - // set value after spinner range is updated - setCurrentHour(currentHour); + // set value after spinner range is updated - be aware that because mIs24HourView has + // changed then getCurrentHour() is not equal to the currentHour we cached before so + // explicitly ask for *not* propagating any onTimeChanged() + setCurrentHour(currentHour, false /* no onTimeChanged() */); updateMinuteControl(); updateAmPmControl(); } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index aa94728..8819237 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -22,11 +22,13 @@ import com.android.internal.content.PackageMonitor; import android.app.ActivityManager; import android.app.ActivityManagerNative; +import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; +import android.content.pm.IPackageManager; import android.content.pm.LabeledIntent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -45,7 +47,6 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Button; -import android.widget.GridView; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; @@ -71,13 +72,13 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte private PackageManager mPm; private boolean mAlwaysUseOption; private boolean mShowExtended; - private GridView mGrid; + private ListView mListView; private Button mAlwaysButton; private Button mOnceButton; private int mIconDpi; private int mIconSize; private int mMaxColumns; - private int mLastSelected = GridView.INVALID_POSITION; + private int mLastSelected = ListView.INVALID_POSITION; private boolean mRegistered; private final PackageMonitor mPackageMonitor = new PackageMonitor() { @@ -139,17 +140,15 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte finish(); return; } else if (count > 1) { - ap.mView = getLayoutInflater().inflate(R.layout.resolver_grid, null); - mGrid = (GridView) ap.mView.findViewById(R.id.resolver_grid); - mGrid.setAdapter(mAdapter); - mGrid.setOnItemClickListener(this); - mGrid.setOnItemLongClickListener(new ItemLongClickListener()); + ap.mView = getLayoutInflater().inflate(R.layout.resolver_list, null); + mListView = (ListView) ap.mView.findViewById(R.id.resolver_list); + mListView.setAdapter(mAdapter); + mListView.setOnItemClickListener(this); + mListView.setOnItemLongClickListener(new ItemLongClickListener()); if (alwaysUseOption) { - mGrid.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); } - - resizeGrid(); } else if (count == 1) { startActivity(mAdapter.intentForPosition(0)); mPackageMonitor.unregister(); @@ -172,11 +171,11 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte mAlwaysUseOption = false; } } - } - - void resizeGrid() { - final int itemCount = mAdapter.getCount(); - mGrid.setNumColumns(Math.min(itemCount, mMaxColumns)); + final int initialHighlight = mAdapter.getInitialHighlight(); + if (initialHighlight >= 0) { + mListView.setItemChecked(initialHighlight, true); + onItemClick(null, null, initialHighlight, 0); // Other entries are not used + } } Drawable getIcon(Resources res, int resId) { @@ -247,26 +246,26 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); if (mAlwaysUseOption) { - final int checkedPos = mGrid.getCheckedItemPosition(); - final boolean enabled = checkedPos != GridView.INVALID_POSITION; + final int checkedPos = mListView.getCheckedItemPosition(); + final boolean enabled = checkedPos != ListView.INVALID_POSITION; mLastSelected = checkedPos; mAlwaysButton.setEnabled(enabled); mOnceButton.setEnabled(enabled); if (enabled) { - mGrid.setSelection(checkedPos); + mListView.setSelection(checkedPos); } } } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - final int checkedPos = mGrid.getCheckedItemPosition(); - final boolean hasValidSelection = checkedPos != GridView.INVALID_POSITION; + final int checkedPos = mListView.getCheckedItemPosition(); + final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION; if (mAlwaysUseOption && (!hasValidSelection || mLastSelected != checkedPos)) { mAlwaysButton.setEnabled(hasValidSelection); mOnceButton.setEnabled(hasValidSelection); if (hasValidSelection) { - mGrid.smoothScrollToPosition(checkedPos); + mListView.smoothScrollToPosition(checkedPos); } mLastSelected = checkedPos; } else { @@ -276,7 +275,7 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte public void onButtonClick(View v) { final int id = v.getId(); - startSelected(mGrid.getCheckedItemPosition(), id == R.id.button_always); + startSelected(mListView.getCheckedItemPosition(), id == R.id.button_always); dismiss(); } @@ -288,7 +287,7 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte } protected void onIntentSelected(ResolveInfo ri, Intent intent, boolean alwaysCheck) { - if (alwaysCheck) { + if (mAlwaysUseOption) { // Build a reasonable intent filter, based on what matched. IntentFilter filter = new IntentFilter(); @@ -374,8 +373,19 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte r.activityInfo.name); if (r.match > bestMatch) bestMatch = r.match; } - getPackageManager().addPreferredActivity(filter, bestMatch, set, - intent.getComponent()); + if (alwaysCheck) { + getPackageManager().addPreferredActivity(filter, bestMatch, set, + intent.getComponent()); + } else { + try { + AppGlobals.getPackageManager().setLastChosenActivity(intent, + intent.resolveTypeIfNeeded(getContentResolver()), + PackageManager.MATCH_DEFAULT_ONLY, + filter, bestMatch, intent.getComponent()); + } catch (RemoteException re) { + Log.d(TAG, "Error calling setLastChosenActivity\n" + re); + } + } } } @@ -410,11 +420,13 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte private final class ResolveListAdapter extends BaseAdapter { private final Intent[] mInitialIntents; private final List<ResolveInfo> mBaseResolveList; + private ResolveInfo mLastChosen; private final Intent mIntent; private final int mLaunchedFromUid; private final LayoutInflater mInflater; private List<DisplayResolveInfo> mList; + private int mInitialHighlight = -1; public ResolveListAdapter(Context context, Intent intent, Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid) { @@ -436,14 +448,24 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte if (newItemCount == 0) { // We no longer have any items... just finish the activity. finish(); - } else if (newItemCount != oldItemCount) { - resizeGrid(); } } + public int getInitialHighlight() { + return mInitialHighlight; + } + private void rebuildList() { List<ResolveInfo> currentResolveList; + try { + mLastChosen = AppGlobals.getPackageManager().getLastChosenActivity( + mIntent, mIntent.resolveTypeIfNeeded(getContentResolver()), + PackageManager.MATCH_DEFAULT_ONLY); + } catch (RemoteException re) { + Log.d(TAG, "Error calling setLastChosenActivity\n" + re); + } + mList.clear(); if (mBaseResolveList != null) { currentResolveList = mBaseResolveList; @@ -556,6 +578,12 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte // Process labels from start to i int num = end - start+1; if (num == 1) { + if (mLastChosen != null + && mLastChosen.activityInfo.packageName.equals( + ro.activityInfo.packageName) + && mLastChosen.activityInfo.name.equals(ro.activityInfo.name)) { + mInitialHighlight = mList.size(); + } // No duplicate labels. Use label for entry at start mList.add(new DisplayResolveInfo(ro, roLabel, null, null)); } else { @@ -585,6 +613,12 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte } for (int k = start; k <= end; k++) { ResolveInfo add = rList.get(k); + if (mLastChosen != null + && mLastChosen.activityInfo.packageName.equals( + add.activityInfo.packageName) + && mLastChosen.activityInfo.name.equals(add.activityInfo.name)) { + mInitialHighlight = mList.size(); + } if (usePkg) { // Use application name for all entries from start to end-1 mList.add(new DisplayResolveInfo(add, roLabel, diff --git a/core/java/com/android/internal/notification/DemoContactNotificationScorer.java b/core/java/com/android/internal/notification/DemoContactNotificationScorer.java index 62529e9..f484724 100644 --- a/core/java/com/android/internal/notification/DemoContactNotificationScorer.java +++ b/core/java/com/android/internal/notification/DemoContactNotificationScorer.java @@ -41,8 +41,8 @@ import java.util.List; */ public class DemoContactNotificationScorer implements NotificationScorer { - private static final String TAG = "StarredContactScoring"; - private static final boolean DBG = true; + private static final String TAG = "DemoContactNotificationScorer"; + private static final boolean DBG = false; protected static final boolean ENABLE_CONTACT_SCORER = true; private static final String SETTING_ENABLE_SCORER = "contact_scorer_enabled"; diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java index cdd2ad1..a85d5fe 100644 --- a/core/java/com/android/internal/os/RuntimeInit.java +++ b/core/java/com/android/internal/os/RuntimeInit.java @@ -334,6 +334,7 @@ public class RuntimeInit { } } catch (Throwable t2) { Slog.e(TAG, "Error reporting WTF", t2); + Slog.e(TAG, "Original WTF:", t); } } diff --git a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java index 5d0a603..f060efd 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java +++ b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java @@ -565,7 +565,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter setVisibility(VISIBLE); setEnabled(true); - setOnTouchListener(new ForwardingListener(context) { + setOnTouchListener(new ForwardingListener(this) { @Override public ListPopupWindow getPopup() { if (mOverflowPopup == null) { @@ -630,7 +630,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - info.setOpensPopup(true); + info.setCanOpenPopup(true); } } diff --git a/core/java/com/android/internal/view/menu/ListMenuItemView.java b/core/java/com/android/internal/view/menu/ListMenuItemView.java index 85d9cbd..a2a4acc 100644 --- a/core/java/com/android/internal/view/menu/ListMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ListMenuItemView.java @@ -276,7 +276,7 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView super.onInitializeAccessibilityNodeInfo(info); if (mItemData != null && mItemData.hasSubMenu()) { - info.setOpensPopup(true); + info.setCanOpenPopup(true); } } } diff --git a/core/java/com/android/internal/widget/AutoScrollHelper.java b/core/java/com/android/internal/widget/AutoScrollHelper.java new file mode 100644 index 0000000..afa4103 --- /dev/null +++ b/core/java/com/android/internal/widget/AutoScrollHelper.java @@ -0,0 +1,924 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.content.res.Resources; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.AbsListView; + +/** + * AutoScrollHelper is a utility class for adding automatic edge-triggered + * scrolling to Views. + * <p> + * <b>Note:</b> Implementing classes are responsible for overriding the + * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and + * {@link #canTargetScrollVertically} methods. See + * {@link AbsListViewAutoScroller} for an {@link android.widget.AbsListView} + * -specific implementation. + * <p> + * <h1>Activation</h1> Automatic scrolling starts when the user touches within + * an activation area. By default, activation areas are defined as the top, + * left, right, and bottom 20% of the host view's total area. Touching within + * the top activation area scrolls up, left scrolls to the left, and so on. + * <p> + * As the user touches closer to the extreme edge of the activation area, + * scrolling accelerates up to a maximum velocity. When using the default edge + * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds + * will scroll at the maximum velocity. + * <p> + * The following activation properties may be configured: + * <ul> + * <li>Delay after entering activation area before auto-scrolling begins, see + * {@link #setActivationDelay}. Default value is + * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps. + * <li>Location of activation areas, see {@link #setEdgeType}. Default value is + * {@link #EDGE_TYPE_INSIDE_EXTEND}. + * <li>Size of activation areas relative to view size, see + * {@link #setRelativeEdges}. Default value is 20% for both vertical and + * horizontal edges. + * <li>Maximum size used to constrain relative size, see + * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}. + * </ul> + * <h1>Scrolling</h1> When automatic scrolling is active, the helper will + * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets. + * <p> + * The following scrolling properties may be configured: + * <ul> + * <li>Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default + * value is 2500 milliseconds. + * <li>Acceleration ramp-down duration, see {@link #setRampDownDuration}. + * Default value is 500 milliseconds. + * <li>Target velocity relative to view size, see {@link #setRelativeVelocity}. + * Default value is 100% per second for both vertical and horizontal. + * <li>Minimum velocity used to constrain relative velocity, see + * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the + * larger of either this value or the relative target value. Default value is + * approximately 5 centimeters or 315 dips per second. + * <li>Maximum velocity used to constrain relative velocity, see + * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or + * 1575 dips per second. + * </ul> + */ +public abstract class AutoScrollHelper implements View.OnTouchListener { + /** + * Constant passed to {@link #setRelativeEdges} or + * {@link #setRelativeVelocity}. Using this value ensures that the computed + * relative value is ignored and the absolute maximum value is always used. + */ + public static final float RELATIVE_UNSPECIFIED = 0; + + /** + * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity}, + * or {@link #setMinimumVelocity}. Using this value ensures that the + * computed relative value is always used without constraining to a + * particular minimum or maximum value. + */ + public static final float NO_MAX = Float.MAX_VALUE; + + /** + * Constant passed to {@link #setMaximumEdges}, or + * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this + * value ensures that the computed relative value is always used without + * constraining to a particular minimum or maximum value. + */ + public static final float NO_MIN = 0; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending inward. Moving outside the view bounds will stop scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_INSIDE = 0; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending inward. After activation begins, moving outside the view + * bounds will continue scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_INSIDE_EXTEND = 1; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending outward. Moving inside the view bounds will stop scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_OUTSIDE = 2; + + private static final int HORIZONTAL = 0; + private static final int VERTICAL = 1; + + /** Scroller used to control acceleration toward maximum velocity. */ + private final ClampedScroller mScroller = new ClampedScroller(); + + /** Interpolator used to scale velocity with touch position. */ + private final Interpolator mEdgeInterpolator = new AccelerateInterpolator(); + + /** The view to auto-scroll. Might not be the source of touch events. */ + private final View mTarget; + + /** Runnable used to animate scrolling. */ + private Runnable mRunnable; + + /** Edge insets used to activate auto-scrolling. */ + private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED }; + + /** Clamping values for edge insets used to activate auto-scrolling. */ + private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX }; + + /** The type of edge being used. */ + private int mEdgeType; + + /** Delay after entering an activation edge before auto-scrolling begins. */ + private int mActivationDelay; + + /** Relative scrolling velocity at maximum edge distance. */ + private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED }; + + /** Clamping values used for scrolling velocity. */ + private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN }; + + /** Clamping values used for scrolling velocity. */ + private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX }; + + /** Whether to start activation immediately. */ + private boolean mAlreadyDelayed; + + /** Whether to reset the scroller start time on the next animation. */ + private boolean mNeedsReset; + + /** Whether to send a cancel motion event to the target view. */ + private boolean mNeedsCancel; + + /** Whether the auto-scroller is actively scrolling. */ + private boolean mAnimating; + + /** Whether the auto-scroller is enabled. */ + private boolean mEnabled; + + /** Whether the auto-scroller consumes events when scrolling. */ + private boolean mExclusive; + + // Default values. + private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND; + private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315; + private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575; + private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX; + private static final float DEFAULT_RELATIVE_EDGE = 0.2f; + private static final float DEFAULT_RELATIVE_VELOCITY = 1f; + private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout(); + private static final int DEFAULT_RAMP_UP_DURATION = 2500; + private static final int DEFAULT_RAMP_DOWN_DURATION = 500; + + /** + * Creates a new helper for scrolling the specified target view. + * <p> + * The resulting helper may be configured by chaining setter calls and + * should be set as a touch listener on the target view. + * <p> + * By default, the helper is disabled and will not respond to touch events + * until it is enabled using {@link #setEnabled}. + * + * @param target The view to automatically scroll. + */ + public AutoScrollHelper(View target) { + mTarget = target; + + final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); + final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f); + final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f); + setMaximumVelocity(maxVelocity, maxVelocity); + setMinimumVelocity(minVelocity, minVelocity); + + setEdgeType(DEFAULT_EDGE_TYPE); + setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE); + setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE); + setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY); + setActivationDelay(DEFAULT_ACTIVATION_DELAY); + setRampUpDuration(DEFAULT_RAMP_UP_DURATION); + setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION); + } + + /** + * Sets whether the scroll helper is enabled and should respond to touch + * events. + * + * @param enabled Whether the scroll helper is enabled. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setEnabled(boolean enabled) { + if (mEnabled && !enabled) { + requestStop(); + } + + mEnabled = enabled; + return this; + } + + /** + * @return True if this helper is enabled and responding to touch events. + */ + public boolean isEnabled() { + return mEnabled; + } + + /** + * Enables or disables exclusive handling of touch events during scrolling. + * By default, exclusive handling is disabled and the target view receives + * all touch events. + * <p> + * When enabled, {@link #onTouch} will return true if the helper is + * currently scrolling and false otherwise. + * + * @param exclusive True to exclusively handle touch events during scrolling, + * false to allow the target view to receive all touch events. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setExclusive(boolean exclusive) { + mExclusive = exclusive; + return this; + } + + /** + * Indicates whether the scroll helper handles touch events exclusively + * during scrolling. + * + * @return True if exclusive handling of touch events during scrolling is + * enabled, false otherwise. + * @see #setExclusive(boolean) + */ + public boolean isExclusive() { + return mExclusive; + } + + /** + * Sets the absolute maximum scrolling velocity. + * <p> + * If relative velocity is not specified, scrolling will always reach the + * same maximum velocity. If both relative and maximum velocities are + * specified, the maximum velocity will be used to clamp the calculated + * relative velocity. + * + * @param horizontalMax The maximum horizontal scrolling velocity, or + * {@link #NO_MAX} to leave the relative value unconstrained. + * @param verticalMax The maximum vertical scrolling velocity, or + * {@link #NO_MAX} to leave the relative value unconstrained. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) { + mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f; + mMaximumVelocity[VERTICAL] = verticalMax / 1000f; + return this; + } + + /** + * Sets the absolute minimum scrolling velocity. + * <p> + * If both relative and minimum velocities are specified, the minimum + * velocity will be used to clamp the calculated relative velocity. + * + * @param horizontalMin The minimum horizontal scrolling velocity, or + * {@link #NO_MIN} to leave the relative value unconstrained. + * @param verticalMin The minimum vertical scrolling velocity, or + * {@link #NO_MIN} to leave the relative value unconstrained. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) { + mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f; + mMinimumVelocity[VERTICAL] = verticalMin / 1000f; + return this; + } + + /** + * Sets the target scrolling velocity relative to the host view's + * dimensions. + * <p> + * If both relative and maximum velocities are specified, the maximum + * velocity will be used to clamp the calculated relative velocity. + * + * @param horizontal The target horizontal velocity as a fraction of the + * host view width per second, or {@link #RELATIVE_UNSPECIFIED} + * to ignore. + * @param vertical The target vertical velocity as a fraction of the host + * view height per second, or {@link #RELATIVE_UNSPECIFIED} to + * ignore. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) { + mRelativeVelocity[HORIZONTAL] = horizontal / 1000f; + mRelativeVelocity[VERTICAL] = vertical / 1000f; + return this; + } + + /** + * Sets the activation edge type, one of: + * <ul> + * <li>{@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside + * the bounds of the host view. If touch moves outside the bounds, scrolling + * will stop. + * <li>{@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to + * scroll when touch moves outside the bounds of the host view. + * <li>{@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches + * that move outside the bounds of the host view. + * </ul> + * + * @param type The type of edge to use. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setEdgeType(int type) { + mEdgeType = type; + return this; + } + + /** + * Sets the activation edge size relative to the host view's dimensions. + * <p> + * If both relative and maximum edges are specified, the maximum edge will + * be used to constrain the calculated relative edge size. + * + * @param horizontal The horizontal edge size as a fraction of the host view + * width, or {@link #RELATIVE_UNSPECIFIED} to always use the + * maximum value. + * @param vertical The vertical edge size as a fraction of the host view + * height, or {@link #RELATIVE_UNSPECIFIED} to always use the + * maximum value. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) { + mRelativeEdges[HORIZONTAL] = horizontal; + mRelativeEdges[VERTICAL] = vertical; + return this; + } + + /** + * Sets the absolute maximum edge size. + * <p> + * If relative edge size is not specified, activation edges will always be + * the maximum edge size. If both relative and maximum edges are specified, + * the maximum edge will be used to constrain the calculated relative edge + * size. + * + * @param horizontalMax The maximum horizontal edge size in pixels, or + * {@link #NO_MAX} to use the unconstrained calculated relative + * value. + * @param verticalMax The maximum vertical edge size in pixels, or + * {@link #NO_MAX} to use the unconstrained calculated relative + * value. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) { + mMaximumEdges[HORIZONTAL] = horizontalMax; + mMaximumEdges[VERTICAL] = verticalMax; + return this; + } + + /** + * Sets the delay after entering an activation edge before activation of + * auto-scrolling. By default, the activation delay is set to + * {@link ViewConfiguration#getTapTimeout()}. + * <p> + * Specifying a delay of zero will start auto-scrolling immediately after + * the touch position enters an activation edge. + * + * @param delayMillis The activation delay in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setActivationDelay(int delayMillis) { + mActivationDelay = delayMillis; + return this; + } + + /** + * Sets the amount of time after activation of auto-scrolling that is takes + * to reach target velocity for the current touch position. + * <p> + * Specifying a duration greater than zero prevents sudden jumps in + * velocity. + * + * @param durationMillis The ramp-up duration in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRampUpDuration(int durationMillis) { + mScroller.setRampUpDuration(durationMillis); + return this; + } + + /** + * Sets the amount of time after de-activation of auto-scrolling that is + * takes to slow to a stop. + * <p> + * Specifying a duration greater than zero prevents sudden jumps in + * velocity. + * + * @param durationMillis The ramp-down duration in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRampDownDuration(int durationMillis) { + mScroller.setRampDownDuration(durationMillis); + return this; + } + + /** + * Handles touch events by activating automatic scrolling, adjusting scroll + * velocity, or stopping. + * <p> + * If {@link #isExclusive()} is false, always returns false so that + * the host view may handle touch events. Otherwise, returns true when + * automatic scrolling is active and false otherwise. + */ + @Override + public boolean onTouch(View v, MotionEvent event) { + if (!mEnabled) { + return false; + } + + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mNeedsCancel = true; + mAlreadyDelayed = false; + // $FALL-THROUGH$ + case MotionEvent.ACTION_MOVE: + final float xTargetVelocity = computeTargetVelocity( + HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth()); + final float yTargetVelocity = computeTargetVelocity( + VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight()); + mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity); + + // If the auto scroller was not previously active, but it should + // be, then update the state and start animations. + if (!mAnimating && shouldAnimate()) { + startAnimating(); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + requestStop(); + break; + } + + return mExclusive && mAnimating; + } + + /** + * @return whether the target is able to scroll in the requested direction + */ + private boolean shouldAnimate() { + final ClampedScroller scroller = mScroller; + final int verticalDirection = scroller.getVerticalDirection(); + final int horizontalDirection = scroller.getHorizontalDirection(); + + return verticalDirection != 0 && canTargetScrollVertically(verticalDirection) + || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection); + } + + /** + * Starts the scroll animation. + */ + private void startAnimating() { + if (mRunnable == null) { + mRunnable = new ScrollAnimationRunnable(); + } + + mAnimating = true; + mNeedsReset = true; + + if (!mAlreadyDelayed && mActivationDelay > 0) { + mTarget.postOnAnimationDelayed(mRunnable, mActivationDelay); + } else { + mRunnable.run(); + } + + // If we start animating again before the user lifts their finger, we + // already know it's not a tap and don't need an activation delay. + mAlreadyDelayed = true; + } + + /** + * Requests that the scroll animation slow to a stop. If there is an + * activation delay, this may occur between posting the animation and + * actually running it. + */ + private void requestStop() { + if (mNeedsReset) { + // The animation has been posted, but hasn't run yet. Manually + // stopping animation will prevent it from running. + mAnimating = false; + } else { + mScroller.requestStop(); + } + } + + private float computeTargetVelocity( + int direction, float coordinate, float srcSize, float dstSize) { + final float relativeEdge = mRelativeEdges[direction]; + final float maximumEdge = mMaximumEdges[direction]; + final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate); + if (value == 0) { + // The edge in this direction is not activated. + return 0; + } + + final float relativeVelocity = mRelativeVelocity[direction]; + final float minimumVelocity = mMinimumVelocity[direction]; + final float maximumVelocity = mMaximumVelocity[direction]; + final float targetVelocity = relativeVelocity * dstSize; + + // Target velocity is adjusted for interpolated edge position, then + // clamped to the minimum and maximum values. Later, this value will be + // adjusted for time-based acceleration. + if (value > 0) { + return constrain(value * targetVelocity, minimumVelocity, maximumVelocity); + } else { + return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity); + } + } + + /** + * Override this method to scroll the target view by the specified number of + * pixels. + * + * @param deltaX The number of pixels to scroll by horizontally. + * @param deltaY The number of pixels to scroll by vertically. + */ + public abstract void scrollTargetBy(int deltaX, int deltaY); + + /** + * Override this method to return whether the target view can be scrolled + * horizontally in a certain direction. + * + * @param direction Negative to check scrolling left, positive to check + * scrolling right. + * @return true if the target view is able to horizontally scroll in the + * specified direction. + */ + public abstract boolean canTargetScrollHorizontally(int direction); + + /** + * Override this method to return whether the target view can be scrolled + * vertically in a certain direction. + * + * @param direction Negative to check scrolling up, positive to check + * scrolling down. + * @return true if the target view is able to vertically scroll in the + * specified direction. + */ + public abstract boolean canTargetScrollVertically(int direction); + + /** + * Returns the interpolated position of a touch point relative to an edge + * defined by its relative inset, its maximum absolute inset, and the edge + * interpolator. + * + * @param relativeValue The size of the inset relative to the total size. + * @param size Total size. + * @param maxValue The maximum size of the inset, used to clamp (relative * + * total). + * @param current Touch position within within the total size. + * @return Interpolated value of the touch position within the edge. + */ + private float getEdgeValue(float relativeValue, float size, float maxValue, float current) { + // For now, leading and trailing edges are always the same size. + final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue); + final float valueLeading = constrainEdgeValue(current, edgeSize); + final float valueTrailing = constrainEdgeValue(size - current, edgeSize); + final float value = (valueTrailing - valueLeading); + final float interpolated; + if (value < 0) { + interpolated = -mEdgeInterpolator.getInterpolation(-value); + } else if (value > 0) { + interpolated = mEdgeInterpolator.getInterpolation(value); + } else { + return 0; + } + + return constrain(interpolated, -1, 1); + } + + private float constrainEdgeValue(float current, float leading) { + if (leading == 0) { + return 0; + } + + switch (mEdgeType) { + case EDGE_TYPE_INSIDE: + case EDGE_TYPE_INSIDE_EXTEND: + if (current < leading) { + if (current >= 0) { + // Movement up to the edge is scaled. + return 1f - current / leading; + } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) { + // Movement beyond the edge is always maximum. + return 1f; + } + } + break; + case EDGE_TYPE_OUTSIDE: + if (current < 0) { + // Movement beyond the edge is scaled. + return current / -leading; + } + break; + } + + return 0; + } + + private static int constrain(int value, int min, int max) { + if (value > max) { + return max; + } else if (value < min) { + return min; + } else { + return value; + } + } + + private static float constrain(float value, float min, float max) { + if (value > max) { + return max; + } else if (value < min) { + return min; + } else { + return value; + } + } + + /** + * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view, + * canceling any ongoing touch events. + */ + private void cancelTargetTouch() { + final long eventTime = SystemClock.uptimeMillis(); + final MotionEvent cancel = MotionEvent.obtain( + eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mTarget.onTouchEvent(cancel); + cancel.recycle(); + } + + private class ScrollAnimationRunnable implements Runnable { + @Override + public void run() { + if (!mAnimating) { + return; + } + + if (mNeedsReset) { + mNeedsReset = false; + mScroller.start(); + } + + final ClampedScroller scroller = mScroller; + if (scroller.isFinished() || !shouldAnimate()) { + mAnimating = false; + return; + } + + if (mNeedsCancel) { + mNeedsCancel = false; + cancelTargetTouch(); + } + + scroller.computeScrollDelta(); + + final int deltaX = scroller.getDeltaX(); + final int deltaY = scroller.getDeltaY(); + scrollTargetBy(deltaX, deltaY); + + // Keep going until the scroller has permanently stopped. + mTarget.postOnAnimation(this); + } + } + + /** + * Scroller whose velocity follows the curve of an {@link Interpolator} and + * is clamped to the interpolated 0f value before starting and the + * interpolated 1f value after a specified duration. + */ + private static class ClampedScroller { + private int mRampUpDuration; + private int mRampDownDuration; + private float mTargetVelocityX; + private float mTargetVelocityY; + + private long mStartTime; + + private long mDeltaTime; + private int mDeltaX; + private int mDeltaY; + + private long mStopTime; + private float mStopValue; + private int mEffectiveRampDown; + + /** + * Creates a new ramp-up scroller that reaches full velocity after a + * specified duration. + */ + public ClampedScroller() { + mStartTime = Long.MIN_VALUE; + mStopTime = -1; + mDeltaTime = 0; + mDeltaX = 0; + mDeltaY = 0; + } + + public void setRampUpDuration(int durationMillis) { + mRampUpDuration = durationMillis; + } + + public void setRampDownDuration(int durationMillis) { + mRampDownDuration = durationMillis; + } + + /** + * Starts the scroller at the current animation time. + */ + public void start() { + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStopTime = -1; + mDeltaTime = mStartTime; + mStopValue = 0.5f; + mDeltaX = 0; + mDeltaY = 0; + } + + /** + * Stops the scroller at the current animation time. + */ + public void requestStop() { + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration); + mStopValue = getValueAt(currentTime); + mStopTime = currentTime; + } + + public boolean isFinished() { + return mStopTime > 0 + && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown; + } + + private float getValueAt(long currentTime) { + if (currentTime < mStartTime) { + return 0f; + } else if (mStopTime < 0 || currentTime < mStopTime) { + final long elapsedSinceStart = currentTime - mStartTime; + return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1); + } else { + final long elapsedSinceEnd = currentTime - mStopTime; + return (1 - mStopValue) + mStopValue + * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1); + } + } + + /** + * Interpolates the value along a parabolic curve corresponding to the equation + * <code>y = -4x * (x-1)</code>. + * + * @param value The value to interpolate, between 0 and 1. + * @return the interpolated value, between 0 and 1. + */ + private float interpolateValue(float value) { + return -4 * value * value + 4 * value; + } + + /** + * Computes the current scroll deltas. This usually only be called after + * starting the scroller with {@link #start()}. + * + * @see #getDeltaX() + * @see #getDeltaY() + */ + public void computeScrollDelta() { + if (mDeltaTime == 0) { + throw new RuntimeException("Cannot compute scroll delta before calling start()"); + } + + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + final float value = getValueAt(currentTime); + final float scale = interpolateValue(value); + final long elapsedSinceDelta = currentTime - mDeltaTime; + + mDeltaTime = currentTime; + mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX); + mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY); + } + + /** + * Sets the target velocity for this scroller. + * + * @param x The target X velocity in pixels per millisecond. + * @param y The target Y velocity in pixels per millisecond. + */ + public void setTargetVelocity(float x, float y) { + mTargetVelocityX = x; + mTargetVelocityY = y; + } + + public int getHorizontalDirection() { + return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX)); + } + + public int getVerticalDirection() { + return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY)); + } + + /** + * The distance traveled in the X-coordinate computed by the last call + * to {@link #computeScrollDelta()}. + */ + public int getDeltaX() { + return mDeltaX; + } + + /** + * The distance traveled in the Y-coordinate computed by the last call + * to {@link #computeScrollDelta()}. + */ + public int getDeltaY() { + return mDeltaY; + } + } + + /** + * An implementation of {@link AutoScrollHelper} that knows how to scroll + * through an {@link AbsListView}. + */ + public static class AbsListViewAutoScroller extends AutoScrollHelper { + private final AbsListView mTarget; + + public AbsListViewAutoScroller(AbsListView target) { + super(target); + + mTarget = target; + } + + @Override + public void scrollTargetBy(int deltaX, int deltaY) { + mTarget.scrollListBy(deltaY); + } + + @Override + public boolean canTargetScrollHorizontally(int direction) { + // List do not scroll horizontally. + return false; + } + + @Override + public boolean canTargetScrollVertically(int direction) { + final AbsListView target = mTarget; + final int itemCount = target.getCount(); + final int childCount = target.getChildCount(); + final int firstPosition = target.getFirstVisiblePosition(); + final int lastPosition = firstPosition + childCount; + + if (direction > 0) { + // Are we already showing the entire last item? + if (lastPosition >= itemCount) { + final View lastView = target.getChildAt(childCount - 1); + if (lastView.getBottom() <= target.getHeight()) { + return false; + } + } + } else if (direction < 0) { + // Are we already showing the entire first item? + if (firstPosition <= 0) { + final View firstView = target.getChildAt(0); + if (firstView.getTop() >= 0) { + return false; + } + } + } else { + // The behavior for direction 0 is undefined and we can return + // whatever we want. + return false; + } + + return true; + } + } +} |