diff options
68 files changed, 3753 insertions, 697 deletions
diff --git a/api/current.txt b/api/current.txt index 2e28d7b..cfaa1bb 100644 --- a/api/current.txt +++ b/api/current.txt @@ -339,8 +339,10 @@ package android { field public static final int checkedTextViewStyle = 16843720; // 0x10103c8 field public static final int childDivider = 16843025; // 0x1010111 field public static final int childIndicator = 16843020; // 0x101010c + field public static final int childIndicatorEnd = 16843732; // 0x10103d4 field public static final int childIndicatorLeft = 16843023; // 0x101010f field public static final int childIndicatorRight = 16843024; // 0x1010110 + field public static final int childIndicatorStart = 16843731; // 0x10103d3 field public static final int choiceMode = 16843051; // 0x101012b field public static final int clearTaskOnLaunch = 16842773; // 0x1010015 field public static final int clickable = 16842981; // 0x10100e5 @@ -574,8 +576,10 @@ package android { field public static final int indeterminateDuration = 16843069; // 0x101013d field public static final int indeterminateOnly = 16843066; // 0x101013a field public static final int indeterminateProgressStyle = 16843544; // 0x1010318 + field public static final int indicatorEnd = 16843730; // 0x10103d2 field public static final int indicatorLeft = 16843021; // 0x101010d field public static final int indicatorRight = 16843022; // 0x101010e + field public static final int indicatorStart = 16843729; // 0x10103d1 field public static final int inflatedId = 16842995; // 0x10100f3 field public static final int initOrder = 16842778; // 0x101001a field public static final int initialKeyguardLayout = 16843714; // 0x10103c2 @@ -4333,6 +4337,7 @@ package android.app.admin { method public boolean hasGrantedPolicy(android.content.ComponentName, int); method public boolean isActivePasswordSufficient(); method public boolean isAdminActive(android.content.ComponentName); + method public boolean isDeviceOwner(java.lang.String); method public void lockNow(); method public void removeActiveAdmin(android.content.ComponentName); method public boolean resetPassword(java.lang.String, int); @@ -16784,9 +16789,20 @@ package android.os { method public int getUserCount(); method public android.os.UserHandle getUserForSerialNumber(long); method public java.lang.String getUserName(); + method public android.os.Bundle getUserRestrictions(); + method public android.os.Bundle getUserRestrictions(android.os.UserHandle); method public boolean isUserAGoat(); + method public boolean isUserRestricted(); method public boolean isUserRunning(android.os.UserHandle); method public boolean isUserRunningOrStopping(android.os.UserHandle); + method public void setUserRestriction(java.lang.String, boolean); + method public void setUserRestrictions(android.os.Bundle); + method public void setUserRestrictions(android.os.Bundle, android.os.UserHandle); + field public static final java.lang.String DISALLOW_CONFIG_WIFI = "no_config_wifi"; + field public static final java.lang.String DISALLOW_INSTALL_APPS = "no_install_apps"; + field public static final java.lang.String DISALLOW_MODIFY_ACCOUNTS = "no_modify_accounts"; + field public static final java.lang.String DISALLOW_SHARE_LOCATION = "no_share_location"; + field public static final java.lang.String DISALLOW_UNINSTALL_APPS = "no_uninstall_apps"; } public abstract class Vibrator { @@ -28616,8 +28632,10 @@ package android.widget { method public void setChildDivider(android.graphics.drawable.Drawable); method public void setChildIndicator(android.graphics.drawable.Drawable); method public void setChildIndicatorBounds(int, int); + method public void setChildIndicatorBoundsRelative(int, int); method public void setGroupIndicator(android.graphics.drawable.Drawable); method public void setIndicatorBounds(int, int); + method public void setIndicatorBoundsRelative(int, int); method public void setOnChildClickListener(android.widget.ExpandableListView.OnChildClickListener); method public void setOnGroupClickListener(android.widget.ExpandableListView.OnGroupClickListener); method public void setOnGroupCollapseListener(android.widget.ExpandableListView.OnGroupCollapseListener); diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index 26dc60d..165c3db 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -461,39 +461,63 @@ public class DownloadManager { } /** - * Set the local destination for the downloaded file to a path within the application's - * external files directory (as returned by {@link Context#getExternalFilesDir(String)}. + * Set the local destination for the downloaded file to a path within + * the application's external files directory (as returned by + * {@link Context#getExternalFilesDir(String)}. * <p> - * The downloaded file is not scanned by MediaScanner. - * But it can be made scannable by calling {@link #allowScanningByMediaScanner()}. + * The downloaded file is not scanned by MediaScanner. But it can be + * made scannable by calling {@link #allowScanningByMediaScanner()}. * - * @param context the {@link Context} to use in determining the external files directory - * @param dirType the directory type to pass to {@link Context#getExternalFilesDir(String)} - * @param subPath the path within the external directory, including the destination filename + * @param context the {@link Context} to use in determining the external + * files directory + * @param dirType the directory type to pass to + * {@link Context#getExternalFilesDir(String)} + * @param subPath the path within the external directory, including the + * destination filename * @return this object + * @throws IllegalStateException If the external storage directory + * cannot be found or created. */ public Request setDestinationInExternalFilesDir(Context context, String dirType, String subPath) { - setDestinationFromBase(context.getExternalFilesDir(dirType), subPath); + final File file = context.getExternalFilesDir(dirType); + if (file == null) { + throw new IllegalStateException("Failed to get external storage files directory"); + } else if (file.exists()) { + if (!file.isDirectory()) { + throw new IllegalStateException(file.getAbsolutePath() + + " already exists and is not a directory"); + } + } else { + if (!file.mkdirs()) { + throw new IllegalStateException("Unable to create directory: "+ + file.getAbsolutePath()); + } + } + setDestinationFromBase(file, subPath); return this; } /** - * Set the local destination for the downloaded file to a path within the public external - * storage directory (as returned by - * {@link Environment#getExternalStoragePublicDirectory(String)}. - *<p> - * The downloaded file is not scanned by MediaScanner. - * But it can be made scannable by calling {@link #allowScanningByMediaScanner()}. + * Set the local destination for the downloaded file to a path within + * the public external storage directory (as returned by + * {@link Environment#getExternalStoragePublicDirectory(String)}). + * <p> + * The downloaded file is not scanned by MediaScanner. But it can be + * made scannable by calling {@link #allowScanningByMediaScanner()}. * - * @param dirType the directory type to pass to - * {@link Environment#getExternalStoragePublicDirectory(String)} - * @param subPath the path within the external directory, including the destination filename + * @param dirType the directory type to pass to {@link Environment#getExternalStoragePublicDirectory(String)} + * @param subPath the path within the external directory, including the + * destination filename * @return this object + * @throws IllegalStateException If the external storage directory + * cannot be found or created. */ public Request setDestinationInExternalPublicDir(String dirType, String subPath) { File file = Environment.getExternalStoragePublicDirectory(dirType); - if (file.exists()) { + if (file == null) { + throw new IllegalStateException("Failed to get external storage public directory"); + } else if (file.exists()) { if (!file.isDirectory()) { throw new IllegalStateException(file.getAbsolutePath() + " already exists and is not a directory"); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 4c0eba0..8284b2c 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -1513,4 +1513,57 @@ public class DevicePolicyManager { } } } + + /** + * @hide + * Sets the given package as the device owner. The package must already be installed and there + * shouldn't be an existing device owner registered, for this call to succeed. Also, this + * method must be called before the device is provisioned. + * @param packageName the package name of the application to be registered as the device owner. + * @return whether the package was successfully registered as the device owner. + * @throws IllegalArgumentException if the package name is null or invalid + * @throws IllegalStateException if a device owner is already registered or the device has + * already been provisioned. + */ + public boolean setDeviceOwner(String packageName) throws IllegalArgumentException, + IllegalStateException { + if (mService != null) { + try { + return mService.setDeviceOwner(packageName); + } catch (RemoteException re) { + Log.w(TAG, "Failed to set device owner"); + } + } + return false; + } + + /** + * Used to determine if a particular package has been registered as a Device Owner admin. + * Device Owner admins cannot be deactivated by the user unless the Device Owner itself allows + * it. And Device Owner packages cannot be uninstalled, once registered. + * @param packageName the package name to check against the registered device owner. + * @return whether or not the package is registered as the Device Owner. + */ + public boolean isDeviceOwner(String packageName) { + if (mService != null) { + try { + return mService.isDeviceOwner(packageName); + } catch (RemoteException re) { + Log.w(TAG, "Failed to check device owner"); + } + } + return false; + } + + /** @hide */ + public String getDeviceOwner() { + if (mService != null) { + try { + return mService.getDeviceOwner(); + } catch (RemoteException re) { + Log.w(TAG, "Failed to get device owner"); + } + } + return null; + } } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index e061ab3..b2a65bf 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -97,4 +97,8 @@ interface IDevicePolicyManager { int numbers, int symbols, int nonletter, int userHandle); void reportFailedPasswordAttempt(int userHandle); void reportSuccessfulPasswordAttempt(int userHandle); + + boolean setDeviceOwner(String packageName); + boolean isDeviceOwner(String packageName); + String getDeviceOwner(); } diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index 3425765..37fddcb 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -477,21 +477,31 @@ public abstract class BackupAgent extends ContextWrapper { } } else { // Not a supported location - Log.i(TAG, "Data restored from non-app domain " + domain + ", ignoring"); + Log.i(TAG, "Unrecognized domain " + domain); } // Now that we've figured out where the data goes, send it on its way if (basePath != null) { + // Canonicalize the nominal path and verify that it lies within the stated domain File outFile = new File(basePath, path); - if (DEBUG) Log.i(TAG, "[" + domain + " : " + path + "] mapped to " + outFile.getPath()); - onRestoreFile(data, size, outFile, type, mode, mtime); - } else { - // Not a supported output location? We need to consume the data - // anyway, so just use the default "copy the data out" implementation - // with a null destination. - if (DEBUG) Log.i(TAG, "[ skipping data from unsupported domain " + domain + "]"); - FullBackup.restoreFile(data, size, type, mode, mtime, null); + String outPath = outFile.getCanonicalPath(); + if (outPath.startsWith(basePath + File.separatorChar)) { + if (DEBUG) Log.i(TAG, "[" + domain + " : " + path + "] mapped to " + outPath); + onRestoreFile(data, size, outFile, type, mode, mtime); + return; + } else { + // Attempt to restore to a path outside the file's nominal domain. + if (DEBUG) { + Log.e(TAG, "Cross-domain restore attempt: " + outPath); + } + } } + + // Not a supported output location, or bad path: we need to consume the data + // anyway, so just use the default "copy the data out" implementation + // with a null destination. + if (DEBUG) Log.i(TAG, "[ skipping file " + path + "]"); + FullBackup.restoreFile(data, size, type, mode, mtime, null); } // ----- Core implementation ----- diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 32cc7fd..02401dc 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -398,6 +398,15 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public String[] resourceDirs; /** + * String retrieved from the seinfo tag found in selinux policy. This value + * is useful in setting an SELinux security context on the process as well + * as its data directory. + * + * {@hide} + */ + public String seinfo; + + /** * Paths to all shared libraries this application is linked against. This * field is only set if the {@link PackageManager#GET_SHARED_LIBRARY_FILES * PackageManager.GET_SHARED_LIBRARY_FILES} flag was used when retrieving @@ -477,6 +486,9 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { if (resourceDirs != null) { pw.println(prefix + "resourceDirs=" + resourceDirs); } + if (seinfo != null) { + pw.println(prefix + "seinfo=" + seinfo); + } pw.println(prefix + "dataDir=" + dataDir); if (sharedLibraryFiles != null) { pw.println(prefix + "sharedLibraryFiles=" + sharedLibraryFiles); @@ -544,6 +556,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { publicSourceDir = orig.publicSourceDir; nativeLibraryDir = orig.nativeLibraryDir; resourceDirs = orig.resourceDirs; + seinfo = orig.seinfo; sharedLibraryFiles = orig.sharedLibraryFiles; dataDir = orig.dataDir; uid = orig.uid; @@ -583,6 +596,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeString(publicSourceDir); dest.writeString(nativeLibraryDir); dest.writeStringArray(resourceDirs); + dest.writeString(seinfo); dest.writeStringArray(sharedLibraryFiles); dest.writeString(dataDir); dest.writeInt(uid); @@ -621,6 +635,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { publicSourceDir = source.readString(); nativeLibraryDir = source.readString(); resourceDirs = source.readStringArray(); + seinfo = source.readString(); sharedLibraryFiles = source.readStringArray(); dataDir = source.readString(); uid = source.readInt(); diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java index ce1276f..786439e 100644 --- a/core/java/android/net/TrafficStats.java +++ b/core/java/android/net/TrafficStats.java @@ -119,6 +119,8 @@ public class TrafficStats { * Tags between {@code 0xFFFFFF00} and {@code 0xFFFFFFFF} are reserved and * used internally by system services like {@link DownloadManager} when * performing traffic on behalf of an application. + * + * @see #clearThreadStatsTag() */ public static void setThreadStatsTag(int tag) { NetworkManagementSocketTagger.setThreadSocketStatsTag(tag); @@ -128,11 +130,19 @@ public class TrafficStats { * Get the active tag used when accounting {@link Socket} traffic originating * from the current thread. Only one active tag per thread is supported. * {@link #tagSocket(Socket)}. + * + * @see #setThreadStatsTag(int) */ public static int getThreadStatsTag() { return NetworkManagementSocketTagger.getThreadSocketStatsTag(); } + /** + * Clear any active tag set to account {@link Socket} traffic originating + * from the current thread. + * + * @see #setThreadStatsTag(int) + */ public static void clearThreadStatsTag() { NetworkManagementSocketTagger.setThreadSocketStatsTag(-1); } @@ -148,7 +158,7 @@ public class TrafficStats { * To take effect, caller must hold * {@link android.Manifest.permission#UPDATE_DEVICE_STATS} permission. * - * {@hide} + * @hide */ public static void setThreadStatsUid(int uid) { NetworkManagementSocketTagger.setThreadSocketStatsUid(uid); @@ -260,10 +270,13 @@ public class TrafficStats { } /** - * Get the total number of packets transmitted through the mobile interface. - * - * @return number of packets. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * Return number of packets transmitted across mobile networks since device + * boot. Counts packets across all mobile network interfaces, and always + * increases monotonically since device boot. Statistics are measured at the + * network layer, so they include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getMobileTxPackets() { long total = 0; @@ -274,10 +287,13 @@ public class TrafficStats { } /** - * Get the total number of packets received through the mobile interface. - * - * @return number of packets. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * Return number of packets received across mobile networks since device + * boot. Counts packets across all mobile network interfaces, and always + * increases monotonically since device boot. Statistics are measured at the + * network layer, so they include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getMobileRxPackets() { long total = 0; @@ -288,10 +304,13 @@ public class TrafficStats { } /** - * Get the total number of bytes transmitted through the mobile interface. - * - * @return number of bytes. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * Return number of bytes transmitted across mobile networks since device + * boot. Counts packets across all mobile network interfaces, and always + * increases monotonically since device boot. Statistics are measured at the + * network layer, so they include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getMobileTxBytes() { long total = 0; @@ -302,10 +321,13 @@ public class TrafficStats { } /** - * Get the total number of bytes received through the mobile interface. - * - * @return number of bytes. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * Return number of bytes received across mobile networks since device boot. + * Counts packets across all mobile network interfaces, and always increases + * monotonically since device boot. Statistics are measured at the network + * layer, so they include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getMobileRxBytes() { long total = 0; @@ -339,85 +361,73 @@ public class TrafficStats { return total; } - /** - * Get the total number of packets transmitted through the specified interface. - * - * @return number of packets. If the statistics are not supported by this interface, - * {@link #UNSUPPORTED} will be returned. - * @hide - */ + /** {@hide} */ public static long getTxPackets(String iface) { return nativeGetIfaceStat(iface, TYPE_TX_PACKETS); } - /** - * Get the total number of packets received through the specified interface. - * - * @return number of packets. If the statistics are not supported by this interface, - * {@link #UNSUPPORTED} will be returned. - * @hide - */ + /** {@hide} */ public static long getRxPackets(String iface) { return nativeGetIfaceStat(iface, TYPE_RX_PACKETS); } - /** - * Get the total number of bytes transmitted through the specified interface. - * - * @return number of bytes. If the statistics are not supported by this interface, - * {@link #UNSUPPORTED} will be returned. - * @hide - */ + /** {@hide} */ public static long getTxBytes(String iface) { return nativeGetIfaceStat(iface, TYPE_TX_BYTES); } - /** - * Get the total number of bytes received through the specified interface. - * - * @return number of bytes. If the statistics are not supported by this interface, - * {@link #UNSUPPORTED} will be returned. - * @hide - */ + /** {@hide} */ public static long getRxBytes(String iface) { return nativeGetIfaceStat(iface, TYPE_RX_BYTES); } /** - * Get the total number of packets sent through all network interfaces. - * - * @return the number of packets. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * Return number of packets transmitted since device boot. Counts packets + * across all network interfaces, and always increases monotonically since + * device boot. Statistics are measured at the network layer, so they + * include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getTotalTxPackets() { return nativeGetTotalStat(TYPE_TX_PACKETS); } /** - * Get the total number of packets received through all network interfaces. - * - * @return number of packets. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * Return number of packets received since device boot. Counts packets + * across all network interfaces, and always increases monotonically since + * device boot. Statistics are measured at the network layer, so they + * include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getTotalRxPackets() { return nativeGetTotalStat(TYPE_RX_PACKETS); } /** - * Get the total number of bytes sent through all network interfaces. - * - * @return number of bytes. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * Return number of bytes transmitted since device boot. Counts packets + * across all network interfaces, and always increases monotonically since + * device boot. Statistics are measured at the network layer, so they + * include both TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getTotalTxBytes() { return nativeGetTotalStat(TYPE_TX_BYTES); } /** - * Get the total number of bytes received through all network interfaces. - * - * @return number of bytes. If the statistics are not supported by this device, - * {@link #UNSUPPORTED} will be returned. + * Return number of bytes received since device boot. Counts packets across + * all network interfaces, and always increases monotonically since device + * boot. Statistics are measured at the network layer, so they include both + * TCP and UDP usage. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getTotalRxBytes() { return nativeGetTotalStat(TYPE_RX_BYTES); @@ -580,6 +590,7 @@ public class TrafficStats { * special permission. */ private static NetworkStats getDataLayerSnapshotForUid(Context context) { + // TODO: take snapshot locally, since proc file is now visible final int uid = android.os.Process.myUid(); try { return getStatsService().getDataLayerSnapshotForUid(uid); diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index 4c2d7a6..2e8092a 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -34,6 +34,7 @@ interface IUserManager { Bitmap getUserIcon(int userHandle); List<UserInfo> getUsers(boolean excludeDying); UserInfo getUserInfo(int userHandle); + boolean isRestricted(); void setGuestEnabled(boolean enable); boolean isGuestEnabled(); void wipeUser(int userHandle); diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java index c9adf45..729c64b 100644 --- a/core/java/android/os/SystemClock.java +++ b/core/java/android/os/SystemClock.java @@ -138,8 +138,6 @@ public final class SystemClock { /** * Returns milliseconds since boot, not counting time spent in deep sleep. - * <b>Note:</b> This value may get reset occasionally (before it would - * otherwise wrap around). * * @return milliseconds of non-sleep uptime since boot. */ diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 7c05528..622308f 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -37,48 +37,56 @@ public class UserManager { private final Context mContext; /** - * @hide - * Key for user restrictions. Specifies if a user is allowed to add or remove accounts. + * Key for user restrictions. Specifies if a user is disallowed from adding and removing + * accounts. + * The default value is <code>false</code>. + * <p/> * Type: Boolean * @see #setUserRestrictions(Bundle) * @see #getUserRestrictions() */ - public static final String ALLOW_MODIFY_ACCOUNTS = "modify_accounts"; + public static final String DISALLOW_MODIFY_ACCOUNTS = "no_modify_accounts"; /** - * @hide - * Key for user restrictions. Specifies if a user is allowed to change Wi-Fi access points. + * Key for user restrictions. Specifies if a user is disallowed from changing Wi-Fi + * access points. + * The default value is <code>false</code>. + * <p/> * Type: Boolean * @see #setUserRestrictions(Bundle) * @see #getUserRestrictions() */ - public static final String ALLOW_CONFIG_WIFI = "config_wifi"; + public static final String DISALLOW_CONFIG_WIFI = "no_config_wifi"; /** - * @hide - * Key for user restrictions. Specifies if a user is allowed to install applications. + * Key for user restrictions. Specifies if a user is disallowed from installing applications. + * The default value is <code>false</code>. + * <p/> * Type: Boolean * @see #setUserRestrictions(Bundle) * @see #getUserRestrictions() */ - public static final String ALLOW_INSTALL_APPS = "install_apps"; + public static final String DISALLOW_INSTALL_APPS = "no_install_apps"; /** - * @hide - * Key for user restrictions. Specifies if a user is allowed to uninstall applications. + * Key for user restrictions. Specifies if a user is disallowed from uninstalling applications. + * The default value is <code>false</code>. + * <p/> * Type: Boolean * @see #setUserRestrictions(Bundle) * @see #getUserRestrictions() */ - public static final String ALLOW_UNINSTALL_APPS = "uninstall_apps"; + public static final String DISALLOW_UNINSTALL_APPS = "no_uninstall_apps"; - /** @hide * - * Key for user restrictions. Specifies if a user is allowed to toggle location sharing. + /** + * Key for user restrictions. Specifies if a user is disallowed from toggling location sharing. + * The default value is <code>false</code>. + * <p/> * Type: Boolean * @see #setUserRestrictions(Bundle) * @see #getUserRestrictions() */ - public static final String ALLOW_CONFIG_LOCATION_ACCESS = "config_location_access"; + public static final String DISALLOW_SHARE_LOCATION = "no_share_location"; /** @hide */ public UserManager(Context context, IUserManager service) { @@ -129,11 +137,14 @@ public class UserManager { } /** - * @hide + * Used to check if the user making this call is a restricted user. Restricted users may have + * application restrictions imposed on them. All apps should default to the most restrictive + * version, unless they have specific restrictions available through a call to + * {@link Context#getApplicationRestrictions()}. */ public boolean isUserRestricted() { try { - return mService.getUserInfo(getUserHandle()).isRestricted(); + return mService.isRestricted(); } catch (RemoteException re) { Log.w(TAG, "Could not check if user restricted ", re); return false; @@ -189,12 +200,19 @@ public class UserManager { } } - /** @hide */ + /** + * Returns the user-wide restrictions imposed on this user. + * @return a Bundle containing all the restrictions. + */ public Bundle getUserRestrictions() { return getUserRestrictions(Process.myUserHandle()); } - /** @hide */ + /** + * Returns the user-wide restrictions imposed on the user specified by <code>userHandle</code>. + * @param userHandle the UserHandle of the user for whom to retrieve the restrictions. + * @return a Bundle containing all the restrictions. + */ public Bundle getUserRestrictions(UserHandle userHandle) { try { return mService.getUserRestrictions(userHandle.getIdentifier()); @@ -204,12 +222,21 @@ public class UserManager { } } - /** @hide */ + /** + * Sets all the user-wide restrictions for this user. + * Requires the MANAGE_USERS permission. + * @param restrictions the Bundle containing all the restrictions. + */ public void setUserRestrictions(Bundle restrictions) { setUserRestrictions(restrictions, Process.myUserHandle()); } - /** @hide */ + /** + * Sets all the user-wide restrictions for the specified user. + * Requires the MANAGE_USERS permission. + * @param restrictions the Bundle containing all the restrictions. + * @param userHandle the UserHandle of the user for whom to set the restrictions. + */ public void setUserRestrictions(Bundle restrictions, UserHandle userHandle) { try { mService.setUserRestrictions(restrictions, userHandle.getIdentifier()); @@ -218,7 +245,26 @@ public class UserManager { } } - /** @hide */ + /** + * Sets the value of a specific restriction. + * Requires the MANAGE_USERS permission. + * @param key the key of the restriction + * @param value the value for the restriction + */ + public void setUserRestriction(String key, boolean value) { + Bundle bundle = getUserRestrictions(); + bundle.putBoolean(key, value); + setUserRestrictions(bundle); + } + + /** + * @hide + * Sets the value of a specific restriction on a specific user. + * Requires the {@link android.Manifest.permission#MANAGE_USERS} permission. + * @param key the key of the restriction + * @param value the value for the restriction + * @param userHandle the user whose restriction is to be changed. + */ public void setUserRestriction(String key, boolean value, UserHandle userHandle) { Bundle bundle = getUserRestrictions(userHandle); bundle.putBoolean(key, value); @@ -467,7 +513,7 @@ public class UserManager { * @hide */ public boolean isLocationSharingToggleAllowed() { - return getUserRestrictions().getBoolean(ALLOW_CONFIG_LOCATION_ACCESS); + return !getUserRestrictions().getBoolean(DISALLOW_SHARE_LOCATION, false); } /** diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java index 7e9d811..5a88cf6 100644 --- a/core/java/android/text/format/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -39,7 +39,6 @@ public class DateUtils { private static final Object sLock = new Object(); private static Configuration sLastConfig; - private static java.text.DateFormat sStatusTimeFormat; private static String sElapsedFormatMMSS; private static String sElapsedFormatHMMSS; @@ -95,14 +94,14 @@ public class DateUtils // translated. /** * This is not actually the preferred 24-hour date format in all locales. - * @deprecated use {@link java.text.SimpleDateFormat} instead. + * @deprecated Use {@link java.text.SimpleDateFormat} instead. */ @Deprecated public static final String HOUR_MINUTE_24 = "%H:%M"; public static final String MONTH_FORMAT = "%B"; /** * This is not actually a useful month name in all locales. - * @deprecated use {@link java.text.SimpleDateFormat} instead. + * @deprecated Use {@link java.text.SimpleDateFormat} instead. */ @Deprecated public static final String ABBREV_MONTH_FORMAT = "%b"; @@ -118,7 +117,7 @@ public class DateUtils // The index is constructed from a bit-wise OR of the boolean values: // {showTime, showYear, showWeekDay}. For example, if showYear and // showWeekDay are both true, then the index would be 3. - /** @deprecated do not use. */ + /** @deprecated Do not use. */ public static final int sameYearTable[] = { com.android.internal.R.string.same_year_md1_md2, com.android.internal.R.string.same_year_wday1_md1_wday2_md2, @@ -145,7 +144,7 @@ public class DateUtils // The index is constructed from a bit-wise OR of the boolean values: // {showTime, showYear, showWeekDay}. For example, if showYear and // showWeekDay are both true, then the index would be 3. - /** @deprecated do not use. */ + /** @deprecated Do not use. */ public static final int sameMonthTable[] = { com.android.internal.R.string.same_month_md1_md2, com.android.internal.R.string.same_month_wday1_md1_wday2_md2, @@ -172,7 +171,7 @@ public class DateUtils * * @more <p> * e.g. "Sunday" or "January" - * @deprecated use {@link java.text.SimpleDateFormat} instead. + * @deprecated Use {@link java.text.SimpleDateFormat} instead. */ @Deprecated public static final int LENGTH_LONG = 10; @@ -183,7 +182,7 @@ public class DateUtils * * @more <p> * e.g. "Sun" or "Jan" - * @deprecated use {@link java.text.SimpleDateFormat} instead. + * @deprecated Use {@link java.text.SimpleDateFormat} instead. */ @Deprecated public static final int LENGTH_MEDIUM = 20; @@ -195,7 +194,7 @@ public class DateUtils * <p>e.g. "Su" or "Jan" * <p>In most languages, the results returned for LENGTH_SHORT will be the same as * the results returned for {@link #LENGTH_MEDIUM}. - * @deprecated use {@link java.text.SimpleDateFormat} instead. + * @deprecated Use {@link java.text.SimpleDateFormat} instead. */ @Deprecated public static final int LENGTH_SHORT = 30; @@ -204,7 +203,7 @@ public class DateUtils * Request an even shorter abbreviated version of the name. * Do not use this. Currently this will always return the same result * as {@link #LENGTH_SHORT}. - * @deprecated use {@link java.text.SimpleDateFormat} instead. + * @deprecated Use {@link java.text.SimpleDateFormat} instead. */ @Deprecated public static final int LENGTH_SHORTER = 40; @@ -216,7 +215,7 @@ public class DateUtils * <p>e.g. "S", "T", "T" or "J" * <p>In some languages, the results returned for LENGTH_SHORTEST will be the same as * the results returned for {@link #LENGTH_SHORT}. - * @deprecated use {@link java.text.SimpleDateFormat} instead. + * @deprecated Use {@link java.text.SimpleDateFormat} instead. */ @Deprecated public static final int LENGTH_SHORTEST = 50; @@ -232,7 +231,7 @@ public class DateUtils * Undefined lengths will return {@link #LENGTH_MEDIUM} * but may return something different in the future. * @throws IndexOutOfBoundsException if the dayOfWeek is out of bounds. - * @deprecated use {@link java.text.SimpleDateFormat} instead. + * @deprecated Use {@link java.text.SimpleDateFormat} instead. */ @Deprecated public static String getDayOfWeekString(int dayOfWeek, int abbrev) { @@ -254,7 +253,7 @@ public class DateUtils * @param ampm Either {@link Calendar#AM Calendar.AM} or {@link Calendar#PM Calendar.PM}. * @throws IndexOutOfBoundsException if the ampm is out of bounds. * @return Localized version of "AM" or "PM". - * @deprecated use {@link java.text.SimpleDateFormat} instead. + * @deprecated Use {@link java.text.SimpleDateFormat} instead. */ @Deprecated public static String getAMPMString(int ampm) { @@ -270,7 +269,7 @@ public class DateUtils * Undefined lengths will return {@link #LENGTH_MEDIUM} * but may return something different in the future. * @return Localized month of the year. - * @deprecated use {@link java.text.SimpleDateFormat} instead. + * @deprecated Use {@link java.text.SimpleDateFormat} instead. */ @Deprecated public static String getMonthString(int month, int abbrev) { @@ -514,7 +513,6 @@ public class DateUtils Configuration cfg = r.getConfiguration(); if (sLastConfig == null || !sLastConfig.equals(cfg)) { sLastConfig = cfg; - sStatusTimeFormat = java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT); sElapsedFormatMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_mm_ss); sElapsedFormatHMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_h_mm_ss); } @@ -651,99 +649,6 @@ public class DateUtils } /** - * Return a string containing the date and time in RFC2445 format. - * Ensures that the time is written in UTC. The Calendar class doesn't - * really help out with this, so this is slower than it ought to be. - * - * @param cal the date and time to write - * @hide - * @deprecated use {@link android.text.format.Time} - */ - public static String writeDateTime(Calendar cal) - { - TimeZone tz = TimeZone.getTimeZone("GMT"); - GregorianCalendar c = new GregorianCalendar(tz); - c.setTimeInMillis(cal.getTimeInMillis()); - return writeDateTime(c, true); - } - - /** - * Return a string containing the date and time in RFC2445 format. - * - * @param cal the date and time to write - * @param zulu If the calendar is in UTC, pass true, and a Z will - * be written at the end as per RFC2445. Otherwise, the time is - * considered in localtime. - * @hide - * @deprecated use {@link android.text.format.Time} - */ - public static String writeDateTime(Calendar cal, boolean zulu) - { - StringBuilder sb = new StringBuilder(); - sb.ensureCapacity(16); - if (zulu) { - sb.setLength(16); - sb.setCharAt(15, 'Z'); - } else { - sb.setLength(15); - } - return writeDateTime(cal, sb); - } - - /** - * Return a string containing the date and time in RFC2445 format. - * - * @param cal the date and time to write - * @param sb a StringBuilder to use. It is assumed that setLength - * has already been called on sb to the appropriate length - * which is sb.setLength(zulu ? 16 : 15) - * @hide - * @deprecated use {@link android.text.format.Time} - */ - public static String writeDateTime(Calendar cal, StringBuilder sb) - { - int n; - - n = cal.get(Calendar.YEAR); - sb.setCharAt(3, (char)('0'+n%10)); - n /= 10; - sb.setCharAt(2, (char)('0'+n%10)); - n /= 10; - sb.setCharAt(1, (char)('0'+n%10)); - n /= 10; - sb.setCharAt(0, (char)('0'+n%10)); - - n = cal.get(Calendar.MONTH) + 1; - sb.setCharAt(5, (char)('0'+n%10)); - n /= 10; - sb.setCharAt(4, (char)('0'+n%10)); - - n = cal.get(Calendar.DAY_OF_MONTH); - sb.setCharAt(7, (char)('0'+n%10)); - n /= 10; - sb.setCharAt(6, (char)('0'+n%10)); - - sb.setCharAt(8, 'T'); - - n = cal.get(Calendar.HOUR_OF_DAY); - sb.setCharAt(10, (char)('0'+n%10)); - n /= 10; - sb.setCharAt(9, (char)('0'+n%10)); - - n = cal.get(Calendar.MINUTE); - sb.setCharAt(12, (char)('0'+n%10)); - n /= 10; - sb.setCharAt(11, (char)('0'+n%10)); - - n = cal.get(Calendar.SECOND); - sb.setCharAt(14, (char)('0'+n%10)); - n /= 10; - sb.setCharAt(13, (char)('0'+n%10)); - - return sb.toString(); - } - - /** * Formats a date or a time range according to the local conventions. * <p> * Note that this is a convenience method. Using it involves creating an diff --git a/core/java/android/text/format/Formatter.java b/core/java/android/text/format/Formatter.java index 121c6f2..9c98b98 100644 --- a/core/java/android/text/format/Formatter.java +++ b/core/java/android/text/format/Formatter.java @@ -95,16 +95,12 @@ public final class Formatter { } /** - * Returns a string in the canonical IP format ###.###.###.### from a packed integer containing - * the IP address. The IP address is expected to be in little-endian format (LSB first). That - * is, 0x01020304 will return "4.3.2.1". + * Returns a string in the canonical IPv4 format ###.###.###.### from a packed integer + * containing the IP address. The IPv4 address is expected to be in little-endian + * format (LSB first). That is, 0x01020304 will return "4.3.2.1". * - * @param ipv4Address the IP address as a packed integer with LSB first. - * @return string with canonical IP address format. - * - * @deprecated this method doesn't support IPv6 addresses. Prefer {@link - * java.net.InetAddress#getHostAddress()}, which supports both IPv4 and - * IPv6 addresses. + * @deprecated Use {@link java.net.InetAddress#getHostAddress()}, which supports both IPv4 and + * IPv6 addresses. This method does not support IPv6 addresses. */ @Deprecated public static String formatIpAddress(int ipv4Address) { diff --git a/core/java/android/view/Overlay.java b/core/java/android/view/Overlay.java index 210bc31..6630752 100644 --- a/core/java/android/view/Overlay.java +++ b/core/java/android/view/Overlay.java @@ -18,9 +18,10 @@ package android.view; import android.graphics.drawable.Drawable; /** - * An overlay is an extra layer that sits on top of a View (the "host view") which is drawn after - * all other content in that view (including children, if the view is a ViewGroup). Interaction - * with the overlay layer is done in terms of adding/removing views and drawables. + * An overlay is an extra layer that sits on top of a View (the "host view") + * which is drawn after all other content in that view (including children, + * if the view is a ViewGroup). Interaction with the overlay layer is done in + * terms of adding/removing views and drawables. * * @see android.view.View#getOverlay() */ @@ -47,10 +48,16 @@ public interface Overlay { void remove(Drawable drawable); /** - * Adds a View to the overlay. The bounds of the added view should be relative to - * the host view. Any view added to the overlay should be removed when it is no longer - * needed or no longer visible. The view must not be parented elsewhere when it is added - * to the overlay. + * Adds a View to the overlay. The bounds of the added view should be + * relative to the host view. Any view added to the overlay should be + * removed when it is no longer needed or no longer visible. + * + * <p>If the view has a parent, the view will be removed from that parent + * before being added to the overlay. Also, the view will be repositioned + * such that it is in the same relative location inside the activity. For + * example, if the view's current parent lies 100 pixels to the right + * and 200 pixels down from the origin of the overlay's + * host view, then the view will be offset by (100, 200).</p> * * @param view The View to be added to the overlay. The added view will be * drawn when the overlay is drawn. diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 34f5a2b..a5b3c8f 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -12098,7 +12098,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, //System.out.println("Attached! " + this); mAttachInfo = info; if (mOverlay != null) { - mOverlay.mAttachInfo = info; + mOverlay.dispatchAttachedToWindow(info, visibility); } mWindowAttachCount++; // We will need to evaluate the drawable state at least once. @@ -12169,7 +12169,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mAttachInfo = null; if (mOverlay != null) { - mOverlay.mAttachInfo = null; + mOverlay.dispatchDetachedFromWindow(); } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index d63f7bc..688e17e 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -1868,13 +1868,37 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // have become out of sync. removePointersFromTouchTargets(idBitsToAssign); + final float x = ev.getX(actionIndex); + final float y = ev.getY(actionIndex); + + if (mOverlay != null) { + ViewOverlay overlay = (ViewOverlay) mOverlay; + // Check to see whether the overlay can handle the event + final View child = mOverlay; + if (canViewReceivePointerEvents(child) && + isTransformedTouchPointInView(x, y, child, null)) { + newTouchTarget = getTouchTarget(child); + if (newTouchTarget != null) { + newTouchTarget.pointerIdBits |= idBitsToAssign; + } else { + resetCancelNextUpFlag(child); + if (dispatchTransformedTouchEvent(ev, false, child, + idBitsToAssign)) { + mLastTouchDownTime = ev.getDownTime(); + mLastTouchDownX = ev.getX(); + mLastTouchDownY = ev.getY(); + newTouchTarget = addTouchTarget(child, idBitsToAssign); + alreadyDispatchedToNewTouchTarget = true; + } + } + } + } + final int childrenCount = mChildrenCount; - if (childrenCount != 0 || mOverlay != null) { + if (newTouchTarget == null && childrenCount != 0) { // Find a child that can receive the event. // Scan children from front to back. final View[] children = mChildren; - final float x = ev.getX(actionIndex); - final float y = ev.getY(actionIndex); final boolean customOrder = isChildrenDrawingOrderEnabled(); for (int i = childrenCount - 1; i >= 0; i--) { @@ -1906,27 +1930,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager break; } } - if (mOverlay != null && newTouchTarget == null) { - // Check to see whether the overlay can handle the event - final View child = mOverlay; - if (canViewReceivePointerEvents(child) && - isTransformedTouchPointInView(x, y, child, null)) { - newTouchTarget = getTouchTarget(child); - if (newTouchTarget != null) { - newTouchTarget.pointerIdBits |= idBitsToAssign; - } else { - resetCancelNextUpFlag(child); - if (dispatchTransformedTouchEvent(ev, false, child, - idBitsToAssign)) { - mLastTouchDownTime = ev.getDownTime(); - mLastTouchDownX = ev.getX(); - mLastTouchDownY = ev.getY(); - newTouchTarget = addTouchTarget(child, idBitsToAssign); - alreadyDispatchedToNewTouchTarget = true; - } - } - } - } } if (newTouchTarget == null && mFirstTouchTarget != null) { @@ -1957,7 +1960,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) - || intercepted; + || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; diff --git a/core/java/android/view/ViewOverlay.java b/core/java/android/view/ViewOverlay.java index 939377d..8b18d53 100644 --- a/core/java/android/view/ViewOverlay.java +++ b/core/java/android/view/ViewOverlay.java @@ -23,17 +23,21 @@ import android.graphics.drawable.Drawable; import java.util.ArrayList; /** - * ViewOverlay is a container that View uses to host all objects (views and drawables) that - * are added to its "overlay", gotten through {@link View#getOverlay()}. Views and drawables are - * added to the overlay via the add/remove methods in this class. These views and drawables are - * drawn whenever the view itself is drawn; first the view draws its own content (and children, - * if it is a ViewGroup), then it draws its overlay (if it has one). + * ViewOverlay is a container that View uses to host all objects (views and + * drawables) that are added to its "overlay", gotten through + * {@link View#getOverlay()}. Views and drawables are added to the overlay + * via the add/remove methods in this class. These views and drawables are + * drawn whenever the view itself is drawn; first the view draws its own + * content (and children, if it is a ViewGroup), then it draws its overlay + * (if it has one). * - * Besides managing and drawing the list of drawables, this class serves two purposes: + * Besides managing and drawing the list of drawables, this class serves + * two purposes: * (1) it noops layout calls because children are absolutely positioned and - * (2) it forwards all invalidation calls to its host view. The invalidation redirect is - * necessary because the overlay is not a child of the host view and invalidation cannot - * therefore follow the normal path up through the parent hierarchy. + * (2) it forwards all invalidation calls to its host view. The invalidation + * redirect is necessary because the overlay is not a child of the host view + * and invalidation cannot therefore follow the normal path up through the + * parent hierarchy. * * @hide */ @@ -85,6 +89,22 @@ class ViewOverlay extends ViewGroup implements Overlay { @Override public void add(View child) { + int deltaX = 0; + int deltaY = 0; + if (child.getParent() instanceof ViewGroup) { + ViewGroup parent = (ViewGroup) child.getParent(); + if (parent != mHostView) { + // Moving to different container; figure out how to position child such that + // it is in the same location on the screen + int[] parentLocation = new int[2]; + int[] hostViewLocation = new int[2]; + parent.getLocationOnScreen(parentLocation); + mHostView.getLocationOnScreen(hostViewLocation); + child.offsetLeftAndRight(parentLocation[0] - hostViewLocation[0]); + child.offsetTopAndBottom(parentLocation[1] - hostViewLocation[1]); + } + parent.removeView(child); + } super.addView(child); } @@ -133,7 +153,6 @@ class ViewOverlay extends ViewGroup implements Overlay { public void invalidate(Rect dirty) { super.invalidate(dirty); if (mHostView != null) { - dirty.offset(getLeft(), getTop()); mHostView.invalidate(dirty); } } @@ -203,7 +222,15 @@ class ViewOverlay extends ViewGroup implements Overlay { @Override public ViewParent invalidateChildInParent(int[] location, Rect dirty) { if (mHostView != null) { - mHostView.invalidate(dirty); + dirty.offset(location[0], location[1]); + if (mHostView instanceof ViewGroup) { + location[0] = 0; + location[1] = 0; + super.invalidateChildInParent(location, dirty); + return ((ViewGroup) mHostView).invalidateChildInParent(location, dirty); + } else { + invalidate(dirty); + } } return null; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index b30dc83..401db1f 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -2952,6 +2952,8 @@ public final class ViewRootImpl implements ViewParent, private final static int MSG_DISPATCH_DONE_ANIMATING = 22; private final static int MSG_INVALIDATE_WORLD = 23; private final static int MSG_WINDOW_MOVED = 24; + private final static int MSG_ENQUEUE_X_AXIS_KEY_REPEAT = 25; + private final static int MSG_ENQUEUE_Y_AXIS_KEY_REPEAT = 26; final class ViewRootHandler extends Handler { @Override @@ -3003,6 +3005,10 @@ public final class ViewRootImpl implements ViewParent, return "MSG_DISPATCH_DONE_ANIMATING"; case MSG_WINDOW_MOVED: return "MSG_WINDOW_MOVED"; + case MSG_ENQUEUE_X_AXIS_KEY_REPEAT: + return "MSG_ENQUEUE_X_AXIS_KEY_REPEAT"; + case MSG_ENQUEUE_Y_AXIS_KEY_REPEAT: + return "MSG_ENQUEUE_Y_AXIS_KEY_REPEAT"; } return super.getMessageName(message); } @@ -3229,6 +3235,18 @@ public final class ViewRootImpl implements ViewParent, invalidateWorld(mView); } } break; + case MSG_ENQUEUE_X_AXIS_KEY_REPEAT: + case MSG_ENQUEUE_Y_AXIS_KEY_REPEAT: { + KeyEvent oldEvent = (KeyEvent)msg.obj; + KeyEvent e = KeyEvent.changeTimeRepeat(oldEvent, SystemClock.uptimeMillis(), + oldEvent.getRepeatCount() + 1); + if (mAttachInfo.mHasWindowFocus) { + enqueueInputEvent(e); + Message m = obtainMessage(msg.what, e); + m.setAsynchronous(true); + sendMessageDelayed(m, mViewConfiguration.getKeyRepeatDelay()); + } + } break; } } } @@ -3677,6 +3695,7 @@ public final class ViewRootImpl implements ViewParent, if (xDirection != mLastJoystickXDirection) { if (mLastJoystickXKeyCode != 0) { + mHandler.removeMessages(MSG_ENQUEUE_X_AXIS_KEY_REPEAT); enqueueInputEvent(new KeyEvent(time, time, KeyEvent.ACTION_UP, mLastJoystickXKeyCode, 0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); @@ -3688,14 +3707,19 @@ public final class ViewRootImpl implements ViewParent, if (xDirection != 0 && synthesizeNewKeys) { mLastJoystickXKeyCode = xDirection > 0 ? KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT; - enqueueInputEvent(new KeyEvent(time, time, + final KeyEvent e = new KeyEvent(time, time, KeyEvent.ACTION_DOWN, mLastJoystickXKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); + deviceId, 0, KeyEvent.FLAG_FALLBACK, source); + enqueueInputEvent(e); + Message m = mHandler.obtainMessage(MSG_ENQUEUE_X_AXIS_KEY_REPEAT, e); + m.setAsynchronous(true); + mHandler.sendMessageDelayed(m, mViewConfiguration.getKeyRepeatTimeout()); } } if (yDirection != mLastJoystickYDirection) { if (mLastJoystickYKeyCode != 0) { + mHandler.removeMessages(MSG_ENQUEUE_Y_AXIS_KEY_REPEAT); enqueueInputEvent(new KeyEvent(time, time, KeyEvent.ACTION_UP, mLastJoystickYKeyCode, 0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); @@ -3707,9 +3731,13 @@ public final class ViewRootImpl implements ViewParent, if (yDirection != 0 && synthesizeNewKeys) { mLastJoystickYKeyCode = yDirection > 0 ? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP; - enqueueInputEvent(new KeyEvent(time, time, + final KeyEvent e = new KeyEvent(time, time, KeyEvent.ACTION_DOWN, mLastJoystickYKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); + deviceId, 0, KeyEvent.FLAG_FALLBACK, source); + enqueueInputEvent(e); + Message m = mHandler.obtainMessage(MSG_ENQUEUE_Y_AXIS_KEY_REPEAT, e); + m.setAsynchronous(true); + mHandler.sendMessageDelayed(m, mViewConfiguration.getKeyRepeatTimeout()); } } } diff --git a/core/java/android/widget/AppSecurityPermissions.java b/core/java/android/widget/AppSecurityPermissions.java index 06dadb0..7c961bd 100644 --- a/core/java/android/widget/AppSecurityPermissions.java +++ b/core/java/android/widget/AppSecurityPermissions.java @@ -66,20 +66,17 @@ public class AppSecurityPermissions { private final static String TAG = "AppSecurityPermissions"; private final static boolean localLOGV = false; - private Context mContext; - private LayoutInflater mInflater; - private PackageManager mPm; - private PackageInfo mInstalledPackageInfo; + private final Context mContext; + private final LayoutInflater mInflater; + private final PackageManager mPm; private final Map<String, MyPermissionGroupInfo> mPermGroups = new HashMap<String, MyPermissionGroupInfo>(); private final List<MyPermissionGroupInfo> mPermGroupsList = new ArrayList<MyPermissionGroupInfo>(); - private final PermissionGroupInfoComparator mPermGroupComparator; - private final PermissionInfoComparator mPermComparator; - private List<MyPermissionInfo> mPermsList; - private CharSequence mNewPermPrefix; - private Drawable mNormalIcon; - private Drawable mDangerousIcon; + private final PermissionGroupInfoComparator mPermGroupComparator = new PermissionGroupInfoComparator(); + private final PermissionInfoComparator mPermComparator = new PermissionInfoComparator(); + private final List<MyPermissionInfo> mPermsList = new ArrayList<MyPermissionInfo>(); + private final CharSequence mNewPermPrefix; static class MyPermissionGroupInfo extends PermissionGroupInfo { CharSequence mLabel; @@ -113,7 +110,7 @@ public class AppSecurityPermissions { } } - static class MyPermissionInfo extends PermissionInfo { + private static class MyPermissionInfo extends PermissionInfo { CharSequence mLabel; /** @@ -132,19 +129,9 @@ public class AppSecurityPermissions { */ boolean mNew; - MyPermissionInfo() { - } - MyPermissionInfo(PermissionInfo info) { super(info); } - - MyPermissionInfo(MyPermissionInfo info) { - super(info); - mNewReqFlags = info.mNewReqFlags; - mExistingReqFlags = info.mExistingReqFlags; - mNew = info.mNew; - } } public static class PermissionItemView extends LinearLayout implements View.OnClickListener { @@ -233,25 +220,16 @@ public class AppSecurityPermissions { } } - public AppSecurityPermissions(Context context, List<PermissionInfo> permList) { + private AppSecurityPermissions(Context context) { mContext = context; + mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mPm = mContext.getPackageManager(); - loadResources(); - mPermComparator = new PermissionInfoComparator(); - mPermGroupComparator = new PermissionGroupInfoComparator(); - for (PermissionInfo pi : permList) { - mPermsList.add(new MyPermissionInfo(pi)); - } - setPermissions(mPermsList); + // Pick up from framework resources instead. + mNewPermPrefix = mContext.getText(R.string.perms_new_perm_prefix); } - + public AppSecurityPermissions(Context context, String packageName) { - mContext = context; - mPm = mContext.getPackageManager(); - loadResources(); - mPermComparator = new PermissionInfoComparator(); - mPermGroupComparator = new PermissionGroupInfoComparator(); - mPermsList = new ArrayList<MyPermissionInfo>(); + this(context); Set<MyPermissionInfo> permSet = new HashSet<MyPermissionInfo>(); PackageInfo pkgInfo; try { @@ -264,19 +242,12 @@ public class AppSecurityPermissions { if((pkgInfo.applicationInfo != null) && (pkgInfo.applicationInfo.uid != -1)) { getAllUsedPermissions(pkgInfo.applicationInfo.uid, permSet); } - for(MyPermissionInfo tmpInfo : permSet) { - mPermsList.add(tmpInfo); - } + mPermsList.addAll(permSet); setPermissions(mPermsList); } public AppSecurityPermissions(Context context, PackageInfo info) { - mContext = context; - mPm = mContext.getPackageManager(); - loadResources(); - mPermComparator = new PermissionInfoComparator(); - mPermGroupComparator = new PermissionGroupInfoComparator(); - mPermsList = new ArrayList<MyPermissionInfo>(); + this(context); Set<MyPermissionInfo> permSet = new HashSet<MyPermissionInfo>(); if(info == null) { return; @@ -300,23 +271,14 @@ public class AppSecurityPermissions { sharedUid = mPm.getUidForSharedUser(info.sharedUserId); getAllUsedPermissions(sharedUid, permSet); } catch (NameNotFoundException e) { - Log.w(TAG, "Could'nt retrieve shared user id for:"+info.packageName); + Log.w(TAG, "Couldn't retrieve shared user id for: " + info.packageName); } } // Retrieve list of permissions - for (MyPermissionInfo tmpInfo : permSet) { - mPermsList.add(tmpInfo); - } + mPermsList.addAll(permSet); setPermissions(mPermsList); } - private void loadResources() { - // Pick up from framework resources instead. - mNewPermPrefix = mContext.getText(R.string.perms_new_perm_prefix); - mNormalIcon = mContext.getResources().getDrawable(R.drawable.ic_text_dot); - mDangerousIcon = mContext.getResources().getDrawable(R.drawable.ic_bullet_key_permission); - } - /** * Utility to retrieve a view displaying a single permission. This provides * the old UI layout for permissions; it is only here for the device admin @@ -332,10 +294,6 @@ public class AppSecurityPermissions { description, dangerous, icon); } - public PackageInfo getInstalledPackageInfo() { - return mInstalledPackageInfo; - } - private void getAllUsedPermissions(int sharedUid, Set<MyPermissionInfo> permSet) { String sharedPkgList[] = mPm.getPackagesForUid(sharedUid); if(sharedPkgList == null || (sharedPkgList.length == 0)) { @@ -346,17 +304,12 @@ public class AppSecurityPermissions { } } - private void getPermissionsForPackage(String packageName, - Set<MyPermissionInfo> permSet) { - PackageInfo pkgInfo; + private void getPermissionsForPackage(String packageName, Set<MyPermissionInfo> permSet) { try { - pkgInfo = mPm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS); - } catch (NameNotFoundException e) { - Log.w(TAG, "Couldn't retrieve permissions for package:"+packageName); - return; - } - if ((pkgInfo != null) && (pkgInfo.requestedPermissions != null)) { + PackageInfo pkgInfo = mPm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS); extractPerms(pkgInfo, permSet, pkgInfo); + } catch (NameNotFoundException e) { + Log.w(TAG, "Couldn't retrieve permissions for package: " + packageName); } } @@ -367,7 +320,6 @@ public class AppSecurityPermissions { if ((strList == null) || (strList.length == 0)) { return; } - mInstalledPackageInfo = installedPkgInfo; for (int i=0; i<strList.length; i++) { String permName = strList[i]; // If we are only looking at an existing app, then we only @@ -471,8 +423,6 @@ public class AppSecurityPermissions { } public View getPermissionsView(int which) { - mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - LinearLayout permsView = (LinearLayout) mInflater.inflate(R.layout.app_perms_summary, null); LinearLayout displayList = (LinearLayout) permsView.findViewById(R.id.perms_list); View noPermsView = permsView.findViewById(R.id.no_permissions); @@ -557,16 +507,25 @@ public class AppSecurityPermissions { private boolean isDisplayablePermission(PermissionInfo pInfo, int newReqFlags, int existingReqFlags) { final int base = pInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE; - // Dangerous and normal permissions are always shown to the user. - if (base == PermissionInfo.PROTECTION_DANGEROUS || - base == PermissionInfo.PROTECTION_NORMAL) { + final boolean isNormal = (base == PermissionInfo.PROTECTION_NORMAL); + final boolean isDangerous = (base == PermissionInfo.PROTECTION_DANGEROUS); + final boolean isRequired = + ((newReqFlags&PackageInfo.REQUESTED_PERMISSION_REQUIRED) != 0); + final boolean isDevelopment = + ((pInfo.protectionLevel&PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0); + final boolean wasGranted = + ((existingReqFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0); + + // Dangerous and normal permissions are always shown to the user if the permission + // is required, or it was previously granted + if ((isNormal || isDangerous) && (isRequired || wasGranted)) { return true; } + // Development permissions are only shown to the user if they are already // granted to the app -- if we are installing an app and they are not // already granted, they will not be granted as part of the install. - if ((existingReqFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0 - && (pInfo.protectionLevel & PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0) { + if (isDevelopment && wasGranted) { if (localLOGV) Log.i(TAG, "Special perm " + pInfo.name + ": protlevel=0x" + Integer.toHexString(pInfo.protectionLevel)); return true; diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index 0dd567b..7b81aa8 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -36,6 +36,8 @@ import android.widget.ExpandableListConnector.PositionMetadata; import java.util.ArrayList; +import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; + /** * A view that shows items in a vertically scrolling two-level list. This * differs from the {@link ListView} by allowing two levels: groups which can @@ -76,6 +78,10 @@ import java.util.ArrayList; * @attr ref android.R.styleable#ExpandableListView_childIndicatorLeft * @attr ref android.R.styleable#ExpandableListView_childIndicatorRight * @attr ref android.R.styleable#ExpandableListView_childDivider + * @attr ref android.R.styleable#ExpandableListView_indicatorStart + * @attr ref android.R.styleable#ExpandableListView_indicatorEnd + * @attr ref android.R.styleable#ExpandableListView_childIndicatorStart + * @attr ref android.R.styleable#ExpandableListView_childIndicatorEnd */ public class ExpandableListView extends ListView { @@ -134,6 +140,12 @@ public class ExpandableListView extends ListView { /** Right bound for drawing the indicator. */ private int mIndicatorRight; + /** Start bound for drawing the indicator. */ + private int mIndicatorStart; + + /** End bound for drawing the indicator. */ + private int mIndicatorEnd; + /** * Left bound for drawing the indicator of a child. Value of * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorLeft. @@ -147,11 +159,28 @@ public class ExpandableListView extends ListView { private int mChildIndicatorRight; /** + * Start bound for drawing the indicator of a child. Value of + * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorStart. + */ + private int mChildIndicatorStart; + + /** + * End bound for drawing the indicator of a child. Value of + * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorEnd. + */ + private int mChildIndicatorEnd; + + /** * Denotes when a child indicator should inherit this bound from the generic * indicator bounds */ public static final int CHILD_INDICATOR_INHERIT = -1; - + + /** + * Denotes an undefined value for an indicator + */ + private static final int INDICATOR_UNDEFINED = -2; + /** The indicator drawn next to a group. */ private Drawable mGroupIndicator; @@ -202,30 +231,118 @@ public class ExpandableListView extends ListView { super(context, attrs, defStyle); TypedArray a = - context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ExpandableListView, defStyle, - 0); - - mGroupIndicator = a - .getDrawable(com.android.internal.R.styleable.ExpandableListView_groupIndicator); - mChildIndicator = a - .getDrawable(com.android.internal.R.styleable.ExpandableListView_childIndicator); - mIndicatorLeft = a - .getDimensionPixelSize(com.android.internal.R.styleable.ExpandableListView_indicatorLeft, 0); - mIndicatorRight = a - .getDimensionPixelSize(com.android.internal.R.styleable.ExpandableListView_indicatorRight, 0); + context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.ExpandableListView, defStyle, 0); + + mGroupIndicator = a.getDrawable( + com.android.internal.R.styleable.ExpandableListView_groupIndicator); + mChildIndicator = a.getDrawable( + com.android.internal.R.styleable.ExpandableListView_childIndicator); + mIndicatorLeft = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_indicatorLeft, 0); + mIndicatorRight = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_indicatorRight, 0); if (mIndicatorRight == 0 && mGroupIndicator != null) { mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth(); } mChildIndicatorLeft = a.getDimensionPixelSize( - com.android.internal.R.styleable.ExpandableListView_childIndicatorLeft, CHILD_INDICATOR_INHERIT); + com.android.internal.R.styleable.ExpandableListView_childIndicatorLeft, + CHILD_INDICATOR_INHERIT); mChildIndicatorRight = a.getDimensionPixelSize( - com.android.internal.R.styleable.ExpandableListView_childIndicatorRight, CHILD_INDICATOR_INHERIT); - mChildDivider = a.getDrawable(com.android.internal.R.styleable.ExpandableListView_childDivider); - + com.android.internal.R.styleable.ExpandableListView_childIndicatorRight, + CHILD_INDICATOR_INHERIT); + mChildDivider = a.getDrawable( + com.android.internal.R.styleable.ExpandableListView_childDivider); + + if (!isRtlCompatibilityMode()) { + mIndicatorStart = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_indicatorStart, + INDICATOR_UNDEFINED); + mIndicatorEnd = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_indicatorEnd, + INDICATOR_UNDEFINED); + + mChildIndicatorStart = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_childIndicatorStart, + CHILD_INDICATOR_INHERIT); + mChildIndicatorEnd = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_childIndicatorEnd, + CHILD_INDICATOR_INHERIT); + } + a.recycle(); } - - + + /** + * Return true if we are in RTL compatibility mode (either before Jelly Bean MR1 or + * RTL not supported) + */ + private boolean isRtlCompatibilityMode() { + final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion; + return targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport(); + } + + /** + * Return true if the application tag in the AndroidManifest has set "supportRtl" to true + */ + private boolean hasRtlSupport() { + return mContext.getApplicationInfo().hasRtlSupport(); + } + + public void onRtlPropertiesChanged(int layoutDirection) { + resolveIndicator(); + resolveChildIndicator(); + } + + /** + * Resolve start/end indicator. start/end indicator always takes precedence over left/right + * indicator when defined. + */ + private void resolveIndicator() { + final boolean isLayoutRtl = isLayoutRtl(); + if (isLayoutRtl) { + if (mIndicatorStart >= 0) { + mIndicatorRight = mIndicatorStart; + } + if (mIndicatorEnd >= 0) { + mIndicatorLeft = mIndicatorEnd; + } + } else { + if (mIndicatorStart >= 0) { + mIndicatorLeft = mIndicatorStart; + } + if (mIndicatorEnd >= 0) { + mIndicatorRight = mIndicatorEnd; + } + } + if (mIndicatorRight == 0 && mGroupIndicator != null) { + mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth(); + } + } + + /** + * Resolve start/end child indicator. start/end child indicator always takes precedence over + * left/right child indicator when defined. + */ + private void resolveChildIndicator() { + final boolean isLayoutRtl = isLayoutRtl(); + if (isLayoutRtl) { + if (mChildIndicatorStart >= CHILD_INDICATOR_INHERIT) { + mChildIndicatorRight = mChildIndicatorStart; + } + if (mChildIndicatorEnd >= CHILD_INDICATOR_INHERIT) { + mChildIndicatorLeft = mChildIndicatorEnd; + } + } else { + if (mChildIndicatorStart >= CHILD_INDICATOR_INHERIT) { + mChildIndicatorLeft = mChildIndicatorStart; + } + if (mChildIndicatorEnd >= CHILD_INDICATOR_INHERIT) { + mChildIndicatorRight = mChildIndicatorEnd; + } + } + } + @Override protected void dispatchDraw(Canvas canvas) { // Draw children, etc. @@ -1053,8 +1170,26 @@ public class ExpandableListView extends ListView { public void setChildIndicatorBounds(int left, int right) { mChildIndicatorLeft = left; mChildIndicatorRight = right; + resolveChildIndicator(); } - + + /** + * Sets the relative drawing bounds for the child indicator. For either, you can + * specify {@link #CHILD_INDICATOR_INHERIT} to use inherit from the general + * indicator's bounds. + * + * @see #setIndicatorBounds(int, int) + * @param start The start position (relative to the start bounds of this View) + * to start drawing the indicator. + * @param end The end position (relative to the end bounds of this + * View) to end the drawing of the indicator. + */ + public void setChildIndicatorBoundsRelative(int start, int end) { + mChildIndicatorStart = start; + mChildIndicatorEnd = end; + resolveChildIndicator(); + } + /** * Sets the indicator to be drawn next to a group. * @@ -1084,8 +1219,26 @@ public class ExpandableListView extends ListView { public void setIndicatorBounds(int left, int right) { mIndicatorLeft = left; mIndicatorRight = right; + resolveIndicator(); } - + + /** + * Sets the relative drawing bounds for the indicators (at minimum, the group indicator + * is affected by this; the child indicator is affected by this if the + * child indicator bounds are set to inherit). + * + * @see #setChildIndicatorBounds(int, int) + * @param start The start position (relative to the start bounds of this View) + * to start drawing the indicator. + * @param end The end position (relative to the end bounds of this + * View) to end the drawing of the indicator. + */ + public void setIndicatorBoundsRelative(int start, int end) { + mIndicatorStart = start; + mIndicatorEnd = end; + resolveIndicator(); + } + /** * Extra menu information specific to an {@link ExpandableListView} provided * to the diff --git a/core/java/com/android/internal/backup/LocalTransport.java b/core/java/com/android/internal/backup/LocalTransport.java index eed3e67..eb2d1fe 100644 --- a/core/java/com/android/internal/backup/LocalTransport.java +++ b/core/java/com/android/internal/backup/LocalTransport.java @@ -27,6 +27,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.os.RemoteException; +import android.os.SELinux; import android.util.Log; import com.android.org.bouncycastle.util.encoders.Base64; @@ -64,6 +65,10 @@ public class LocalTransport extends IBackupTransport.Stub { public LocalTransport(Context context) { mContext = context; + mDataDir.mkdirs(); + if (!SELinux.restorecon(mDataDir)) { + Log.e(TAG, "SELinux restorecon failed for " + mDataDir); + } } public Intent configurationIntent() { diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index 6bb7ac7..d1db230 100644 --- a/core/java/com/android/internal/widget/ActionBarView.java +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -835,6 +835,8 @@ public class ActionBarView extends AbsActionBarView { (TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mSubtitle))) { // Don't show while in expanded mode or with empty text mTitleLayout.setVisibility(GONE); + } else { + mTitleLayout.setVisibility(VISIBLE); } } @@ -1587,15 +1589,10 @@ public class ActionBarView extends AbsActionBarView { mTitleLayout.setVisibility(VISIBLE); } } - if (mTabScrollView != null && mNavigationMode == ActionBar.NAVIGATION_MODE_TABS) { - mTabScrollView.setVisibility(VISIBLE); - } - if (mSpinner != null && mNavigationMode == ActionBar.NAVIGATION_MODE_LIST) { - mSpinner.setVisibility(VISIBLE); - } - if (mCustomNavView != null && (mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) { - mCustomNavView.setVisibility(VISIBLE); - } + if (mTabScrollView != null) mTabScrollView.setVisibility(VISIBLE); + if (mSpinner != null) mSpinner.setVisibility(VISIBLE); + if (mCustomNavView != null) mCustomNavView.setVisibility(VISIBLE); + mExpandedHomeLayout.setIcon(null); mCurrentExpandedItem = null; setHomeButtonEnabled(mWasHomeEnabled); // Set by expandItemActionView above diff --git a/core/jni/android_ddm_DdmHandleNativeHeap.cpp b/core/jni/android_ddm_DdmHandleNativeHeap.cpp index 42d408d..f5eaf94 100644 --- a/core/jni/android_ddm_DdmHandleNativeHeap.cpp +++ b/core/jni/android_ddm_DdmHandleNativeHeap.cpp @@ -2,16 +2,16 @@ ** ** Copyright 2006, 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 +** 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 +** 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 +** 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. */ @@ -23,20 +23,17 @@ #include <android_runtime/AndroidRuntime.h> #include <utils/Log.h> +#include <utils/String8.h> #include <fcntl.h> #include <errno.h> #include <sys/types.h> #include <sys/stat.h> -#if defined(__arm__) -extern "C" void get_malloc_leak_info(uint8_t** info, size_t* overallSize, - size_t* infoSize, size_t* totalMemory, size_t* backtraceSize); - -extern "C" void free_malloc_leak_info(uint8_t* info); -#endif +extern "C" void get_malloc_leak_info(uint8_t** info, size_t* overallSize, + size_t* infoSize, size_t* totalMemory, size_t* backtraceSize); -#define MAPS_FILE_SIZE 65 * 1024 +extern "C" void free_malloc_leak_info(uint8_t* info); struct Header { size_t mapSize; @@ -48,96 +45,57 @@ struct Header { namespace android { +static void ReadFile(const char* path, String8& s) { + int fd = open(path, O_RDONLY); + if (fd != -1) { + char bytes[1024]; + ssize_t byteCount; + while ((byteCount = TEMP_FAILURE_RETRY(read(fd, bytes, sizeof(bytes)))) > 0) { + s.append(bytes, byteCount); + } + close(fd); + } +} + /* - * Retrieve the native heap information and the info from /proc/<self>/maps, + * Retrieve the native heap information and the info from /proc/self/maps, * copy them into a byte[] with a "struct Header" that holds data offsets, * and return the array. */ -static jbyteArray getLeakInfo(JNIEnv *env, jobject clazz) -{ -#if defined(__arm__) - // get the info in /proc/[pid]/map +static jbyteArray DdmHandleNativeHeap_getLeakInfo(JNIEnv* env, jobject) { Header header; memset(&header, 0, sizeof(header)); - pid_t pid = getpid(); - - char path[FILENAME_MAX]; - sprintf(path, "/proc/%d/maps", pid); - - struct stat sb; - int ret = stat(path, &sb); - - uint8_t* mapsFile = NULL; - if (ret == 0) { - mapsFile = (uint8_t*)malloc(MAPS_FILE_SIZE); - int fd = open(path, O_RDONLY); - - if (mapsFile != NULL && fd != -1) { - int amount = 0; - do { - uint8_t* ptr = mapsFile + header.mapSize; - amount = read(fd, ptr, MAPS_FILE_SIZE); - if (amount <= 0) { - if (errno != EINTR) - break; - else - continue; - } - header.mapSize += amount; - } while (header.mapSize < MAPS_FILE_SIZE); - - ALOGD("**** read %d bytes from '%s'", (int) header.mapSize, path); - } - } + String8 maps; + ReadFile("/proc/self/maps", maps); + header.mapSize = maps.size(); uint8_t* allocBytes; - get_malloc_leak_info(&allocBytes, &header.allocSize, &header.allocInfoSize, - &header.totalMemory, &header.backtraceSize); + get_malloc_leak_info(&allocBytes, &header.allocSize, &header.allocInfoSize, + &header.totalMemory, &header.backtraceSize); - jbyte* bytes = NULL; - jbyte* ptr = NULL; - jbyteArray array = env->NewByteArray(sizeof(Header) + header.mapSize + header.allocSize); - if (array == NULL) { - goto done; - } - - bytes = env->GetByteArrayElements(array, NULL); - ptr = bytes; - -// ALOGD("*** mapSize: %d allocSize: %d allocInfoSize: %d totalMemory: %d", -// header.mapSize, header.allocSize, header.allocInfoSize, header.totalMemory); + ALOGD("*** mapSize: %d allocSize: %d allocInfoSize: %d totalMemory: %d", + header.mapSize, header.allocSize, header.allocInfoSize, header.totalMemory); - memcpy(ptr, &header, sizeof(header)); - ptr += sizeof(header); - - if (header.mapSize > 0 && mapsFile != NULL) { - memcpy(ptr, mapsFile, header.mapSize); - ptr += header.mapSize; + jbyteArray array = env->NewByteArray(sizeof(Header) + header.mapSize + header.allocSize); + if (array != NULL) { + env->SetByteArrayRegion(array, 0, + sizeof(header), reinterpret_cast<jbyte*>(&header)); + env->SetByteArrayRegion(array, sizeof(header), + maps.size(), reinterpret_cast<const jbyte*>(maps.string())); + env->SetByteArrayRegion(array, sizeof(header) + maps.size(), + header.allocSize, reinterpret_cast<jbyte*>(allocBytes)); } - - memcpy(ptr, allocBytes, header.allocSize); - env->ReleaseByteArrayElements(array, bytes, 0); -done: - if (mapsFile != NULL) { - free(mapsFile); - } - // free the info up! free_malloc_leak_info(allocBytes); - return array; -#else - return NULL; -#endif } static JNINativeMethod method_table[] = { - { "getLeakInfo", "()[B", (void*)getLeakInfo }, + { "getLeakInfo", "()[B", (void*) DdmHandleNativeHeap_getLeakInfo }, }; -int register_android_ddm_DdmHandleNativeHeap(JNIEnv *env) -{ +int register_android_ddm_DdmHandleNativeHeap(JNIEnv* env) { return AndroidRuntime::registerNativeMethods(env, "android/ddm/DdmHandleNativeHeap", method_table, NELEM(method_table)); } diff --git a/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_default_holo_dark.9.png b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_default_holo_dark.9.png Binary files differnew file mode 100644 index 0000000..45450e4 --- /dev/null +++ b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_default_holo_dark.9.png diff --git a/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_default_holo_light.9.png b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_default_holo_light.9.png Binary files differnew file mode 100644 index 0000000..b568989 --- /dev/null +++ b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_default_holo_light.9.png diff --git a/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_focused_holo_dark.9.png b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_focused_holo_dark.9.png Binary files differnew file mode 100644 index 0000000..e043458 --- /dev/null +++ b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_focused_holo_dark.9.png diff --git a/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_focused_holo_light.9.png b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_focused_holo_light.9.png Binary files differnew file mode 100644 index 0000000..f208c99 --- /dev/null +++ b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_focused_holo_light.9.png diff --git a/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_pressed_holo_dark.9.png b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_pressed_holo_dark.9.png Binary files differnew file mode 100644 index 0000000..94eb994 --- /dev/null +++ b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_pressed_holo_dark.9.png diff --git a/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_pressed_holo_light.9.png b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_pressed_holo_light.9.png Binary files differnew file mode 100644 index 0000000..1fee149 --- /dev/null +++ b/core/res/res/drawable-ldrtl-hdpi/btn_cab_done_pressed_holo_light.9.png diff --git a/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_default_holo_dark.9.png b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_default_holo_dark.9.png Binary files differnew file mode 100644 index 0000000..abffc49 --- /dev/null +++ b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_default_holo_dark.9.png diff --git a/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_default_holo_light.9.png b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_default_holo_light.9.png Binary files differnew file mode 100644 index 0000000..a369081 --- /dev/null +++ b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_default_holo_light.9.png diff --git a/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_focused_holo_dark.9.png b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_focused_holo_dark.9.png Binary files differnew file mode 100644 index 0000000..e33e964 --- /dev/null +++ b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_focused_holo_dark.9.png diff --git a/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_focused_holo_light.9.png b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_focused_holo_light.9.png Binary files differnew file mode 100644 index 0000000..0a845dd --- /dev/null +++ b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_focused_holo_light.9.png diff --git a/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_pressed_holo_dark.9.png b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_pressed_holo_dark.9.png Binary files differnew file mode 100644 index 0000000..74b0352 --- /dev/null +++ b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_pressed_holo_dark.9.png diff --git a/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_pressed_holo_light.9.png b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_pressed_holo_light.9.png Binary files differnew file mode 100644 index 0000000..bfb4972 --- /dev/null +++ b/core/res/res/drawable-ldrtl-mdpi/btn_cab_done_pressed_holo_light.9.png diff --git a/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_default_holo_dark.9.png b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_default_holo_dark.9.png Binary files differnew file mode 100644 index 0000000..d253dd4 --- /dev/null +++ b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_default_holo_dark.9.png diff --git a/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_default_holo_light.9.png b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_default_holo_light.9.png Binary files differnew file mode 100644 index 0000000..65f9ec1 --- /dev/null +++ b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_default_holo_light.9.png diff --git a/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_focused_holo_dark.9.png b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_focused_holo_dark.9.png Binary files differnew file mode 100644 index 0000000..105da60 --- /dev/null +++ b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_focused_holo_dark.9.png diff --git a/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_focused_holo_light.9.png b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_focused_holo_light.9.png Binary files differnew file mode 100644 index 0000000..de53be7 --- /dev/null +++ b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_focused_holo_light.9.png diff --git a/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_pressed_holo_dark.9.png b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_pressed_holo_dark.9.png Binary files differnew file mode 100644 index 0000000..3be0b0c --- /dev/null +++ b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_pressed_holo_dark.9.png diff --git a/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_pressed_holo_light.9.png b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_pressed_holo_light.9.png Binary files differnew file mode 100644 index 0000000..878c702 --- /dev/null +++ b/core/res/res/drawable-ldrtl-xhdpi/btn_cab_done_pressed_holo_light.9.png diff --git a/core/res/res/values-in/strings.xml b/core/res/res/values-in/strings.xml index 630b1a5..88b5a2d 100644 --- a/core/res/res/values-in/strings.xml +++ b/core/res/res/values-in/strings.xml @@ -355,7 +355,7 @@ <string name="permlab_canRequestEnahncedWebAccessibility" msgid="1905232971331801453">"meminta aksesibilitas web yang disempurnakan"</string> <string name="permdesc_canRequestEnahncedWebAccessibility" msgid="4500520989321729676">"Memungkinkan pemegang meminta pengaktifan penyempurnaan aksesibilitas web. Contohnya, memasang skrip agar konten aplikasi lebih mudah diakses."</string> <string name="permlab_bindTextService" msgid="7358378401915287938">"mengikat ke layanan SMS"</string> - <string name="permdesc_bindTextService" msgid="8151968910973998670">"Mengizinkan pemegang mengikat antarmuka tingkat tinggi dari suatu layanan teks (mis. SpellCheckerService). Tidak pernah diperlukan oleh apl normal."</string> + <string name="permdesc_bindTextService" msgid="8151968910973998670">"Mengizinkan pemegang mengikat antarmuka tingkat tinggi dari suatu layanan teks (misal: SpellCheckerService). Tidak pernah diperlukan oleh apl normal."</string> <string name="permlab_bindVpnService" msgid="4708596021161473255">"mengikat ke layanan VPN"</string> <string name="permdesc_bindVpnService" msgid="2067845564581693905">"Mengizinkan pemegang mengikat antarmuka tingkat tinggi dari suatu layanan Vpn. Tidak pernah diperlukan oleh apl normal."</string> <string name="permlab_bindWallpaper" msgid="8716400279937856462">"mengikat ke wallpaper"</string> diff --git a/core/res/res/values-sw/strings.xml b/core/res/res/values-sw/strings.xml index 58e98a3..04ee91e 100644 --- a/core/res/res/values-sw/strings.xml +++ b/core/res/res/values-sw/strings.xml @@ -641,7 +641,7 @@ <string name="policydesc_expirePassword" msgid="1729725226314691591">"Dhibiti ni mara ngapi nenosiri la kufunga skrini linafaa libadilishwe."</string> <string name="policylab_encryptedStorage" msgid="8901326199909132915">"Weka msimbo fiche wa hifadhi"</string> <string name="policydesc_encryptedStorage" msgid="2637732115325316992">"Inahitaji kwamba data ya programu iliyohifadhiwa iwe na msimbo fiche."</string> - <string name="policylab_disableCamera" msgid="6395301023152297826">"Lemaza kamera"</string> + <string name="policylab_disableCamera" msgid="6395301023152297826">"Zima kamera"</string> <string name="policydesc_disableCamera" msgid="2306349042834754597">"Zuia matumizi yote ya kamera za kifaa."</string> <string name="policylab_disableKeyguardFeatures" msgid="266329104542638802">"Lemaza vipengele kwenye kilinzi cha kitufe."</string> <string name="policydesc_disableKeyguardFeatures" msgid="3467082272186534614">"Inazuia matumizi ya baadhi ya vipengele kwenye kilinzi cha kitufe."</string> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index badd3d7..5282df3 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2709,6 +2709,16 @@ below and above child items.) The height of this will be the same as the height of the normal list item divider. --> <attr name="childDivider" format="reference|color" /> + <!-- The start bound for an item's indicator. To specify a start bound specific to children, + use childIndicatorStart. --> + <attr name="indicatorStart" format="dimension" /> + <!-- The end bound for an item's indicator. To specify a right bound specific to children, + use childIndicatorEnd. --> + <attr name="indicatorEnd" format="dimension" /> + <!-- The start bound for a child's indicator. --> + <attr name="childIndicatorStart" format="dimension" /> + <!-- The end bound for a child's indicator. --> + <attr name="childIndicatorEnd" format="dimension" /> </declare-styleable> <declare-styleable name="Gallery"> <attr name="gravity" /> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 9f810af..489a947 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2037,6 +2037,11 @@ <public type="attr" name="mirrorForRtl" /> <public type="attr" name="windowOverscan" /> <public type="attr" name="requiredForAllUsers" /> + <public type="attr" name="indicatorStart" /> + <public type="attr" name="indicatorEnd" /> + <public type="attr" name="childIndicatorStart" /> + <public type="attr" name="childIndicatorEnd" /> + <public type="style" name="Theme.NoTitleBar.Overscan" /> <public type="style" name="Theme.Light.NoTitleBar.Overscan" /> <public type="style" name="Theme.Black.NoTitleBar.Overscan" /> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index a2d4570..9e10661 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3202,7 +3202,8 @@ <string name="wifi_p2p_enter_pin_message">Type the required PIN: </string> <string name="wifi_p2p_show_pin_message">PIN: </string> - <string name="wifi_p2p_frequency_conflict_message">The phone will temporarily disconnect from Wi-Fi while it\'s connected to <xliff:g id="device_name">%1$s</xliff:g></string> + <string name="wifi_p2p_frequency_conflict_message" product="tablet">The tablet will temporarily disconnect from Wi-Fi while it\'s connected to <xliff:g id="device_name">%1$s</xliff:g></string> + <string name="wifi_p2p_frequency_conflict_message" product="default">The phone will temporarily disconnect from Wi-Fi while it\'s connected to <xliff:g id="device_name">%1$s</xliff:g></string> <!-- Name of the dialog that lets the user choose an accented character to insert --> <string name="select_character">Insert character</string> diff --git a/docs/html/guide/topics/providers/calendar-provider.jd b/docs/html/guide/topics/providers/calendar-provider.jd index f53b062..5adc68c 100644 --- a/docs/html/guide/topics/providers/calendar-provider.jd +++ b/docs/html/guide/topics/providers/calendar-provider.jd @@ -605,7 +605,7 @@ ContentValues values = new ContentValues(); Uri updateUri = null; // The new title for the event values.put(Events.TITLE, "Kickboxing"); -myUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventID); +updateUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventID); int rows = getContentResolver().update(updateUri, values, null, null); Log.i(DEBUG_TAG, "Rows updated: " + rows); </pre> diff --git a/docs/html/training/contacts-provider/ContactsList.zip b/docs/html/training/contacts-provider/ContactsList.zip Binary files differnew file mode 100644 index 0000000..d2a5cfb --- /dev/null +++ b/docs/html/training/contacts-provider/ContactsList.zip diff --git a/docs/html/training/contacts-provider/display-contact-badge.jd b/docs/html/training/contacts-provider/display-contact-badge.jd new file mode 100644 index 0000000..f08935d --- /dev/null +++ b/docs/html/training/contacts-provider/display-contact-badge.jd @@ -0,0 +1,635 @@ +page.title=Displaying the Quick Contact Badge + +trainingnavtop=true +@jd:body + + +<div id="tb-wrapper"> +<div id="tb"> + +<!-- table of contents --> +<h2>This lesson teaches you to</h2> +<ol> + <li> + <a href="#AddView">Add a QuickContactBadge View</a> + </li> + <li> + <a href="#SetURIThumbnail">Set the Contact URI and Thumbnail</a> + </li> + <li> + <a href="#ListView"> + Add a QuickContactBadge to a ListView + </a> + </li> +</ol> + +<!-- other docs (NOT javadocs) --> +<h2>You should also read</h2> +<ul> + <li> + <a href="{@docRoot}guide/topics/providers/content-provider-basics.html"> + Content Provider Basics + </a> + </li> + <li> + <a href="{@docRoot}guide/topics/providers/contacts-provider.html"> + Contacts Provider + </a> + </li> +</ul> + +<h2>Try it out</h2> + +<div class="download-box"> + <a href="http://developer.android.com/shareables/training/ContactsList.zip" class="button"> + Download the sample + </a> + <p class="filename">ContactsList.zip</p> +</div> + +</div> +</div> +<p> + This lesson shows you how to add a {@link android.widget.QuickContactBadge} to your UI + and how to bind data to it. A {@link android.widget.QuickContactBadge} is a widget that + initially appears as a thumbnail image. Although you can use any {@link android.graphics.Bitmap} + for the thumbnail image, you usually use a {@link android.graphics.Bitmap} decoded from the + contact's photo thumbnail image. +</p> +<p> + The small image acts as a control; when users click on the image, the + {@link android.widget.QuickContactBadge} expands into a dialog containing the following: +</p> +<dl> + <dt>A large image</dt> + <dd> + The large image associated with the contact, or no image is available, a placeholder + graphic. + </dd> + <dt> + App icons + </dt> + <dd> + An app icon for each piece of detail data that can be handled by a built-in app. For + example, if the contact's details include one or more email addresses, an email icon + appears. When users click the icon, all of the contact's email addresses appear. When users + click one of the addresses, the email app displays a screen for composing a message to the + selected email address. + </dd> +</dl> +<p> + The {@link android.widget.QuickContactBadge} view provides instant access to a contact's + details, as well as a fast way of communicating with the contact. Users don't have to look up + a contact, find and copy information, and then paste it into the appropriate app. Instead, they + can click on the {@link android.widget.QuickContactBadge}, choose the communication method they + want to use, and send the information for that method directly to the appropriate app. +</p> +<h2 id="AddView">Add a QuickContactBadge View</h2> +<p> + To add a {@link android.widget.QuickContactBadge}, insert a + <code><QuickContactBadge></code> element in your layout. For example: +</p> +<pre> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> +... + <QuickContactBadge + android:id=@+id/quickbadge + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:scaleType="centerCrop"/> + ... +</RelativeLayout> +</pre> +<h2 id="">Retrieve provider data</h2> +<p> + To display a contact in the {@link android.widget.QuickContactBadge}, you need a content URI + for the contact and a {@link android.graphics.Bitmap} for the small image. You generate + both the content URI and the {@link android.graphics.Bitmap} from columns retrieved from the + Contacts Provider. Specify these columns as part of the projection you use to load data into + your {@link android.database.Cursor}. +</p> +<p> + For Android 3.0 (API level 11) and later, include the following columns in your projection:</p> +<ul> + <li>{@link android.provider.ContactsContract.Contacts#_ID Contacts._ID}</li> + <li>{@link android.provider.ContactsContract.Contacts#LOOKUP_KEY Contacts.LOOKUP_KEY}</li> + <li> + {@link android.provider.ContactsContract.Contacts#PHOTO_THUMBNAIL_URI + Contacts.PHOTO_THUMBNAIL_URI} + </li> +</ul> +<p> + For Android 2.3.3 (API level 10) and earlier, use the following columns: +</p> +<ul> + <li>{@link android.provider.ContactsContract.Contacts#_ID Contacts._ID}</li> + <li>{@link android.provider.ContactsContract.Contacts#LOOKUP_KEY Contacts.LOOKUP_KEY}</li> +</ul> +<p> + The remainder of this lesson assumes that you've already loaded a + {@link android.database.Cursor} that contains these columns as well as others you may have + chosen. To learn how to retrieve this columns in a {@link android.database.Cursor}, read the + lesson <a href="retrieve-names.html">Retrieving a List of Contacts</a>. +</p> +<h2 id="SetURIThumbnail">Set the Contact URI and Thumbnail</h2> +<p> + Once you have the necessary columns, you can bind data to the + {@link android.widget.QuickContactBadge}. +</p> +<h3>Set the Contact URI</h3> +<p> + To set the content URI for the contact, call + {@link android.provider.ContactsContract.Contacts#getLookupUri getLookupUri(id,lookupKey)} to + get a {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}, then + call {@link android.widget.QuickContactBadge#assignContactUri assignContactUri()} to set the + contact. For example: +</p> +<pre> + // The Cursor that contains contact rows + Cursor mCursor; + // The index of the _ID column in the Cursor + int mIdColumn; + // The index of the LOOKUP_KEY column in the Cursor + int mLookupKeyColumn; + // A content URI for the desired contact + Uri mContactUri; + // A handle to the QuickContactBadge view + QuickContactBadge mBadge; + ... + mBadge = (QuickContactBadge) findViewById(R.id.quickbadge); + /* + * Insert code here to move to the desired cursor row + */ + // Gets the _ID column index + mIdColumn = mCursor.getColumnIndex(Contacts._ID); + // Gets the LOOKUP_KEY index + mLookupKeyColumn = mCursor.getColumnIndex(Contacts.LOOKUP_KEY); + // Gets a content URI for the contact + mContactUri = + Contacts.getLookupUri( + Cursor.getLong(mIdColumn), + Cursor.getString(mLookupKeyColumn) + ); + mBadge.assignContactUri(mContactUri); +</pre> +<p> + When users click the {@link android.widget.QuickContactBadge} icon, the contact's + details automatically appear in the dialog. +</p> +<h3>Set the photo thumbnail</h3> +<p> + Setting the contact URI for the {@link android.widget.QuickContactBadge} does not automatically + load the contact's thumbnail photo. To load the photo, get a URI for the photo from the + contact's {@link android.database.Cursor} row, use it to open the file containing the compressed + thumbnail photo, and read the file into a {@link android.graphics.Bitmap}. +</p> +<p class="note"> + <strong>Note:</strong> The + {@link android.provider.ContactsContract.Contacts#PHOTO_THUMBNAIL_URI} column isn't available + in platform versions prior to 3.0. For those versions, you must retrieve the URI + from the {@link android.provider.ContactsContract.Contacts.Photo Contacts.Photo} subtable. +</p> +<p> + First, set up variables for accessing the {@link android.database.Cursor} containing the + {@link android.provider.ContactsContract.Contacts#_ID Contacts._ID} and + {@link android.provider.ContactsContract.Contacts#LOOKUP_KEY Contacts.LOOKUP_KEY} columns, as + described previously: +</p> +<pre> + // The column in which to find the thumbnail ID + int mThumbnailColumn; + /* + * The thumbnail URI, expressed as a String. + * Contacts Provider stores URIs as String values. + */ + String mThumbnailUri; + ... + /* + * Gets the photo thumbnail column index if + * platform version >= Honeycomb + */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mThumbnailColumn = + mCursor.getColumnIndex(Contacts.PHOTO_THUMBNAIL_URI); + // Otherwise, sets the thumbnail column to the _ID column + } else { + mThumbnailColumn = mIdColumn; + } + /* + * Assuming the current Cursor position is the contact you want, + * gets the thumbnail ID + */ + mThumbnailUri = Cursor.getString(mThumbnailColumn); + ... +</pre> +<p> + Define a method that takes photo-related data for the contact and dimensions for the + destination view, and returns the properly-sized thumbnail in a + {@link android.graphics.Bitmap}. Start by constructing a URI that points to the + thumbnail: +<p> +<pre> + /** + * Load a contact photo thumbnail and return it as a Bitmap, + * resizing the image to the provided image dimensions as needed. + * @param photoData photo ID Prior to Honeycomb, the contact's _ID value. + * For Honeycomb and later, the value of PHOTO_THUMBNAIL_URI. + * @return A thumbnail Bitmap, sized to the provided width and height. + * Returns null if the thumbnail is not found. + */ + private Bitmap loadContactPhotoThumbnail(String photoData) { + // Creates an asset file descriptor for the thumbnail file. + AssetFileDescriptor afd = null; + // try-catch block for file not found + try { + // Creates a holder for the URI. + Uri thumbUri; + // If Android 3.0 or later + if (Build.VERSION.SDK_INT + >= + Build.VERSION_CODES.HONEYCOMB) { + // Sets the URI from the incoming PHOTO_THUMBNAIL_URI + thumbUri = Uri.parse(photoData); + } else { + // Prior to Android 3.0, constructs a photo Uri using _ID + /* + * Creates a contact URI from the Contacts content URI + * incoming photoData (_ID) + */ + final Uri contactUri = Uri.withAppendedPath( + Contacts.CONTENT_URI, photoData); + /* + * Creates a photo URI by appending the content URI of + * Contacts.Photo. + */ + thumbUri = + Uri.withAppendedPath( + contactUri, Photo.CONTENT_DIRECTORY); + } + + /* + * Retrieves an AssetFileDescriptor object for the thumbnail + * URI + * using ContentResolver.openAssetFileDescriptor + */ + afd = getActivity().getContentResolver(). + openAssetFileDescriptor(thumbUri, "r"); + /* + * Gets a file descriptor from the asset file descriptor. + * This object can be used across processes. + */ + FileDescriptor fileDescriptor = afd.getFileDescriptor(); + // Decode the photo file and return the result as a Bitmap + // If the file descriptor is valid + if (fileDescriptor != null) { + // Decodes the bitmap + return BitmapFactory.decodeFileDescriptor( + fileDescriptor, null, null); + } + // If the file isn't found + } catch (FileNotFoundException e) { + /* + * Handle file not found errors + */ + } + // In all cases, close the asset file descriptor + } finally { + if (afd != null) { + try { + afd.close(); + } catch (IOException e) {} + } + } + return null; + } +</pre> +<p> + Call the <code>loadContactPhotoThumbnail()</code> method in your code to get the + thumbnail {@link android.graphics.Bitmap}, and use the result to set the photo thumbnail in + your {@link android.widget.QuickContactBadge}: +</p> +<pre> + ... + /* + * Decodes the thumbnail file to a Bitmap. + */ + Bitmap mThumbnail = + loadContactPhotoThumbnail(mThumbnailUri); + /* + * Sets the image in the QuickContactBadge + * QuickContactBadge inherits from ImageView, so + */ + mBadge.setImageBitmap(mThumbnail); +</pre> +<h2 id="ListView">Add a QuickContactBadge to a ListView</h2> +<p> + A {@link android.widget.QuickContactBadge} is a useful addition to a + {@link android.widget.ListView} that displays a list of contacts. Use the + {@link android.widget.QuickContactBadge} to display a thumbnail photo for each contact; when + users click the thumbnail, the {@link android.widget.QuickContactBadge} dialog appears. +</p> +<h3>Add the QuickContactBadge element</h3> +<p> + To start, add a {@link android.widget.QuickContactBadge} view element to your item layout + For example, if you want to display a {@link android.widget.QuickContactBadge} and a name for + each contact you retrieve, put the following XML into a layout file: +</p> +<pre> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <QuickContactBadge + android:id="@+id/quickcontact" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:scaleType="centerCrop"/> + <TextView android:id="@+id/displayname" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_toRightOf="@+id/quickcontact" + android:gravity="center_vertical" + android:layout_alignParentRight="true" + android:layout_alignParentTop="true"/> +</RelativeLayout> +</pre> +<p> + In the following sections, this file is referred to as <code>contact_item_layout.xml</code>. +</p> +<h3>Set up a custom CursorAdapter</h3> +<p> + To bind a {@link android.support.v4.widget.CursorAdapter} to a {@link android.widget.ListView} + containing a {@link android.widget.QuickContactBadge}, define a custom adapter that + extends {@link android.support.v4.widget.CursorAdapter}. This approach allows you to process the + data in the {@link android.database.Cursor} before you bind it to the + {@link android.widget.QuickContactBadge}. This approach also allows you to bind multiple + {@link android.database.Cursor} columns to the {@link android.widget.QuickContactBadge}. Neither + of these operations is possible in a regular {@link android.support.v4.widget.CursorAdapter}. +</p> +<p> + The subclass of {@link android.support.v4.widget.CursorAdapter} that you define must + override the following methods: +</p> +<dl> + <dt>{@link android.support.v4.widget.CursorAdapter#newView CursorAdapter.newView()}</dt> + <dd> + Inflates a new {@link android.view.View} object to hold the item layout. In the override + of this method, store handles to the child {@link android.view.View} objects of the layout, + including the child {@link android.widget.QuickContactBadge}. By taking this approach, you + avoid having to get handles to the child {@link android.view.View} objects each time you + inflate a new layout. + <p> + You must override this method so you can get handles to the individual child + {@link android.view.View} objects. This technique allows you to control their binding in + {@link android.support.v4.widget.CursorAdapter#bindView CursorAdapter.bindView()}. + </p> + </dd> + <dt>{@link android.support.v4.widget.CursorAdapter#bindView CursorAdapter.bindView()}</dt> + <dd> + Moves data from the current {@link android.database.Cursor} row to the child + {@link android.view.View} objects of the item layout. You must override this method so + you can bind both the contact's URI and thumbnail to the + {@link android.widget.QuickContactBadge}. The default implementation only allows a 1-to-1 + mapping between a column and a {@link android.view.View} + </dd> +</dl> +<p> + The following code snippet contains an example of a custom subclass of + {@link android.support.v4.widget.CursorAdapter}: +</p> +<h3>Define the custom list adapter</h3> +<p> + Define the subclass of {@link android.support.v4.widget.CursorAdapter} including its + constructor, and override + {@link android.support.v4.widget.CursorAdapter#newView newView()} and + {@link android.support.v4.widget.CursorAdapter#bindView bindView()}: +</p> +<pre> + /** + * + * + */ + private class ContactsAdapter extends CursorAdapter { + private LayoutInflater mInflater; + ... + public ContactsAdapter(Context context) { + super(context, null, 0); + + /* + * Gets an inflater that can instantiate + * the ListView layout from the file. + */ + mInflater = LayoutInflater.from(context); + ... + } + ... + /** + * Defines a class that hold resource IDs of each item layout + * row to prevent having to look them up each time data is + * bound to a row. + */ + private class ViewHolder { + TextView displayname; + QuickContactBadge quickcontact; + } + .. + @Override + public View newView( + Context context, + Cursor cursor, + ViewGroup viewGroup) { + /* Inflates the item layout. Stores resource IDs in a + * in a ViewHolder class to prevent having to look + * them up each time bindView() is called. + */ + final View itemView = + mInflater.inflate( + R.layout.contact_list_layout, + viewGroup, + false + ); + final ViewHolder holder = new ViewHolder(); + holder.displayname = + (TextView) view.findViewById(R.id.displayname); + holder.quickcontact = + (QuickContactBadge) + view.findViewById(R.id.quickcontact); + view.setTag(holder); + return view; + } + ... + @Override + public void bindView( + View view, + Context context, + Cursor cursor) { + final ViewHolder holder = (ViewHolder) view.getTag(); + final String photoData = + cursor.getString(mPhotoDataIndex); + final String displayName = + cursor.getString(mDisplayNameIndex); + ... + // Sets the display name in the layout + holder.displayname = cursor.getString(mDisplayNameIndex); + ... + /* + * Generates a contact URI for the QuickContactBadge. + */ + final Uri contactUri = Contacts.getLookupUri( + cursor.getLong(mIdIndex), + cursor.getString(mLookupKeyIndex)); + holder.quickcontact.assignContactUri(contactUri); + String photoData = cursor.getString(mPhotoDataIndex); + /* + * Decodes the thumbnail file to a Bitmap. + * The method loadContactPhotoThumbnail() is defined + * in the section "Set the Contact URI and Thumbnail" + */ + Bitmap thumbnailBitmap = + loadContactPhotoThumbnail(photoData); + /* + * Sets the image in the QuickContactBadge + * QuickContactBadge inherits from ImageView + */ + holder.quickcontact.setImageBitmap(thumbnailBitmap); + } +</pre> + +<h3>Set up variables</h3> +<p> + In your code, set up variables, including a {@link android.database.Cursor} projection that + includes the necessary columns. +</p> +<p class="note"> + <strong>Note:</strong> The following code snippets use the method + <code>loadContactPhotoThumbnail()</code>, which is defined in the section + <a href="#SetURIThumbnail">Set the Contact URI and Thumbnail</a> +</p> +<p> + For example: +</p> +<pre> +public class ContactsFragment extends Fragment implements + LoaderManager.LoaderCallbacks<Cursor> { +... + // Defines a ListView + private ListView mListView; + // Defines a ContactsAdapter + private ContactsAdapter mAdapter; + ... + // Defines a Cursor to contain the retrieved data + private Cursor mCursor; + /* + * Defines a projection based on platform version. This ensures + * that you retrieve the correct columns. + */ + private static final String[] PROJECTION = + { + Contacts._ID, + Contacts.LOOKUP_KEY, + (Build.VERSION.SDK_INT >= + Build.VERSION_CODES.HONEYCOMB) ? + Contacts.DISPLAY_NAME_PRIMARY : + Contacts.DISPLAY_NAME + (Build.VERSION.SDK_INT >= + Build.VERSION_CODES.HONEYCOMB) ? + Contacts.PHOTO_THUMBNAIL_ID : + /* + * Although it's not necessary to include the + * column twice, this keeps the number of + * columns the same regardless of version + */ + Contacts_ID + ... + }; + /* + * As a shortcut, defines constants for the + * column indexes in the Cursor. The index is + * 0-based and always matches the column order + * in the projection. + */ + // Column index of the _ID column + private int mIdIndex = 0; + // Column index of the LOOKUP_KEY column + private int mLookupKeyIndex = 1; + // Column index of the display name column + private int mDisplayNameIndex = 3; + /* + * Column index of the photo data column. + * It's PHOTO_THUMBNAIL_URI for Honeycomb and later, + * and _ID for previous versions. + */ + private int mPhotoDataIndex = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? + 3 : + 0; + ... +</pre> +<h3>Set up the ListView</h3> +<p> + In {@link android.support.v4.app.Fragment#onCreate Fragment.onCreate()}, instantiate the custom + cursor adapter and get a handle to the {@link android.widget.ListView}: +</p> +<pre> + @Override + public void onCreate(Bundle savedInstanceState) { + ... + /* + * Instantiates the subclass of + * CursorAdapter + */ + ContactsAdapter mContactsAdapter = + new ContactsAdapter(getActivity()); + /* + * Gets a handle to the ListView in the file + * contact_list_layout.xml + */ + mListView = (ListView) findViewById(R.layout.contact_list_layout); + ... + } + ... +</pre> +<p> + In {@link android.support.v4.app.Fragment#onActivityCreated onActivityCreated()}, bind the + <code>ContactsAdapter</code> to the {@link android.widget.ListView}: +</p> +<pre> + @Override + public void onActivityCreated(Bundle savedInstanceState) { + ... + // Sets up the adapter for the ListView + mListView.setAdapter(mAdapter); + ... + } + ... +</pre> +<p> + When you get back a {@link android.database.Cursor} containing the contacts data, usually in + {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onLoadFinished onLoadFinished()}, + call {@link android.support.v4.widget.CursorAdapter#swapCursor swapCursor()} to move the + {@link android.database.Cursor} data to the {@link android.widget.ListView}. This displays the + {@link android.widget.QuickContactBadge} for each entry in the list of contacts: +</p> +<pre> + public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { + // When the loader has completed, swap the cursor into the adapter. + mContactsAdapter.swapCursor(cursor); + } +</pre> +<p> + When you bind a {@link android.database.Cursor} to a + {@link android.widget.ListView} with a {@link android.support.v4.widget.CursorAdapter} + (or subclass), and you use a {@link android.support.v4.content.CursorLoader} to load the + {@link android.database.Cursor}, always clear references to the {@link android.database.Cursor} + in your implementation of + {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onLoaderReset onLoaderReset()}. + For example: +</p> +<pre> + @Override + public void onLoaderReset(Loader<Cursor> loader) { + // Removes remaining reference to the previous Cursor + mContactsAdapter.swapCursor(null); + } +</pre> diff --git a/docs/html/training/contacts-provider/index.jd b/docs/html/training/contacts-provider/index.jd new file mode 100644 index 0000000..f380d95 --- /dev/null +++ b/docs/html/training/contacts-provider/index.jd @@ -0,0 +1,97 @@ +page.title=Accessing Contacts Data + +trainingnavtop=true +startpage=true + +@jd:body + +<div id="tb-wrapper"> +<div id="tb"> + +<!-- Required platform, tools, add-ons, devices, knowledge, etc. --> +<h2>Dependencies and prerequisites</h2> +<ul> + <li>Android 2.0 (API Level 5) or higher</li> + <li>Experience in using {@link android.content.Intent} objects</li> + <li>Experience in using content providers</li> +</ul> + +<!-- related docs (NOT javadocs) --> +<h2>You should also read</h2> +<ul> + <li> + <a href="{@docRoot}guide/topics/providers/content-provider-basics.html"> + Content Provider Basics</a> + </li> + <li> + <a href="{@docRoot}guide/topics/providers/contacts-provider.html"> + Contacts Provider</a> + </li> +</ul> + +<h2>Try it out</h2> + +<div class="download-box"> + <a href="http://developer.android.com/shareables/training/ContactsList.zip" class="button"> + Download the sample + </a> + <p class="filename">ContactsList.zip</p> +</div> + +</div> +</div> + +<p> + The <a href="{@docRoot}guide/topics/providers/contacts-provider.html">Contacts Provider</a> is + the central repository of the user's contacts information, including data from contacts apps and + social networking apps. In your apps, you can access Contacts Provider information directly by + calling {@link android.content.ContentResolver} methods or by sending intents to a contacts app. +</p> +<p> + This class focuses on retrieving lists of contacts, displaying the details for a particular + contact, and modifying contacts using intents. The basic techniques described + here can be extended to perform more complex tasks. In addition, this class helps you + understand the overall structure and operation of the + <a href="{@docRoot}guide/topics/providers/contacts-provider.html">Contacts Provider</a>. +</p> +<h2>Lessons</h2> + +<dl> + <dt> + <b><a href="retrieve-names.html">Retrieving a List of Contacts</a></b> + </dt> + <dd> + Learn how to retrieve a list of contacts for which the data matches all or part of a search + string, using the following techniques: + <ul> + <li>Match by contact name</li> + <li>Match any type of contact data</li> + <li>Match a specific type of contact data, such as a phone number</li> + </ul> + </dd> + <dt> + <b><a href="retrieve-details.html">Retrieving Details for a Contact</a></b> + </dt> + <dd> + Learn how to retrieve the details for a single contact. A contact's details are data + such as phone numbers and email addresses. You can retrieve all details, or you can + retrieve details of a specific type, such as all email addresses. + </dd> + <dt> + <b><a href="modify-data.html">Modifying Contacts Using Intents</a></b> + </dt> + <dd> + Learn how to modify a contact by sending an intent to the People app. + </dd> + <dt> + <b> + <a href="display-contact-badge.html">Displaying the Quick Contact Badge</a> + </b> + </dt> + <dd> + Learn how to display the {@link android.widget.QuickContactBadge} widget. When the user + clicks the contact badge widget, a dialog opens that displays the contact's details and + action buttons for apps that can handle the details. For example, if the contact has an + email address, the dialog displays an action button for the default email app. + </dd> +</dl> diff --git a/docs/html/training/contacts-provider/modify-data.jd b/docs/html/training/contacts-provider/modify-data.jd new file mode 100644 index 0000000..64853ef --- /dev/null +++ b/docs/html/training/contacts-provider/modify-data.jd @@ -0,0 +1,305 @@ +page.title=Modifying Contacts Using Intents +trainingnavtop=true +@jd:body +<div id="tb-wrapper"> +<div id="tb"> + +<!-- table of contents --> +<h2>This lesson teaches you to</h2> +<ol> + <li><a href="#InsertContact">Insert a New Contact Using an Intent</a></li> + <li><a href="#EditContact">Edit an Existing Contact Using an Intent</a></li> + <li><a href="#InsertEdit">Let Users Choose to Insert or Edit Using an Intent</a> +</ol> +<h2>You should also read</h2> +<ul> + <li> + <a href="{@docRoot}guide/topics/providers/content-provider-basics.html"> + Content Provider Basics + </a> + </li> + <li> + <a href="{@docRoot}guide/topics/providers/contacts-provider.html"> + Contacts Provider + </a> + </li> + <li> + <a href="{@docRoot}guide/components/intents-filters.html">Intents and Intent Filters</a> + </li> +</ul> + +<h2>Try it out</h2> + +<div class="download-box"> + <a href="http://developer.android.com/shareables/training/ContactsList.zip" class="button"> + Download the sample + </a> + <p class="filename">ContactsList.zip</p> +</div> + +</div> +</div> +<p> + This lesson shows you how to use an {@link android.content.Intent} to insert a new contact or + modify a contact's data. Instead of accessing the Contacts Provider directly, an + {@link android.content.Intent} starts the contacts app, which runs the appropriate + {@link android.app.Activity}. For the modification actions described in this lesson, + if you send extended data in the {@link android.content.Intent} it's entered into the UI of the + {@link android.app.Activity} that is started. +</p> +<p> + Using an {@link android.content.Intent} to insert or update a single contact is the preferred + way of modifying the Contacts Provider, for the following reasons: +</p> +<ul> + <li>It saves you the time and and effort of developing your own UI and code.</li> + <li> + It avoids introducing errors caused by modifications that don't follow the + Contacts Provider's rules. + </li> + <li> + It reduces the number of permissions you need to request. Your app doesn't need permission + to write to the Contacts Provider, because it delegates modifications to the contacts app, + which already has that permission. + </li> +</ul> +<h2 id="InsertContact">Insert a New Contact Using an Intent</h2> +<p> + You often want to allow the user to insert a new contact when your app receives new data. For + example, a restaurant review app can allow users to add the restaurant as a contact as they're + reviewing it. To do this using an intent, create the intent using as much data as you have + available, and then send the intent to the contacts app. +</p> +<p> + Inserting a contact using the contacts app inserts a new <em>raw</em> contact into the Contacts + Provider's {@link android.provider.ContactsContract.RawContacts} table. If necessary, + the contacts app prompts users for the account type and account to use when creating the raw + contact. The contacts app also notifies users if the raw contact already exists. Users then have + option of canceling the insertion, in which case no contact is created. To learn + more about raw contacts, see the + <a href="{@docRoot}guide/topics/providers/contacts-provider.html">Contacts Provider</a> + API guide. +</p> + +<h3>Create an Intent</h3> +<p> + To start, create a new {@link android.content.Intent} object with the action + {@link android.provider.ContactsContract.Intents.Insert#ACTION Intents.Insert.ACTION}. + Set the MIME type to {@link android.provider.ContactsContract.RawContacts#CONTENT_TYPE + RawContacts.CONTENT_TYPE}. For example: +</p> +<pre> +... +// Creates a new Intent to insert a contact +Intent intent = new Intent(Intents.Insert.ACTION); +// Sets the MIME type to match the Contacts Provider +intent.setType(ContactsContract.RawContacts.CONTENT_TYPE); +</pre> +<p> + If you already have details for the contact, such as a phone number or email address, you can + insert them into the intent as extended data. For a key value, use the appropriate constant from + {@link android.provider.ContactsContract.Intents.Insert Intents.Insert}. The contacts app + displays the data in its insert screen, allowing users to make further edits and additions. +</p> +<pre> +/* Assumes EditText fields in your UI contain an email address + * and a phone number. + * + */ +private EditText mEmailAddress = (EditText) findViewById(R.id.email); +private EditText mPhoneNumber = (EditText) findViewById(R.id.phone); +... +/* + * Inserts new data into the Intent. This data is passed to the + * contacts app's Insert screen + */ +// Inserts an email address +intent.putExtra(Intents.Insert.EMAIL, mEmailAddress.getText()) +/* + * In this example, sets the email type to be a work email. + * You can set other email types as necessary. + */ + .putExtra(Intents.Insert.EMAIL_TYPE, CommonDataKinds.Email.TYPE_WORK) +// Inserts a phone number + .putExtra(Intents.Insert.PHONE, mPhoneNumber.getText()) +/* + * In this example, sets the phone type to be a work phone. + * You can set other phone types as necessary. + */ + .putExtra(Intents.Insert.PHONE_TYPE, Phone.TYPE_WORK); + +</pre> +<p> + Once you've created the {@link android.content.Intent}, send it by calling + {@link android.support.v4.app.Fragment#startActivity startActivity()}. +</p> +<pre> + /* Sends the Intent + */ + startActivity(intent); +</pre> +<p> + This call opens a screen in the contacts app that allows users to enter a new contact. The + account type and account name for the contact is listed at the top of the screen. Once users + enter the data and click <i>Done</i>, the contacts app's contact list appears. Users return to + your app by clicking <i>Back</i>. +</p> +<h2 id="EditContact">Edit an Existing Contact Using an Intent</h2> +<p> + Editing an existing contact using an {@link android.content.Intent} is useful if the user + has already chosen a contact of interest. For example, an app that finds contacts that have + postal addresses but lack a postal code could give users the option of looking up the code and + then adding it to the contact. +</p> +<p> + To edit an existing contact using an intent, use a procedure similar to + inserting a contact. Create an intent as described in the section + <a href="#InsertContact">Insert a New Contact Using an Intent</a>, but add the contact's + {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI + Contacts.CONTENT_LOOKUP_URI} and the MIME type + {@link android.provider.ContactsContract.Contacts#CONTENT_ITEM_TYPE + Contacts.CONTENT_ITEM_TYPE} to the intent. If you want to edit the contact with details you + already have, you can put them in the intent's extended data. Notice that some + name columns can't be edited using an intent; these columns are listed in the summary + section of the API reference for the class {@link android.provider.ContactsContract.Contacts} + under the heading "Update". +</p> +<p> + Finally, send the intent. In response, the contacts app displays an edit screen. When the user + finishes editing and saves the edits, the contacts app displays a contact list. When the user + clicks <i>Back</i>, your app is displayed. +</p> +<div class="sidebox-wrapper"> +<div class="sidebox"> + <h2>Contacts Lookup Key</h2> + <p> + A contact's {@link android.provider.ContactsContract.ContactsColumns#LOOKUP_KEY} value is + the identifier that you should use to retrieve a contact. It remains constant, + even if the provider changes the contact's row ID to handle internal operations. + </p> +</div> +</div> +<h3>Create the Intent</h3> +<p> + To edit a contact, call {@link android.content.Intent#Intent Intent(action)} to + create an intent with the action {@link android.content.Intent#ACTION_EDIT}. Call + {@link android.content.Intent#setDataAndType setDataAndType()} to set the data value for the + intent to the contact's {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI + Contacts.CONTENT_LOOKUP_URI} and the MIME type to + {@link android.provider.ContactsContract.Contacts#CONTENT_ITEM_TYPE + Contacts.CONTENT_ITEM_TYPE} MIME type; because a call to + {@link android.content.Intent#setType setType()} overwrites the current data value for the + {@link android.content.Intent}, you must set the data and the MIME type at the same time. +</p> +<p> + To get a contact's {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI + Contacts.CONTENT_LOOKUP_URI}, call + {@link android.provider.ContactsContract.Contacts#getLookupUri + Contacts.getLookupUri(id, lookupkey)} with the contact's + {@link android.provider.ContactsContract.Contacts#_ID Contacts._ID} and + {@link android.provider.ContactsContract.Contacts#LOOKUP_KEY Contacts.LOOKUP_KEY} values as + arguments. +</p> +<p> + The following snippet shows you how to create an intent: +</p> +<pre> + // The Cursor that contains the Contact row + public Cursor mCursor; + // The index of the lookup key column in the cursor + public int mLookupKeyIndex; + // The index of the contact's _ID value + public int mIdIndex; + // The lookup key from the Cursor + public String mCurrentLookupKey; + // The _ID value from the Cursor + public long mCurrentId; + // A content URI pointing to the contact + Uri mSelectedContactUri; + ... + /* + * Once the user has selected a contact to edit, + * this gets the contact's lookup key and _ID values from the + * cursor and creates the necessary URI. + */ + // Gets the lookup key column index + mLookupKeyIndex = mCursor.getColumnIndex(Contacts.LOOKUP_KEY); + // Gets the lookup key value + mCurrentLookupKey = mCursor.getString(mLookupKeyIndex); + // Gets the _ID column index + mIdIndex = mCursor.getColumnIndex(Contacts._ID); + mCurrentId = mCursor.getLong(mIdIndex); + mSelectedContactUri = + Contacts.getLookupUri(mCurrentId, mCurrentLookupKey); + ... + // Creates a new Intent to edit a contact + Intent editIntent = new Intent(Intent.ACTION_EDIT); + /* + * Sets the contact URI to edit, and the data type that the + * Intent must match + */ + editIntent.setDataAndType(mSelectedContactUri,Contacts.CONTENT_ITEM_TYPE); +</pre> +<h3>Add the navigation flag</h3> +<p> + In Android 4.0 (API version 14) and later, a problem in the contacts app causes incorrect + navigation. When your app sends an edit intent to the contacts app, and users edit and save a + contact, when they click <i>Back</i> they see the contacts list screen. To navigate back to + your app, they have to click <i>Recents</i> and choose your app. +</p> +<p> + To work around this problem in Android 4.0.3 (API version 15) and later, add the extended + data key {@code finishActivityOnSaveCompleted} to the intent, with a value of {@code true}. + Android versions prior to Android 4.0 accept this key, but it has no effect. To set the + extended data, do the following: +</p> +<pre> + // Sets the special extended data for navigation + editIntent.putExtra("finishActivityOnSaveCompleted", true); +</pre> +<h3>Add other extended data</h3> +<p> + To add additional extended data to the {@link android.content.Intent}, call + {@link android.content.Intent#putExtra putExtra()} as desired. + You can add extended data for common contact fields by using the key values specified in + {@link android.provider.ContactsContract.Intents.Insert Intents.Insert}. Remember that some + columns in the {@link android.provider.ContactsContract.Contacts} table can't be modified. + These columns are listed in the summary section of the API reference for the class + {@link android.provider.ContactsContract.Contacts} under the heading "Update". +</p> + +<h3>Send the Intent</h3> +<p> + Finally, send the intent you've constructed. For example: +</p> +<pre> + // Sends the Intent + startActivity(editIntent); +</pre> +<h2 id="InsertEdit">Let Users Choose to Insert or Edit Using an Intent</h2> +<p> + You can allow users to choose whether to insert a contact or edit an existing one by sending + an {@link android.content.Intent} with the action + {@link android.content.Intent#ACTION_INSERT_OR_EDIT}. For example, an email client app could + allow users to add an incoming email address to a new contact, or add it as an additional + address for an existing contact. Set the MIME type for this intent to + {@link android.provider.ContactsContract.Contacts#CONTENT_ITEM_TYPE Contacts.CONTENT_ITEM_TYPE}, + but don't set the data URI. +</p> +<p> + When you send this intent, the contacts app displays a list of contacts. + Users can either insert a new contact or pick an existing contact and edit it. + Any extended data fields you add to the intent populates the screen that appears. You can use + any of the key values specified in {@link android.provider.ContactsContract.Intents.Insert + Intents.Insert}. The following code snippet shows how to construct and send the intent: +</p> +<pre> + // Creates a new Intent to insert or edit a contact + Intent intentInsertEdit = new Intent(Intent.ACTION_INSERT_OR_EDIT); + // Sets the MIME type + intentInsertEdit.setType(Contacts.CONTENT_ITEM_TYPE); + // Add code here to insert extended data, if desired + ... + // Sends the Intent with an request ID + startActivity(intentInsertEdit); +</pre> diff --git a/docs/html/training/contacts-provider/retrieve-details.jd b/docs/html/training/contacts-provider/retrieve-details.jd new file mode 100644 index 0000000..0de3b67 --- /dev/null +++ b/docs/html/training/contacts-provider/retrieve-details.jd @@ -0,0 +1,378 @@ +page.title=Retrieving Details for a Contact + +trainingnavtop=true +@jd:body + +<div id="tb-wrapper"> +<div id="tb"> + +<!-- table of contents --> +<h2>This lesson teaches you to</h2> +<ol> + <li><a href="#RetrieveAll">Retrieve All Details for a Contact</a></li> + <li><a href="#RetrieveSpecific">Retrieve Specific Details for a Contact</a></li> +</ol> + +<!-- other docs (NOT javadocs) --> +<h2>You should also read</h2> +<ul> + <li> + <a href="{@docRoot}guide/topics/providers/content-provider-basics.html"> + Content Provider Basics</a> + </li> + <li> + <a href="{@docRoot}guide/topics/providers/contacts-provider.html"> + Contacts Provider</a> + </li> + <li> + <a href="{@docRoot}guide/components/loaders.html">Loaders</a> +</ul> + +<h2>Try it out</h2> + +<div class="download-box"> + <a href="http://developer.android.com/shareables/training/ContactsList.zip" class="button"> + Download the sample + </a> + <p class="filename">ContactsList.zip</p> +</div> + +</div> +</div> +<p> + This lesson shows how to retrieve detail data for a contact, such as email addresses, phone + numbers, and so forth. It's the details that users are looking for when they retrieve a contact. + You can give them all the details for a contact, or only display details of a particular type, + such as email addresses. +</p> +<p> + The steps in this lesson assume that you already have a + {@link android.provider.ContactsContract.Contacts} row for a contact the user has picked. + The <a href="retrieve-names.html">Retrieving Contact Names</a> lesson shows how to + retrieve a list of contacts. +</p> +<h2 id="RetrieveAll">Retrieve All Details for a Contact</h2> +<p> + To retrieve all the details for a contact, search the + {@link android.provider.ContactsContract.Data} table for any rows that contain the contact's + {@link android.provider.ContactsContract.Data#LOOKUP_KEY}. This column is available in + the {@link android.provider.ContactsContract.Data} table, because the Contacts + Provider makes an implicit join between the {@link android.provider.ContactsContract.Contacts} + table and the {@link android.provider.ContactsContract.Data} table. The + {@link android.provider.ContactsContract.Contacts#LOOKUP_KEY} column is described + in more detail in the <a href="retrieve-names.html">Retrieving Contact Names</a> lesson. +</p> +<p class="note"> + <strong>Note:</strong> Retrieving all the details for a contact reduces the performance of a + device, because it needs to retrieve all of the columns in the + {@link android.provider.ContactsContract.Data} table. Consider the performance impact before + you use this technique. +</p> +<h3>Request permissions</h3> +<p> + To read from the Contacts Provider, your app must have + {@link android.Manifest.permission#READ_CONTACTS READ_CONTACTS} permission. + To request this permission, add the following child element of + <code><a href="{@docRoot}guide/topics/manifest/manifest-element.html"> + <manifest></a></code> to your manifest file: +</p> +<pre> + <uses-permission android:name="android.permission.READ_CONTACTS" /> +</pre> +<h3>Set up a projection</h3> +<p> + Depending on the data type a row contains, it may use only a few columns or many. In addition, + the data is in different columns depending on the data type. + To ensure you get all the possible columns for all possible data types, you need to add all the + column names to your projection. Always retrieve + {@link android.provider.ContactsContract.Data#_ID Data._ID} if you're binding the result + {@link android.database.Cursor} to a {@link android.widget.ListView}; otherwise, the binding + won't work. Also retrieve {@link android.provider.ContactsContract.Data#MIMETYPE Data.MIMETYPE} + so you can identify the data type of each row you retrieve. For example: +</p> +<pre> + private static final String PROJECTION = + { + Data._ID, + Data.MIMETYPE, + Data.DATA1, + Data.DATA2, + Data.DATA3, + Data.DATA4, + Data.DATA5, + Data.DATA6, + Data.DATA7, + Data.DATA8, + Data.DATA9, + Data.DATA10, + Data.DATA11, + Data.DATA12, + Data.DATA13, + Data.DATA14, + Data.DATA15 + }; +</pre> +<p> + This projection retrieves all the columns for a row in the + {@link android.provider.ContactsContract.Data} table, using the column names defined in + the {@link android.provider.ContactsContract.Data} class. +</p> +<p> + Optionally, you can also use any other column constants defined in or inherited by the + {@link android.provider.ContactsContract.Data} class. Notice, however, that the columns + {@link android.provider.ContactsContract.DataColumns#SYNC1} through + {@link android.provider.ContactsContract.DataColumns#SYNC4} are meant to be used by sync + adapters, so their data is not useful. +</p> +<h3>Define the selection criteria</h3> +<p> + Define a constant for your selection clause, an array to hold selection arguments, and a + variable to hold the selection value. Use + the {@link android.provider.ContactsContract.Contacts#LOOKUP_KEY Contacts.LOOKUP_KEY} column to + find the contact. For example: +</p> +<pre> + // Defines the selection clause + private static final String SELECTION = Data.LOOKUP_KEY + " = ?"; + // Defines the array to hold the search criteria + private String[] mSelectionArgs = { "" }; + /* + * Defines a variable to contain the selection value. Once you + * have the Cursor from the Contacts table, and you've selected + * the desired row, move the row's LOOKUP_KEY value into this + * variable. + */ + private String mLookupKey; +</pre> +<p> + Using "?" as a placeholder in your selection text expression ensures that the resulting search + is generated by binding rather than SQL compilation. This approach eliminates the + possibility of malicious SQL injection. +</p> +<h3>Define the sort order</h3> +<p> + Define the sort order you want in the resulting {@link android.database.Cursor}. To + keep all rows for a particular data type together, sort by + {@link android.provider.ContactsContract.Data#MIMETYPE Data.MIMETYPE}. This query argument + groups all email rows together, all phone rows together, and so forth. For example: +</p> +<pre> + /* + * Defines a string that specifies a sort order of MIME type + */ + private static final String SORT_ORDER = Data.MIMETYPE; +</pre> +<p class="note"> + <strong>Note:</strong> Some data types don't use a subtype, so you can't sort on subtype. + Instead, you have to iterate through the returned {@link android.database.Cursor}, + determine the data type of the current row, and store data for rows that use a subtype. When + you finish reading the cursor, you can then sort each data type by subtype and display the + results. +</p> +<h3>Initialize the Loader</h3> +<p> + Always do retrievals from the Contacts Provider (and all other content providers) in a + background thread. Use the Loader framework defined by the + {@link android.support.v4.app.LoaderManager} class and the + {@link android.support.v4.app.LoaderManager.LoaderCallbacks} interface to do background + retrievals. +</p> +<p> + When you're ready to retrieve the rows, initialize the loader framework by + calling {@link android.support.v4.app.LoaderManager#initLoader initLoader()}. Pass an + integer identifier to the method; this identifier is passed to + {@link android.support.v4.app.LoaderManager.LoaderCallbacks} methods. The identifier helps you + use multiple loaders in an app by allowing you to differentiate between them. +</p> +<p> + The following snippet shows how to initialize the loader framework: +</p> +<pre> +public class DetailsFragment extends Fragment implements + LoaderManager.LoaderCallbacks<Cursor> { + ... + // Defines a constant that identifies the loader + DETAILS_QUERY_ID = 0; + ... + /* + * Invoked when the parent Activity is instantiated + * and the Fragment's UI is ready. Put final initialization + * steps here. + */ + @Override + onActivityCreated(Bundle savedInstanceState) { + ... + // Initializes the loader framework + getLoaderManager().initLoader(DETAILS_QUERY_ID, null, this); +</pre> +<h3>Implement onCreateLoader()</h3> +<p> + Implement the {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader + onCreateLoader()} method, which is called by the loader framework immediately after you call + {@link android.support.v4.app.LoaderManager#initLoader initLoader()}. Return a + {@link android.support.v4.content.CursorLoader} from this method. Since you're searching + the {@link android.provider.ContactsContract.Data} table, use the constant + {@link android.provider.ContactsContract.Data#CONTENT_URI Data.CONTENT_URI} as the content URI. + For example: +</p> +<pre> + @Override + public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) { + // Choose the proper action + switch (loaderId) { + case DETAILS_QUERY_ID: + // Assigns the selection parameter + mSelectionArgs[0] = mLookupKey; + // Starts the query + CursorLoader mLoader = + new CursorLoader( + getActivity(), + Data.CONTENT_URI, + PROJECTION, + SELECTION, + mSelectionArgs, + SORT_ORDER + ); + ... + } +</pre> +<h3>Implement onLoadFinished() and onLoaderReset()</h3> +<p> + Implement the + {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onLoadFinished onLoadFinished()} + method. The loader framework calls + {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onLoadFinished onLoadFinished()} + when the Contacts Provider returns the results of the query. For example: +</p> +<pre> + public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { + switch (loader.getId()) { + case DETAILS_QUERY_ID: + /* + * Process the resulting Cursor here. + */ + } + break; + ... + } + } +</pre> +<p> +<p> + The method {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onLoaderReset + onLoaderReset()} is invoked when the loader framework detects that the data backing the result + {@link android.database.Cursor} has changed. At this point, remove any existing references + to the {@link android.database.Cursor} by setting them to null. If you don't, the loader + framework won't destroy the old {@link android.database.Cursor}, and you'll get a memory + leak. For example: +<pre> + @Override + public void onLoaderReset(Loader<Cursor> loader) { + switch (loader.getId()) { + case DETAILS_QUERY_ID: + /* + * If you have current references to the Cursor, + * remove them here. + */ + } + break; + } +</pre> +<h2 id="RetrieveSpecific">Retrieve Specific Details for a Contact</h2> +<p> + Retrieving a specific data type for a contact, such as all the emails, follows the same pattern + as retrieving all details. These are the only changes you need to make to the code + listed in <a href="#RetrieveAll">Retrieve All Details for a Contact</a>: +</p> +<dl> + <dt> + Projection + </dt> + <dd> + Modify your projection to retrieve the columns that are specific to the + data type. Also modify the projection to use the column name constants defined in the + {@link android.provider.ContactsContract.CommonDataKinds} subclass corresponding to the + data type. + </dd> + <dt> + Selection + </dt> + <dd> + Modify the selection text to search for the + {@link android.provider.ContactsContract.Data#MIMETYPE MIMETYPE} value that's specific to + your data type. + </dd> + <dt> + Sort order + </dt> + <dd> + Since you're only selecting a single detail type, don't group the returned + {@link android.database.Cursor} by {@link android.provider.ContactsContract.Data#MIMETYPE + Data.MIMETYPE}. + </dd> +</dl> +<p> + These modifications are described in the following sections. +</p> +<h3>Define a projection</h3> +<p> + Define the columns you want to retrieve, using the column name constants in the subclass + of {@link android.provider.ContactsContract.CommonDataKinds} for the data type. + If you plan to bind your {@link android.database.Cursor} to a {@link android.widget.ListView}, + be sure to retrieve the <code>_ID</code> column. For example, to retrieve email data, define the + following projection: +</p> +<pre> + private static final String[] PROJECTION = + { + Email._ID, + Email.ADDRESS, + Email.TYPE, + Email.LABEL + }; +</pre> +<p> + Notice that this projection uses the column names defined in the class + {@link android.provider.ContactsContract.CommonDataKinds.Email}, instead of the column names + defined in the class {@link android.provider.ContactsContract.Data}. Using the email-specific + column names makes the code more readable. +</p> +<p> + In the projection, you can also use any of the other columns defined in the + {@link android.provider.ContactsContract.CommonDataKinds} subclass. +</p> +<h3>Define selection criteria</h3> +<p> + Define a search text expression that retrieves rows for a specific contact's + {@link android.provider.ContactsContract.Data#LOOKUP_KEY} and the + {@link android.provider.ContactsContract.Data#MIMETYPE Data.MIMETYPE} of the details you + want. Enclose the {@link android.provider.ContactsContract.Data#MIMETYPE MIMETYPE} value in + single quotes by concatenating a "<code>'</code>" (single-quote) character to the start and end + of the constant; otherwise, the provider interprets the constant as a variable name rather + than as a string value. You don't need to use a placeholder for this value, because you're + using a constant rather than a user-supplied value. For example: +</p> +<pre> + /* + * Defines the selection clause. Search for a lookup key + * and the Email MIME type + */ + private static final String SELECTION = + Data.LOOKUP_KEY + " = ?" + + " AND " + + Data.MIMETYPE + " = " + + "'" + Email.CONTENT_ITEM_TYPE + "'"; + // Defines the array to hold the search criteria + private String[] mSelectionArgs = { "" }; +</pre> +<h3>Define a sort order</h3> +<p> + Define a sort order for the returned {@link android.database.Cursor}. Since you're retrieving a + specific data type, omit the sort on {@link android.provider.ContactsContract.Data#MIMETYPE}. + Instead, if the type of detail data you're searching includes a subtype, sort on it. + For example, for email data you can sort on + {@link android.provider.ContactsContract.CommonDataKinds.Email#TYPE Email.TYPE}: +</p> +<pre> + private static final String SORT_ORDER = Email.TYPE + " ASC "; +</pre> diff --git a/docs/html/training/contacts-provider/retrieve-names.jd b/docs/html/training/contacts-provider/retrieve-names.jd new file mode 100644 index 0000000..b034a6a --- /dev/null +++ b/docs/html/training/contacts-provider/retrieve-names.jd @@ -0,0 +1,815 @@ +page.title=Retrieving a List of Contacts + +trainingnavtop=true +@jd:body + +<div id="tb-wrapper"> +<div id="tb"> + +<!-- table of contents --> +<h2>This lesson teaches you to</h2> +<ol> + <li><a href="#Permissions">Request Permission to Read the Provider</a> + <li><a href="#NameMatch">Match a Contact by Name and List the Results</a></li> + <li><a href="#TypeMatch">Match a Contact By a Specific Type of Data</a></li> + <li><a href="#GeneralMatch">Match a Contact By Any Type of Data</a></li> +</ol> + +<!-- other docs (NOT javadocs) --> +<h2>You should also read</h2> +<ul> + <li> + <a href="{@docRoot}guide/topics/providers/content-provider-basics.html"> + Content Provider Basics</a> + </li> + <li> + <a href="{@docRoot}guide/topics/providers/contacts-provider.html"> + Contacts Provider</a> + </li> + <li> + <a href="{@docRoot}guide/components/loaders.html">Loaders</a> + </li> + <li> + <a href="{@docRoot}guide/topics/search/search-dialog.html">Creating a Search Interface</a> + </li> +</ul> + +<h2>Try it out</h2> + +<div class="download-box"> + <a href="http://developer.android.com/shareables/training/ContactsList.zip" class="button"> + Download the sample + </a> + <p class="filename">ContactsList.zip</p> +</div> + +</div> +</div> +<p> + This lesson shows you how to retrieve a list of contacts whose data matches all or part of a + search string, using the following techniques: +</p> +<dl> + <dt>Match contact names</dt> + <dd> + Retrieve a list of contacts by matching the search string to all or part of the contact + name data. The Contacts Provider allows multiple instances of the same name, so this + technique can return a list of matches. + </dd> + <dt>Match a specific type of data, such as a phone number</dt> + <dd> + Retrieve a list of contacts by matching the search string to a particular type of detail + data such as an email address. For example, this technique allows you to list all of the + contacts whose email address matches the search string. + </dd> + <dt>Match any type of data</dt> + <dd> + Retrieve a list of contacts by matching the search string to any type of detail data, + including name, phone number, street address, email address, and so forth. For example, + this technique allows you to accept any type of data for a search string and then list the + contacts for which the data matches the string. + </dd> +</dl> +<p class="note"> + <strong>Note:</strong> All the examples in this lesson use a + {@link android.support.v4.content.CursorLoader} to retrieve data from the Contacts + Provider. A {@link android.support.v4.content.CursorLoader} runs its query on a + thread that's separate from the UI thread. This ensures that the query doesn't slow down UI + response times and cause a poor user experience. For more information, see the Android + training class <a href="{@docRoot}training/load-data-background/index.html"> + Loading Data in the Background</a>. +</p> +<h2 id="Permissions">Request Permission to Read the Provider</h2> +<p> + To do any type of search of the Contacts Provider, your app must have + {@link android.Manifest.permission#READ_CONTACTS READ_CONTACTS} permission. + To request this, add this +<code><a href="{@docRoot}guide/topics/manifest/uses-permission-element.html"><uses-permission></a></code> + element to your manifest file as a child element of +<code><a href="{@docRoot}guide/topics/manifest/manifest-element.html"><manifest></a></code>: +</p> +<pre> + <uses-permission android:name="android.permission.READ_CONTACTS" /> +</pre> +<h2 id="NameMatch">Match a Contact by Name and List the Results</h2> +<p> + This technique tries to match a search string to the name of a contact or contacts in the + Contact Provider's {@link android.provider.ContactsContract.Contacts} table. You usually want + to display the results in a {@link android.widget.ListView}, to allow the user to choose among + the matched contacts. +</p> +<h3 id="DefineListView">Define ListView and item layouts</h3> +<p> + To display the search results in a {@link android.widget.ListView}, you need a main layout file + that defines the entire UI including the {@link android.widget.ListView}, and an item layout + file that defines one line of the {@link android.widget.ListView}. For example, you can define + the main layout file <code>res/layout/contacts_list_view.xml</code> that contains the + following XML: +</p> +<pre> +<?xml version="1.0" encoding="utf-8"?> +<ListView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/list" + android:layout_width="match_parent" + android:layout_height="match_parent"/> +</pre> +<p> + This XML uses the built-in Android {@link android.widget.ListView} widget + {@link android.R.id#list android:id/list}. +</p> +<p> + Define the item layout file <code>contacts_list_item.xml</code> with the following XML: +</p> +<pre> +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clickable="true"/> +</pre> +<p> + This XML uses the built-in Android {@link android.widget.TextView} widget + {@link android.R.id#text1 android:text1}. +</p> +<p class="note"> + <strong>Note:</strong> This lesson doesn't describe the UI for getting a search string from the + user, because you may want to get the string indirectly. For example, you can give the user + an option to search for contacts whose name matches a string in an incoming text message. +</p> +<p> + The two layout files you've written define a user interface that shows a + {@link android.widget.ListView}. The next step is to write code that uses this UI to display a + list of contacts. +</p> +<h3 id="Fragment">Define a Fragment that displays the list of contacts</h3> +<p> + To display the list of contacts, start by defining a {@link android.support.v4.app.Fragment} + that's loaded by an {@link android.app.Activity}. Using a + {@link android.support.v4.app.Fragment} is a more flexible technique, because you can use + one {@link android.support.v4.app.Fragment} to display the list and a second + {@link android.support.v4.app.Fragment} to display the details for a contact that the user + chooses from the list. Using this approach, you can combine one of the techniques presented in + this lesson with one from the lesson <a href="retrieve-details.html"> + Retrieving Details for a Contact</a>. +</p> +<p> + To learn how to use one or more {@link android.support.v4.app.Fragment} objects from an + an {@link android.app.Activity}, read the training class + <a href="{@docRoot}training/basics/fragments/index.html"> + Building a Dynamic UI with Fragments</a>. +</p> +<p> + To help you write queries against the Contacts Provider, the Android framework provides a + contracts class called {@link android.provider.ContactsContract}, which defines useful + constants and methods for accessing the provider. When you use this class, you don't have to + define your own constants for content URIs, table names, or columns. To use this class, + include the following statement: +</p> +<pre> +import android.provider.ContactsContract; +</pre> +<p> + Since the code uses a {@link android.support.v4.content.CursorLoader} to retrieve data + from the provider, you must specify that it implements the loader interface + {@link android.support.v4.app.LoaderManager.LoaderCallbacks}. Also, to help detect which contact + the user selects from the list of search results, implement the adapter interface + {@link android.widget.AdapterView.OnItemClickListener}. For example: +</p> +<pre> +... +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.widget.AdapterView; +... +public class ContactsFragment extends Fragment implements + LoaderManager.LoaderCallbacks<Cursor>, + AdapterView.OnItemClickListener { +</pre> +<h3 id="DefineVariables">Define global variables</h3> +<p> + Define global variables that are used in other parts of the code: +</p> +<pre> + ... + /* + * Defines an array that contains column names to move from + * the Cursor to the ListView. + */ + @SuppressLint("InlinedApi") + private final static String[] FROM_COLUMNS = { + Build.VERSION.SDK_INT + >= Build.VERSION_CODES.HONEYCOMB ? + Contacts.DISPLAY_NAME_PRIMARY : + Contacts.DISPLAY_NAME + }; + /* + * Defines an array that contains resource ids for the layout views + * that get the Cursor column contents. The id is pre-defined in + * the Android framework, so it is prefaced with "android.R.id" + */ + private final static int[] TO_IDS = { + android.R.id.text1 + }; + // Define global mutable variables + // Define a ListView object + ListView mContactsList; + // Define variables for the contact the user selects + // The contact's _ID value + long mContactId; + // The contact's LOOKUP_KEY + String mContactKey; + // A content URI for the selected contact + Uri mContactUri; + // An adapter that binds the result Cursor to the ListView + private SimpleCursorAdapter mCursorAdapter; + ... +</pre> +<p class="note"> + <strong>Note:</strong> Since + {@link android.provider.ContactsContract.Contacts#DISPLAY_NAME_PRIMARY + Contacts.DISPLAY_NAME_PRIMARY} requires Android 3.0 (API version 11) or later, setting your + app's <code>minSdkVersion</code> to 10 or below generates an Android Lint warning in + Eclipse with ADK. To turn off this warning, add the annotation + <code>@SuppressLint("InlinedApi")</code> before the definition of <code>FROM_COLUMNS</code>. +</p> +<h3 id="InitializeFragment">Initialize the Fragment</h3> +<p> + + Initialize the {@link android.support.v4.app.Fragment}. Add the empty, public constructor + required by the Android system, and inflate the {@link android.support.v4.app.Fragment} object's + UI in the callback method {@link android.support.v4.app.Fragment#onCreateView onCreateView()}. + For example: +</p> +<pre> + // Empty public constructor, required by the system + public ContactsFragment() {} + + // A UI Fragment must inflate its View + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the fragment layout + return inflater.inflate(R.layout.contacts_list_layout, container, false); + } +</pre> +<h3 id="DefineAdapter">Set up the CursorAdapter for the ListView</h3> +<p> + Set up the {@link android.support.v4.widget.SimpleCursorAdapter} that binds the results of the + search to the {@link android.widget.ListView}. To get the {@link android.widget.ListView} object + that displays the contacts, you need to call {@link android.app.Activity#findViewById + Activity.findViewById()} using the parent activity of the + {@link android.support.v4.app.Fragment}. Use the {@link android.content.Context} of the + parent activity when you call {@link android.widget.ListView#setAdapter setAdapter()}. + For example: +</p> +<pre> + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ... + // Gets the ListView from the View list of the parent activity + mContactsList = (ListView) getActivity().findViewById(R.layout.contact_list_view); + // Gets a CursorAdapter + mCursorAdapter = new SimpleCursorAdapter( + getActivity(), + R.layout.contact_list_item, + null, + FROM_COLUMNS, TO_IDS, + 0); + // Sets the adapter for the ListView + mContactsList.setAdapter(mCursorAdapter); + } +</pre> +<h3 id="SetListener">Set the selected contact listener</h3> +<p> + When you display the results of a search, you usually want to allow the user to select a + single contact for further processing. For example, when the user clicks a contact you can + display the contact's address on a map. To provide this feature, you first defined the current + {@link android.support.v4.app.Fragment} as the click listener by specifying that the class + implements {@link android.widget.AdapterView.OnItemClickListener}, as shown in the section + <a href="#Fragment">Define a Fragment that displays the list of contacts</a>. +</p> +<p> + To continue setting up the listener, bind it to the {@link android.widget.ListView} by + calling the method {@link android.widget.ListView#setOnItemClickListener + setOnItemClickListener()} in {@link android.support.v4.app.Fragment#onActivityCreated + onActivityCreated()}. For example: +</p> +<pre> + public void onActivityCreated(Bundle savedInstanceState) { + ... + // Set the item click listener to be the current fragment. + mContactsList.setOnItemClickListener(this); + ... + } +</pre> +<p> + Since you specified that the current {@link android.support.v4.app.Fragment} is the + {@link android.widget.AdapterView.OnItemClickListener OnItemClickListener} for the + {@link android.widget.ListView}, you now need to implement its required method + {@link android.widget.AdapterView.OnItemClickListener#onItemClick onItemClick()}, which + handles the click event. This is described in a succeeding section. +</p> +<h3 id="DefineProjection">Define a projection</h3> +<p> + Define a constant that contains the columns you want to return from your query. Each item in + the {@link android.widget.ListView} displays the contact's display name, + which contains the main form of the contact's name. In Android 3.0 (API version 11) and later, + the name of this column is + {@link android.provider.ContactsContract.Contacts#DISPLAY_NAME_PRIMARY + Contacts.DISPLAY_NAME_PRIMARY}; in versions previous to that, its name is + {@link android.provider.ContactsContract.Contacts#DISPLAY_NAME Contacts.DISPLAY_NAME}. +</p> +<p> + The column {@link android.provider.ContactsContract.Contacts#_ID Contacts._ID} is used by the + {@link android.support.v4.widget.SimpleCursorAdapter} binding process. + {@link android.provider.ContactsContract.Contacts#_ID Contacts._ID} and + {@link android.provider.ContactsContract.Contacts#LOOKUP_KEY} are used together to + construct a content URI for the contact the user selects. +</p> +<pre> +... +@SuppressLint("InlinedApi") +private static final String[] PROJECTION = + { + Contacts._ID, + Contacts.LOOKUP_KEY, + Build.VERSION.SDK_INT + >= Build.VERSION_CODES.HONEYCOMB ? + Contacts.DISPLAY_NAME_PRIMARY : + Contacts.DISPLAY_NAME + + }; +</pre> +<h3 id="DefineConstants">Define constants for the Cursor column indexes</h3> +<p> + To get data from an individual column in a {@link android.database.Cursor}, you need + the column's index within the {@link android.database.Cursor}. You can define constants + for the indexes of the {@link android.database.Cursor} columns, because the indexes are + the same as the order of the column names in your projection. For example: +</p> +<pre> +// The column index for the _ID column +private static final int CONTACT_ID_INDEX = 0; +// The column index for the LOOKUP_KEY column +private static final int LOOKUP_KEY_INDEX = 1; +</pre> +<h3 id="SelectionCriteria">Specify the selection criteria</h3> +<p> + To specify the data you want, create a combination of text expressions and variables + that tell the provider the data columns to search and the values to find. +</p> +<p> + For the text expression, define a constant that lists the search columns. Although this + expression can contain values as well, the preferred practice is to represent the values with + a "?" placeholder. During retrieval, the placeholder is replaced with values from an + array. Using "?" as a placeholder ensures that the search specification is generated by binding + rather than by SQL compilation. This practice eliminates the possibility of malicious SQL + injection. For example: +</p> +<pre> + // Defines the text expression + @SuppressLint("InlinedApi") + private static final String SELECTION = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? + Contacts.DISPLAY_NAME_PRIMARY + " LIKE ?" : + Contacts.DISPLAY_NAME + " LIKE ?"; + // Defines a variable for the search string + private String mSearchString; + // Defines the array to hold values that replace the ? + private String[] mSelectionArgs = { mSearchString }; +</pre> +<h3 id="OnItemClick">Define the onItemClick() method</h3> +<p> + In a previous section, you set the item click listener for the {@link android.widget.ListView}. + Now implement the action for the listener by defining the method + {@link android.widget.AdapterView.OnItemClickListener#onItemClick + AdapterView.OnItemClickListener.onItemClick()}: +</p> +<pre> + @Override + public void onItemClick( + AdapterView<?> parent, View item, int position, long rowID) { + // Get the Cursor + Cursor cursor = parent.getAdapter().getCursor(); + // Move to the selected contact + cursor.moveToPosition(position); + // Get the _ID value + mContactId = getLong(CONTACT_ID_INDEX); + // Get the selected LOOKUP KEY + mContactKey = getString(CONTACT_KEY_INDEX); + // Create the contact's content Uri + mContactUri = Contacts.getLookupUri(mContactId, mContactKey); + /* + * You can use mContactUri as the content URI for retrieving + * the details for a contact. + */ + } +</pre> +<h3 id="InitializeLoader">Initialize the loader</h3> +<p> + Since you're using a {@link android.support.v4.content.CursorLoader} to retrieve data, + you must initialize the background thread and other variables that control asynchronous + retrieval. Do the initialization in + {@link android.support.v4.app.Fragment#onActivityCreated onActivityCreated()}, which + is invoked immediately before the {@link android.support.v4.app.Fragment} UI appears, as + shown in the following example: +</p> +<pre> +public class ContactsFragment extends Fragment implements + LoaderManager.LoaderCallbacks<Cursor> { + ... + // Called just before the Fragment displays its UI + @Override + public void onActivityCreated(Bundle savedInstanceState) { + // Always call the super method first + super.onActivityCreated(savedInstanceState); + ... + // Initializes the loader + getLoaderManager().initLoader(0, null, this); +</pre> +<h3 id="OnCreateLoader">Implement onCreateLoader()</h3> +<p> + Implement the method + {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader onCreateLoader()}, + which is called by the loader framework immediately after you call + {@link android.support.v4.app.LoaderManager#initLoader initLoader()}. +<p> + In {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader onCreateLoader()}, + set up the search string pattern. To make a string into a pattern, insert "%" + (percent) characters to represent a sequence of zero or more characters, or "_" (underscore) + characters to represent a single character, or both. For example, the pattern "%Jefferson%" + would match both "Thomas Jefferson" and "Jefferson Davis". +</p> +<p> + Return a new {@link android.support.v4.content.CursorLoader} from the method. For the content + URI, use {@link android.provider.ContactsContract.Contacts#CONTENT_URI Contacts.CONTENT_URI}. + This URI refers to the entire table, as shown in the following example: +</p> +<pre> + ... + @Override + public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) { + /* + * Makes search string into pattern and + * stores it in the selection array + */ + mSelectionArgs[0] = "%" + mSearchString + "%"; + // Starts the query + return new CursorLoader( + getActivity(), + Contacts.CONTENT_URI, + PROJECTION, + SELECTION, + mSelectionArgs, + null + ); + } +</pre> +<h3 id="FinishedReset">Implement onLoadFinished() and onLoaderReset()</h3> +<p> + Implement the + {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onLoadFinished onLoadFinished()} + method. The loader framework calls + {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onLoadFinished onLoadFinished()} + when the Contacts Provider returns the results of the query. In this method, put the + result {@link android.database.Cursor} in the + {@link android.support.v4.widget.SimpleCursorAdapter}. This automatically updates the + {@link android.widget.ListView} with the search results: +</p> +<pre> + public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { + // Put the result Cursor in the adapter for the ListView + mCursorAdapter.swapCursor(cursor); + } +</pre> +<p> + The method {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onLoaderReset + onLoaderReset()} is invoked when the loader framework detects that the + result {@link android.database.Cursor} contains stale data. Delete the + {@link android.support.v4.widget.SimpleCursorAdapter} reference to the existing + {@link android.database.Cursor}. If you don't, the loader framework will not + recycle the {@link android.database.Cursor}, which causes a memory leak. For example: +</p> +<pre> + @Override + public void onLoaderReset(Loader<Cursor> loader) { + // Delete the reference to the existing Cursor + mCursorAdapter.swapCursor(null); + + } +</pre> + +<p> + You now have the key pieces of an app that matches a search string to contact names and returns + the result in a {@link android.widget.ListView}. The user can click a contact name to select it. + This triggers a listener, in which you can work further with the contact's data. For example, + you can retrieve the contact's details. To learn how to do this, continue with the next + lesson, <a href="#retrieve-details.html">Retrieving Details for a Contact</a>. +</p> +<p> + To learn more about search user interfaces, read the API guide + <a href="{@docRoot}guide/topics/search/search-dialog.html">Creating a Search Interface</a>. +</p> +<p> + The remaining sections in this lesson demonstrate other ways of finding contacts in the + Contacts Provider. +</p> +<h2 id="TypeMatch">Match a Contact By a Specific Type of Data</h2> +<p> + This technique allows you to specify the type of data you want to match. Retrieving + by name is a specific example of this type of query, but you can also do it for any of the types + of detail data associated with a contact. For example, you can retrieve contacts that have a + specific postal code; in this case, the search string has to match data stored in a postal code + row. +</p> +<p> + To implement this type of retrieval, first implement the following code, as listed in + previous sections: +</p> +<ul> + <li> + Request Permission to Read the Provider. + </li> + <li> + Define ListView and item layouts. + </li> + <li> + Define a Fragment that displays the list of contacts. + </li> + <li> + Define global variables. + </li> + <li> + Initialize the Fragment. + </li> + <li> + Set up the CursorAdapter for the ListView. + </li> + <li> + Set the selected contact listener. + </li> + <li> + Define constants for the Cursor column indexes. + <p> + Although you're retrieving data from a different table, the order of the columns in + the projection is the same, so you can use the same indexes for the Cursor. + </p> + </li> + <li> + Define the onItemClick() method. + </li> + <li> + Initialize the loader. + </li> + <li> + + Implement onLoadFinished() and onLoaderReset(). + </li> +</ul> +<p> + The following steps show you the additional code you need to match a search string to + a particular type of detail data and display the results. +</p> +<h3>Choose the data type and table</h3> +<p> + To search for a particular type of detail data, you have to know the custom MIME type value + for the data type. Each data type has a unique MIME type + value defined by a constant <code>CONTENT_ITEM_TYPE</code> in the subclass of + {@link android.provider.ContactsContract.CommonDataKinds} associated with the data type. + The subclasses have names that indicate their data type; for example, the subclass for email + data is {@link android.provider.ContactsContract.CommonDataKinds.Email}, and the custom MIME + type for email data is defined by the constant + {@link android.provider.ContactsContract.CommonDataKinds.Email#CONTENT_ITEM_TYPE + Email.CONTENT_ITEM_TYPE}. +</p> +<p> + Use the {@link android.provider.ContactsContract.Data} table for your search. All of the + constants you need for your projection, selection clause, and sort order are defined in or + inherited by this table. +</p> +<h3 id="SpecificProjection">Define a projection</h3> +<p> + To define a projection, choose one or more of the columns defined in + {@link android.provider.ContactsContract.Data} or the classes from which it inherits. The + Contacts Provider does an implicit join between {@link android.provider.ContactsContract.Data} + and other tables before it returns rows. For example: +</p> +<pre> + @SuppressLint("InlinedApi") + private static final String[] PROJECTION = + { + /* + * The detail data row ID. To make a ListView work, + * this column is required. + */ + Data._ID, + // The primary display name + Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? + Data.DISPLAY_NAME_PRIMARY : + Data.DISPLAY_NAME, + // The contact's _ID, to construct a content URI + Data.CONTACT_ID + // The contact's LOOKUP_KEY, to construct a content URI + Data.LOOKUP_KEY (a permanent link to the contact + }; +</pre> +<h3 id="SpecificCriteria">Define search criteria</h3> +<p> + To search for a string within a particular type of data, construct a selection clause from + the following: +</p> +<ul> + <li> + The name of the column that contains your search string. This name varies by data type, + so you need to find the subclass of + {@link android.provider.ContactsContract.CommonDataKinds} that corresponds to the data type + and then choose the column name from that subclass. For example, to search for + email addresses, use the column + {@link android.provider.ContactsContract.CommonDataKinds.Email#ADDRESS Email.ADDRESS}. + </li> + <li> + The search string itself, represented as the "?" character in the selection clause. + </li> + <li> + The name of the column that contains the custom MIME type value. This name is always + {@link android.provider.ContactsContract.Data#MIMETYPE Data.MIMETYPE}. + </li> + <li> + The custom MIME type value for the data type. As described previously, this is the constant + <code>CONTENT_ITEM_TYPE</code> in the + {@link android.provider.ContactsContract.CommonDataKinds} subclass. For example, the MIME + type value for email data is + {@link android.provider.ContactsContract.CommonDataKinds.Email#CONTENT_ITEM_TYPE + Email.CONTENT_ITEM_TYPE}. Enclose the value in single quotes by concatenating a + "<code>'</code>" (single quote) character to the start and end of the constant; otherwise, + the provider interprets the value as a variable name rather than as a string value. + You don't need to use a placeholder for this value, because you're using a constant + rather than a user-supplied value. + </li> +</ul> +<p> + For example: +</p> +<pre> + /* + * Constructs search criteria from the search string + * and email MIME type + */ + private static final String SELECTION = + /* + * Searches for an email address + * that matches the search string + */ + Email.ADDRESS + " LIKE ? " + "AND " + + /* + * Searches for a MIME type that matches + * the value of the constant + * Email.CONTENT_ITEM_TYPE. Note the + * single quotes surrounding Email.CONTENT_ITEM_TYPE. + */ + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"; +</pre> +<p> + Next, define variables to contain the selection argument: +</p> +<pre> + String mSearchString; + String[] mSelectionArgs = { "" }; +</pre> +<h3 id="SpecificLoader">Implement onCreateLoader()</h3> +<p> + Now that you've specified the data you want and how to find it, define a query in your + implementation of {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader + onCreateLoader()}. Return a new {@link android.support.v4.content.CursorLoader} from this + method, using your projection, selection text expression, and selection array as + arguments. For a content URI, use + {@link android.provider.ContactsContract.Data#CONTENT_URI Data.CONTENT_URI}. For example: +</p> +<pre> + @Override + public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) { + // OPTIONAL: Makes search string into pattern + mSearchString = "%" + mSearchString + "%"; + // Puts the search string into the selection criteria + mSelectionArgs[0] = mSearchString; + // Starts the query + return new CursorLoader( + getActivity(), + Data.CONTENT_URI, + PROJECTION, + SELECTION, + mSelectionArgs, + null + ); + } +</pre> +<p> + These code snippets are the basis of a simple reverse lookup based on a specific type of detail + data. This is the best technique to use if your app focuses on a particular type of data, such + as emails, and you want allow users to get the names associated with a piece of data. +</p> +<h2 id="GeneralMatch">Match a Contact By Any Type of Data</h2> +<p> + Retrieving a contact based on any type of data returns contacts if any of their data matches a + the search string, including name, email address, postal address, phone number, and so forth. + This results in a broad set of search results. For example, if the search string + is "Doe", then searching for any data type returns the contact "John Doe"; it also returns + contacts who live on "Doe Street". +</p> +<p> + To implement this type of retrieval, first implement the following code, as listed in + previous sections: +</p> +<ul> + <li> + Request Permission to Read the Provider. + </li> + <li> + Define ListView and item layouts. + </li> + <li> + <li> + Define a Fragment that displays the list of contacts. + </li> + <li> + Define global variables. + </li> + <li> + Initialize the Fragment. + </li> + <li> + Set up the CursorAdapter for the ListView. + </li> + <li> + Set the selected contact listener. + </li> + <li> + Define a projection. + </li> + <li> + Define constants for the Cursor column indexes. + <p> + For this type of retrieval, you're using the same table you used in the section + <a href="#NameMatch">Match a Contact by Name and List the Results</a>. Use the + same column indexes as well. + </p> + </li> + <li> + Define the onItemClick() method. + </li> + <li> + Initialize the loader. + </li> + <li> + + Implement onLoadFinished() and onLoaderReset(). + </li> +</ul> +<p> + The following steps show you the additional code you need to match a search string to + any type of data and display the results. +</p> +<h3 id="NoSelection">Remove selection criteria</h3> +<p> + Don't define the <code>SELECTION</code> constants or the <code>mSelectionArgs</code> variable. + These aren't used in this type of retrieval. +</p> +<h3 id="CreateLoaderAny">Implement onCreateLoader()</h3> +<p> + Implement the {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader + onCreateLoader()} method, returning a new {@link android.support.v4.content.CursorLoader}. + You don't need to convert the search string into a pattern, because the Contacts Provider does + that automatically. Use + {@link android.provider.ContactsContract.Contacts#CONTENT_FILTER_URI + Contacts.CONTENT_FILTER_URI} as the base URI, and append your search string to it by calling + {@link android.net.Uri#withAppendedPath Uri.withAppendedPath()}. Using this URI + automatically triggers searching for any data type, as shown in the following example: +</p> +<pre> + @Override + public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) { + /* + * Appends the search string to the base URI. Always + * encode search strings to ensure they're in proper + * format. + */ + Uri contentUri = Uri.withAppendedPath( + Contacts.CONTENT_FILTER_URI, + Uri.encode(mSearchString)); + // Starts the query + return new CursorLoader( + getActivity(), + contentUri, + PROJECTION, + null, + null, + null + ); + } +</pre> +<p> + These code snippets are the basis of an app that does a broad search of the Contacts Provider. + The technique is useful for apps that want to implement functionality similar to the + People app's contact list screen. +</p> diff --git a/docs/html/training/training_toc.cs b/docs/html/training/training_toc.cs index 985fc44..7a3f2ca 100644 --- a/docs/html/training/training_toc.cs +++ b/docs/html/training/training_toc.cs @@ -484,7 +484,37 @@ </a> </div> <ul> - + <li class="nav-section"> + <div class="nav-section-header"> + <a href="<?cs var:toroot ?>training/contacts-provider/index.html" + description= + "How to use Android's central address book, the Contacts Provider, to + display contacts and their details and modify contact information."> + Accessing Contacts Data</a> + </div> + <ul> + <li> + <a href="<?cs var:toroot ?>training/contacts-provider/retrieve-names.html"> + Retrieving a List of Contacts + </a> + </li> + <li> + <a href="<?cs var:toroot ?>training/contacts-provider/retrieve-details.html"> + Retrieving Details for a Contact + </a> + </li> + <li> + <a href="<?cs var:toroot ?>training/contacts-provider/modify-data.html"> + Modifying Contacts Using Intents + </a> + </li> + <li> + <a href="<?cs var:toroot ?>training/contacts-provider/display-contact-badge.html"> + Displaying the Quick Contact Badge + </a> + </li> + </ul> + </li> <li class="nav-section"> <div class="nav-section-header"> <a href="<?cs var:toroot ?>training/id-auth/index.html" @@ -879,7 +909,7 @@ </ul> </li> </ul> - </li> <!-- end of User Input --> + </li> <!-- end of User Input --> <li class="nav-section"> <div class="nav-section-header"> diff --git a/graphics/java/android/renderscript/Type.java b/graphics/java/android/renderscript/Type.java index 9507030..d7c8255 100644 --- a/graphics/java/android/renderscript/Type.java +++ b/graphics/java/android/renderscript/Type.java @@ -110,6 +110,16 @@ public class Type extends BaseObj { } /** + * Get the YUV format + * + * @hide + * @return int + */ + public int getYuv() { + return mDimYuv; + } + + /** * Return if the Type has a mipmap chain. * * @return boolean diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java index 18f3b1e..38cdb8a 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -165,6 +165,7 @@ public class AudioService extends IAudioService.Stub implements OnFinished { private static final int MSG_PERSIST_SAFE_VOLUME_STATE = 28; private static final int MSG_PROMOTE_RCC = 29; private static final int MSG_BROADCAST_BT_CONNECTION_STATE = 30; + private static final int MSG_UNLOAD_SOUND_EFFECTS = 31; // flags for MSG_PERSIST_VOLUME indicating if current and/or last audible volume should be @@ -1685,6 +1686,12 @@ public class AudioService extends IAudioService.Stub implements OnFinished { private static final String ASSET_FILE_VERSION = "1.0"; private static final String GROUP_TOUCH_SOUNDS = "touch_sounds"; + private static final int SOUND_EFECTS_LOAD_TIMEOUT_MS = 5000; + + class LoadSoundEffectReply { + public int mStatus = 1; + }; + private void loadTouchSoundAssetDefaults() { SOUND_EFFECT_FILES.add("Effect_Tick.ogg"); for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) { @@ -1696,6 +1703,11 @@ public class AudioService extends IAudioService.Stub implements OnFinished { private void loadTouchSoundAssets() { XmlResourceParser parser = null; + // only load assets once. + if (!SOUND_EFFECT_FILES.isEmpty()) { + return; + } + loadTouchSoundAssetDefaults(); try { @@ -1765,14 +1777,12 @@ public class AudioService extends IAudioService.Stub implements OnFinished { /** @see AudioManager#playSoundEffect(int) */ public void playSoundEffect(int effectType) { - sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_NOOP, - effectType, -1, null, 0); + playSoundEffectVolume(effectType, -1.0f); } /** @see AudioManager#playSoundEffect(int, float) */ public void playSoundEffectVolume(int effectType, float volume) { - loadSoundEffects(); - sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_NOOP, + sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_QUEUE, effectType, (int) (volume * 1000), null, 0); } @@ -1781,116 +1791,20 @@ public class AudioService extends IAudioService.Stub implements OnFinished { * This method must be called at first when sound effects are enabled */ public boolean loadSoundEffects() { - int status; - - loadTouchSoundAssets(); - - synchronized (mSoundEffectsLock) { - if (!mBootCompleted) { - Log.w(TAG, "loadSoundEffects() called before boot complete"); - return false; - } - - if (mSoundPool != null) { - return true; - } - mSoundPool = new SoundPool(NUM_SOUNDPOOL_CHANNELS, AudioSystem.STREAM_SYSTEM, 0); - - try { - mSoundPoolCallBack = null; - mSoundPoolListenerThread = new SoundPoolListenerThread(); - mSoundPoolListenerThread.start(); - // Wait for mSoundPoolCallBack to be set by the other thread - mSoundEffectsLock.wait(); - } catch (InterruptedException e) { - Log.w(TAG, "Interrupted while waiting sound pool listener thread."); - } - - if (mSoundPoolCallBack == null) { - Log.w(TAG, "loadSoundEffects() could not create SoundPool listener or thread"); - if (mSoundPoolLooper != null) { - mSoundPoolLooper.quit(); - mSoundPoolLooper = null; - } - mSoundPoolListenerThread = null; - mSoundPool.release(); - mSoundPool = null; - return false; - } - /* - * poolId table: The value -1 in this table indicates that corresponding - * file (same index in SOUND_EFFECT_FILES[] has not been loaded. - * Once loaded, the value in poolId is the sample ID and the same - * sample can be reused for another effect using the same file. - */ - int[] poolId = new int[SOUND_EFFECT_FILES.size()]; - for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) { - poolId[fileIdx] = -1; - } - /* - * Effects whose value in SOUND_EFFECT_FILES_MAP[effect][1] is -1 must be loaded. - * If load succeeds, value in SOUND_EFFECT_FILES_MAP[effect][1] is > 0: - * this indicates we have a valid sample loaded for this effect. - */ - - int lastSample = 0; - for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { - // Do not load sample if this effect uses the MediaPlayer - if (SOUND_EFFECT_FILES_MAP[effect][1] == 0) { - continue; - } - if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) { - String filePath = Environment.getRootDirectory() - + SOUND_EFFECTS_PATH - + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effect][0]); - int sampleId = mSoundPool.load(filePath, 0); - if (sampleId <= 0) { - Log.w(TAG, "Soundpool could not load file: "+filePath); - } else { - SOUND_EFFECT_FILES_MAP[effect][1] = sampleId; - poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId; - lastSample = sampleId; - } - } else { - SOUND_EFFECT_FILES_MAP[effect][1] = poolId[SOUND_EFFECT_FILES_MAP[effect][0]]; - } - } - // wait for all samples to be loaded - if (lastSample != 0) { - mSoundPoolCallBack.setLastSample(lastSample); + int attempts = 3; + LoadSoundEffectReply reply = new LoadSoundEffectReply(); + synchronized (reply) { + sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0); + while ((reply.mStatus == 1) && (attempts-- > 0)) { try { - mSoundEffectsLock.wait(); - status = mSoundPoolCallBack.status(); - } catch (java.lang.InterruptedException e) { - Log.w(TAG, "Interrupted while waiting sound pool callback."); - status = -1; - } - } else { - status = -1; - } - - if (mSoundPoolLooper != null) { - mSoundPoolLooper.quit(); - mSoundPoolLooper = null; - } - mSoundPoolListenerThread = null; - if (status != 0) { - Log.w(TAG, - "loadSoundEffects(), Error " - + ((lastSample != 0) ? mSoundPoolCallBack.status() : -1) - + " while loading samples"); - for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { - if (SOUND_EFFECT_FILES_MAP[effect][1] > 0) { - SOUND_EFFECT_FILES_MAP[effect][1] = -1; - } + reply.wait(SOUND_EFECTS_LOAD_TIMEOUT_MS); + } catch (InterruptedException e) { + Log.w(TAG, "loadSoundEffects Interrupted while waiting sound pool loaded."); } - - mSoundPool.release(); - mSoundPool = null; } } - return (status == 0); + return (reply.mStatus == 0); } /** @@ -1899,32 +1813,7 @@ public class AudioService extends IAudioService.Stub implements OnFinished { * sound effects are disabled. */ public void unloadSoundEffects() { - synchronized (mSoundEffectsLock) { - if (mSoundPool == null) { - return; - } - - mAudioHandler.removeMessages(MSG_LOAD_SOUND_EFFECTS); - mAudioHandler.removeMessages(MSG_PLAY_SOUND_EFFECT); - - int[] poolId = new int[SOUND_EFFECT_FILES.size()]; - for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) { - poolId[fileIdx] = 0; - } - - for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { - if (SOUND_EFFECT_FILES_MAP[effect][1] <= 0) { - continue; - } - if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == 0) { - mSoundPool.unload(SOUND_EFFECT_FILES_MAP[effect][1]); - SOUND_EFFECT_FILES_MAP[effect][1] = -1; - poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = -1; - } - } - mSoundPool.release(); - mSoundPool = null; - } + sendMsg(mAudioHandler, MSG_UNLOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, null, 0); } class SoundPoolListenerThread extends Thread { @@ -1952,23 +1841,30 @@ public class AudioService extends IAudioService.Stub implements OnFinished { private final class SoundPoolCallback implements android.media.SoundPool.OnLoadCompleteListener { - int mStatus; - int mLastSample; + int mStatus = 1; // 1 means neither error nor last sample loaded yet + List<Integer> mSamples = new ArrayList<Integer>(); public int status() { return mStatus; } - public void setLastSample(int sample) { - mLastSample = sample; + public void setSamples(int[] samples) { + for (int i = 0; i < samples.length; i++) { + // do not wait ack for samples rejected upfront by SoundPool + if (samples[i] > 0) { + mSamples.add(samples[i]); + } + } } public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { synchronized (mSoundEffectsLock) { - if (status != 0) { - mStatus = status; + int i = mSamples.indexOf(sampleId); + if (i >= 0) { + mSamples.remove(i); } - if (sampleId == mLastSample) { + if ((status != 0) || mSamples. isEmpty()) { + mStatus = status; mSoundEffectsLock.notify(); } } @@ -3404,11 +3300,163 @@ public class AudioService extends IAudioService.Stub implements OnFinished { Settings.Global.putInt(mContentResolver, Settings.Global.MODE_RINGER, ringerMode); } - private void playSoundEffect(int effectType, int volume) { + private boolean onLoadSoundEffects() { + int status; + + synchronized (mSoundEffectsLock) { + if (!mBootCompleted) { + Log.w(TAG, "onLoadSoundEffects() called before boot complete"); + return false; + } + + if (mSoundPool != null) { + return true; + } + + loadTouchSoundAssets(); + + mSoundPool = new SoundPool(NUM_SOUNDPOOL_CHANNELS, AudioSystem.STREAM_SYSTEM, 0); + mSoundPoolCallBack = null; + mSoundPoolListenerThread = new SoundPoolListenerThread(); + mSoundPoolListenerThread.start(); + int attempts = 3; + while ((mSoundPoolCallBack == null) && (attempts-- > 0)) { + try { + // Wait for mSoundPoolCallBack to be set by the other thread + mSoundEffectsLock.wait(SOUND_EFECTS_LOAD_TIMEOUT_MS); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while waiting sound pool listener thread."); + } + } + + if (mSoundPoolCallBack == null) { + Log.w(TAG, "onLoadSoundEffects() SoundPool listener or thread creation error"); + if (mSoundPoolLooper != null) { + mSoundPoolLooper.quit(); + mSoundPoolLooper = null; + } + mSoundPoolListenerThread = null; + mSoundPool.release(); + mSoundPool = null; + return false; + } + /* + * poolId table: The value -1 in this table indicates that corresponding + * file (same index in SOUND_EFFECT_FILES[] has not been loaded. + * Once loaded, the value in poolId is the sample ID and the same + * sample can be reused for another effect using the same file. + */ + int[] poolId = new int[SOUND_EFFECT_FILES.size()]; + for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) { + poolId[fileIdx] = -1; + } + /* + * Effects whose value in SOUND_EFFECT_FILES_MAP[effect][1] is -1 must be loaded. + * If load succeeds, value in SOUND_EFFECT_FILES_MAP[effect][1] is > 0: + * this indicates we have a valid sample loaded for this effect. + */ + + int numSamples = 0; + for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { + // Do not load sample if this effect uses the MediaPlayer + if (SOUND_EFFECT_FILES_MAP[effect][1] == 0) { + continue; + } + if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) { + String filePath = Environment.getRootDirectory() + + SOUND_EFFECTS_PATH + + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effect][0]); + int sampleId = mSoundPool.load(filePath, 0); + if (sampleId <= 0) { + Log.w(TAG, "Soundpool could not load file: "+filePath); + } else { + SOUND_EFFECT_FILES_MAP[effect][1] = sampleId; + poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId; + numSamples++; + } + } else { + SOUND_EFFECT_FILES_MAP[effect][1] = + poolId[SOUND_EFFECT_FILES_MAP[effect][0]]; + } + } + // wait for all samples to be loaded + if (numSamples > 0) { + mSoundPoolCallBack.setSamples(poolId); + + attempts = 3; + status = 1; + while ((status == 1) && (attempts-- > 0)) { + try { + mSoundEffectsLock.wait(SOUND_EFECTS_LOAD_TIMEOUT_MS); + status = mSoundPoolCallBack.status(); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while waiting sound pool callback."); + } + } + } else { + status = -1; + } + + if (mSoundPoolLooper != null) { + mSoundPoolLooper.quit(); + mSoundPoolLooper = null; + } + mSoundPoolListenerThread = null; + if (status != 0) { + Log.w(TAG, + "onLoadSoundEffects(), Error "+status+ " while loading samples"); + for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { + if (SOUND_EFFECT_FILES_MAP[effect][1] > 0) { + SOUND_EFFECT_FILES_MAP[effect][1] = -1; + } + } + + mSoundPool.release(); + mSoundPool = null; + } + } + return (status == 0); + } + + /** + * Unloads samples from the sound pool. + * This method can be called to free some memory when + * sound effects are disabled. + */ + private void onUnloadSoundEffects() { synchronized (mSoundEffectsLock) { if (mSoundPool == null) { return; } + + int[] poolId = new int[SOUND_EFFECT_FILES.size()]; + for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) { + poolId[fileIdx] = 0; + } + + for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { + if (SOUND_EFFECT_FILES_MAP[effect][1] <= 0) { + continue; + } + if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == 0) { + mSoundPool.unload(SOUND_EFFECT_FILES_MAP[effect][1]); + SOUND_EFFECT_FILES_MAP[effect][1] = -1; + poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = -1; + } + } + mSoundPool.release(); + mSoundPool = null; + } + } + + private void onPlaySoundEffect(int effectType, int volume) { + synchronized (mSoundEffectsLock) { + + onLoadSoundEffects(); + + if (mSoundPool == null) { + return; + } float volFloat; // use default if volume is not specified by caller if (volume < 0) { @@ -3418,7 +3466,8 @@ public class AudioService extends IAudioService.Stub implements OnFinished { } if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) { - mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1], volFloat, volFloat, 0, 0, 1.0f); + mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1], + volFloat, volFloat, 0, 0, 1.0f); } else { MediaPlayer mediaPlayer = new MediaPlayer(); try { @@ -3598,12 +3647,25 @@ public class AudioService extends IAudioService.Stub implements OnFinished { AudioSystem.setParameters("restarting=false"); break; + case MSG_UNLOAD_SOUND_EFFECTS: + onUnloadSoundEffects(); + break; + case MSG_LOAD_SOUND_EFFECTS: - loadSoundEffects(); + //FIXME: onLoadSoundEffects() should be executed in a separate thread as it + // can take several dozens of milliseconds to complete + boolean loaded = onLoadSoundEffects(); + if (msg.obj != null) { + LoadSoundEffectReply reply = (LoadSoundEffectReply)msg.obj; + synchronized (reply) { + reply.mStatus = loaded ? 0 : -1; + reply.notify(); + } + } break; case MSG_PLAY_SOUND_EFFECT: - playSoundEffect(msg.arg1, msg.arg2); + onPlaySoundEffect(msg.arg1, msg.arg2); break; case MSG_BTA2DP_DOCK_TIMEOUT: @@ -4151,7 +4213,7 @@ public class AudioService extends IAudioService.Stub implements OnFinished { } } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { mBootCompleted = true; - sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_NOOP, + sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, null, 0); mKeyguardManager = diff --git a/media/jni/android_media_MediaCrypto.cpp b/media/jni/android_media_MediaCrypto.cpp index 517a293..d0f56ea 100644 --- a/media/jni/android_media_MediaCrypto.cpp +++ b/media/jni/android_media_MediaCrypto.cpp @@ -74,7 +74,7 @@ sp<ICrypto> JCrypto::MakeCrypto() { sp<ICrypto> crypto = service->makeCrypto(); - if (crypto == NULL || crypto->initCheck() != OK) { + if (crypto == NULL || (crypto->initCheck() != OK && crypto->initCheck() != NO_INIT)) { return NULL; } diff --git a/services/input/InputDispatcher.h b/services/input/InputDispatcher.h index d4f932e..430721e 100644 --- a/services/input/InputDispatcher.h +++ b/services/input/InputDispatcher.h @@ -165,6 +165,8 @@ struct InputTarget { * Input dispatcher configuration. * * Specifies various options that modify the behavior of the input dispatcher. + * The values provided here are merely defaults. The actual values will come from ViewConfiguration + * and are passed into the dispatcher during initialization. */ struct InputDispatcherConfiguration { // The key repeat initial timeout. diff --git a/services/java/com/android/server/BackupManagerService.java b/services/java/com/android/server/BackupManagerService.java index a45c0ff..2a3c87e 100644 --- a/services/java/com/android/server/BackupManagerService.java +++ b/services/java/com/android/server/BackupManagerService.java @@ -63,6 +63,7 @@ import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; +import android.os.SELinux; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; @@ -746,6 +747,9 @@ class BackupManagerService extends IBackupManager.Stub { // correct directory. mBaseStateDir = new File(Environment.getSecureDataDirectory(), "backup"); mBaseStateDir.mkdirs(); + if (!SELinux.restorecon(mBaseStateDir)) { + Slog.e(TAG, "SELinux restorecon failed on " + mBaseStateDir); + } mDataDir = Environment.getDownloadCacheDirectory(); mPasswordHashFile = new File(mBaseStateDir, "pwhash"); @@ -2136,6 +2140,10 @@ class BackupManagerService extends IBackupManager.Stub { ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE); + if (!SELinux.restorecon(mBackupDataName)) { + Slog.e(TAG, "SELinux restorecon failed on " + mBackupDataName); + } + mNewState = ParcelFileDescriptor.open(mNewStateName, ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE | @@ -4697,6 +4705,10 @@ class BackupManagerService extends IBackupManager.Stub { ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE); + if (!SELinux.restorecon(mBackupDataName)) { + Slog.e(TAG, "SElinux restorecon failed for " + mBackupDataName); + } + if (mTransport.getRestoreData(mBackupData) != BackupConstants.TRANSPORT_OK) { // Transport-level failure, so we wind everything up and // terminate the restore operation. diff --git a/services/java/com/android/server/DevicePolicyManagerService.java b/services/java/com/android/server/DevicePolicyManagerService.java index 18b4ec1..d3e7c24 100644 --- a/services/java/com/android/server/DevicePolicyManagerService.java +++ b/services/java/com/android/server/DevicePolicyManagerService.java @@ -41,10 +41,14 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.Signature; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; +import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.Environment; @@ -62,6 +66,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.util.AtomicFile; import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.Slog; @@ -88,10 +93,11 @@ import java.util.Set; * Implementation of the device policy APIs. */ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { - private static final String DEVICE_POLICIES_XML = "device_policies.xml"; private static final String TAG = "DevicePolicyManagerService"; + private static final String DEVICE_POLICIES_XML = "device_policies.xml"; + private static final int REQUEST_EXPIRE_PASSWORD = 5571; private static final long MS_PER_DAY = 86400 * 1000; @@ -109,6 +115,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { IPowerManager mIPowerManager; IWindowManager mIWindowManager; + private DeviceOwner mDeviceOwner; + public static class DevicePolicyData { int mActivePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; int mActivePasswordLength = 0; @@ -507,6 +515,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { filter.addAction(Intent.ACTION_PACKAGE_CHANGED); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); filter.addDataScheme("package"); context.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); } @@ -545,6 +554,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + void loadDeviceOwner() { + synchronized (this) { + if (DeviceOwner.isRegistered()) { + mDeviceOwner = new DeviceOwner(); + } + } + } + /** * Set an alarm for an upcoming event - expiration warning, expiration, or post-expiration * reminders. Clears alarm if no expirations are configured. @@ -709,7 +726,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Intent resolveIntent = new Intent(); resolveIntent.setComponent(adminName); List<ResolveInfo> infos = mContext.getPackageManager().queryBroadcastReceivers( - resolveIntent, PackageManager.GET_META_DATA, userHandle); + resolveIntent, + PackageManager.GET_META_DATA | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, + userHandle); if (infos == null || infos.size() <= 0) { throw new IllegalArgumentException("Unknown admin: " + adminName); } @@ -994,6 +1013,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { public void systemReady() { synchronized (this) { loadSettingsLocked(getUserData(UserHandle.USER_OWNER), UserHandle.USER_OWNER); + loadDeviceOwner(); } } @@ -1052,6 +1072,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } if (replaceIndex == -1) { policy.mAdminList.add(newAdmin); + enableIfNecessary(info.getPackageName(), userHandle); } else { policy.mAdminList.set(replaceIndex, newAdmin); } @@ -1119,6 +1140,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return; } if (admin.getUid() != Binder.getCallingUid()) { + // If trying to remove device owner, refuse when the caller is not the owner. + if (mDeviceOwner != null + && adminReceiver.getPackageName().equals(mDeviceOwner.getPackageName())) { + return; + } mContext.enforceCallingOrSelfPermission( android.Manifest.permission.BIND_DEVICE_ADMIN, null); } @@ -2351,6 +2377,49 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + @Override + public boolean setDeviceOwner(String packageName) { + if (packageName == null + || !DeviceOwner.isInstalled(packageName, mContext.getPackageManager())) { + throw new IllegalArgumentException("Invalid package name " + packageName + + " for device owner"); + } + synchronized (this) { + if (mDeviceOwner == null && !isDeviceProvisioned()) { + mDeviceOwner = new DeviceOwner(packageName); + mDeviceOwner.writeOwnerFile(); + return true; + } else { + throw new IllegalStateException("Trying to set device owner to " + packageName + + ", owner=" + mDeviceOwner.getPackageName() + + ", device_provisioned=" + isDeviceProvisioned()); + } + } + } + + @Override + public boolean isDeviceOwner(String packageName) { + synchronized (this) { + return mDeviceOwner != null + && mDeviceOwner.getPackageName().equals(packageName); + } + } + + @Override + public String getDeviceOwner() { + synchronized (this) { + if (mDeviceOwner != null) { + return mDeviceOwner.getPackageName(); + } + } + return null; + } + + private boolean isDeviceProvisioned() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0) > 0; + } + private void enforceCrossUserPermission(int userHandle) { if (userHandle < 0) { throw new IllegalArgumentException("Invalid userId " + userHandle); @@ -2364,6 +2433,22 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + private void enableIfNecessary(String packageName, int userId) { + try { + IPackageManager ipm = AppGlobals.getPackageManager(); + ApplicationInfo ai = ipm.getApplicationInfo(packageName, + PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, + userId); + if (ai.enabledSetting + == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) { + ipm.setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + PackageManager.DONT_KILL_APP, userId); + } + } catch (RemoteException e) { + } + } + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) @@ -2399,4 +2484,92 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } } + + static class DeviceOwner { + private static final String DEVICE_OWNER_XML = "device_owner.xml"; + private static final String TAG_DEVICE_OWNER = "device-owner"; + private static final String ATTR_PACKAGE = "package"; + private String mPackageName; + + DeviceOwner() { + readOwnerFile(); + } + + DeviceOwner(String packageName) { + this.mPackageName = packageName; + } + + static boolean isRegistered() { + return new File(Environment.getSystemSecureDirectory(), + DEVICE_OWNER_XML).exists(); + } + + String getPackageName() { + return mPackageName; + } + + static boolean isInstalled(String packageName, PackageManager pm) { + try { + PackageInfo pi; + if ((pi = pm.getPackageInfo(packageName, 0)) != null) { + if ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + return true; + } + } + } catch (NameNotFoundException nnfe) { + Slog.w(TAG, "Device Owner package " + packageName + " not installed."); + } + return false; + } + + void readOwnerFile() { + AtomicFile file = new AtomicFile(new File(Environment.getSystemSecureDirectory(), + DEVICE_OWNER_XML)); + try { + FileInputStream input = file.openRead(); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(input, null); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + String tag = parser.getName(); + if (!TAG_DEVICE_OWNER.equals(tag)) { + throw new XmlPullParserException( + "Device Owner file does not start with device-owner tag: found " + tag); + } + mPackageName = parser.getAttributeValue(null, ATTR_PACKAGE); + input.close(); + } catch (XmlPullParserException xppe) { + Slog.e(TAG, "Error parsing device-owner file\n" + xppe); + } catch (IOException ioe) { + Slog.e(TAG, "IO Exception when reading device-owner file\n" + ioe); + } + } + + void writeOwnerFile() { + synchronized (this) { + writeOwnerFileLocked(); + } + } + + private void writeOwnerFileLocked() { + AtomicFile file = new AtomicFile(new File(Environment.getSystemSecureDirectory(), + DEVICE_OWNER_XML)); + try { + FileOutputStream output = file.startWrite(); + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(output, "utf-8"); + out.startDocument(null, true); + out.startTag(null, TAG_DEVICE_OWNER); + out.attribute(null, ATTR_PACKAGE, mPackageName); + out.endTag(null, TAG_DEVICE_OWNER); + out.endDocument(); + out.flush(); + file.finishWrite(output); + } catch (IOException ioe) { + Slog.e(TAG, "IO Exception when writing device-owner file\n" + ioe); + } + } + } } diff --git a/services/java/com/android/server/MountService.java b/services/java/com/android/server/MountService.java index c6aca2f..e8d7882 100644 --- a/services/java/com/android/server/MountService.java +++ b/services/java/com/android/server/MountService.java @@ -1603,7 +1603,7 @@ class MountService extends IMountService.Stub boolean mounted = false; try { mounted = Environment.MEDIA_MOUNTED.equals(getVolumeState(primary.getPath())); - } catch (IllegalStateException e) { + } catch (IllegalArgumentException e) { } if (!mounted) { diff --git a/services/java/com/android/server/accounts/AccountManagerService.java b/services/java/com/android/server/accounts/AccountManagerService.java index 09daf56..c4b98ad 100644 --- a/services/java/com/android/server/accounts/AccountManagerService.java +++ b/services/java/com/android/server/accounts/AccountManagerService.java @@ -1425,9 +1425,9 @@ public class AccountManagerService if (accountType == null) throw new IllegalArgumentException("accountType is null"); checkManageAccountsPermission(); - // Is user allowed to modify accounts? - if (!getUserManager().getUserRestrictions(Binder.getCallingUserHandle()) - .getBoolean(UserManager.ALLOW_MODIFY_ACCOUNTS)) { + // Is user disallowed from modifying accounts? + if (getUserManager().getUserRestrictions(Binder.getCallingUserHandle()) + .getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false)) { try { response.onError(AccountManager.ERROR_CODE_USER_RESTRICTED, "User is not allowed to add an account!"); @@ -2572,7 +2572,7 @@ public class AccountManagerService if (callingUid != android.os.Process.myUid()) { Bundle restrictions = getUserManager().getUserRestrictions( new UserHandle(UserHandle.getUserId(callingUid))); - if (!restrictions.getBoolean(UserManager.ALLOW_MODIFY_ACCOUNTS)) { + if (restrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false)) { return false; } } diff --git a/services/java/com/android/server/am/ActivityManagerService.java b/services/java/com/android/server/am/ActivityManagerService.java index 6f092bf..04ffbd9 100644 --- a/services/java/com/android/server/am/ActivityManagerService.java +++ b/services/java/com/android/server/am/ActivityManagerService.java @@ -2194,7 +2194,7 @@ public final class ActivityManagerService extends ActivityManagerNative // the PID of the new process, or else throw a RuntimeException. Process.ProcessStartResult startResult = Process.start("android.app.ActivityThread", app.processName, uid, uid, gids, debugFlags, mountExternal, - app.info.targetSdkVersion, null, null); + app.info.targetSdkVersion, app.info.seinfo, null); BatteryStatsImpl bs = app.batteryStats.getBatteryStats(); synchronized (bs) { diff --git a/services/java/com/android/server/pm/Installer.java b/services/java/com/android/server/pm/Installer.java index 02a2c1b..d9c85bf 100644 --- a/services/java/com/android/server/pm/Installer.java +++ b/services/java/com/android/server/pm/Installer.java @@ -188,7 +188,7 @@ public final class Installer { } } - public int install(String name, int uid, int gid) { + public int install(String name, int uid, int gid, String seinfo) { StringBuilder builder = new StringBuilder("install"); builder.append(' '); builder.append(name); @@ -196,6 +196,8 @@ public final class Installer { builder.append(uid); builder.append(' '); builder.append(gid); + builder.append(' '); + builder.append(seinfo != null ? seinfo : "!"); return execute(builder.toString()); } diff --git a/services/java/com/android/server/pm/PackageManagerService.java b/services/java/com/android/server/pm/PackageManagerService.java index 09d1426..afdd294 100644 --- a/services/java/com/android/server/pm/PackageManagerService.java +++ b/services/java/com/android/server/pm/PackageManagerService.java @@ -362,6 +362,9 @@ public class PackageManagerService extends IPackageManager.Stub { final HashMap<String, FeatureInfo> mAvailableFeatures = new HashMap<String, FeatureInfo>(); + // If mac_permissions.xml was found for seinfo labeling. + boolean mFoundPolicyFile; + // All available activities, for your resolving pleasure. final ActivityIntentResolver mActivities = new ActivityIntentResolver(); @@ -1029,8 +1032,11 @@ public class PackageManagerService extends IPackageManager.Stub { readPermissions(); + mFoundPolicyFile = SELinuxMMAC.readInstallPolicy(); + mRestoredSettings = mSettings.readLPw(this, sUserManager.getUsers(false), mSdkVersion, mOnlyCore); + long startTime = SystemClock.uptimeMillis(); EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_SYSTEM_SCAN_START, @@ -2236,6 +2242,34 @@ public class PackageManagerService extends IPackageManager.Stub { } } + private static void checkGrantRevokePermissions(PackageParser.Package pkg, BasePermission bp) { + int index = pkg.requestedPermissions.indexOf(bp.name); + if (index == -1) { + throw new SecurityException("Package " + pkg.packageName + + " has not requested permission " + bp.name); + } + boolean isNormal = + ((bp.protectionLevel&PermissionInfo.PROTECTION_MASK_BASE) + == PermissionInfo.PROTECTION_NORMAL); + boolean isDangerous = + ((bp.protectionLevel&PermissionInfo.PROTECTION_MASK_BASE) + == PermissionInfo.PROTECTION_DANGEROUS); + boolean isDevelopment = + ((bp.protectionLevel&PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0); + + if (!isNormal && !isDangerous && !isDevelopment) { + throw new SecurityException("Permission " + bp.name + + " is not a changeable permission type"); + } + + if (isNormal || isDangerous) { + if (pkg.requestedPermissionsRequired.get(index)) { + throw new SecurityException("Can't change " + bp.name + + ". It is required by the application"); + } + } + } + public void grantPermission(String packageName, String permissionName) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.GRANT_REVOKE_PERMISSIONS, null); @@ -2246,21 +2280,16 @@ public class PackageManagerService extends IPackageManager.Stub { } final BasePermission bp = mSettings.mPermissions.get(permissionName); if (bp == null) { - throw new IllegalArgumentException("Unknown permission: " + packageName); - } - if (!pkg.requestedPermissions.contains(permissionName)) { - throw new SecurityException("Package " + packageName - + " has not requested permission " + permissionName); - } - if ((bp.protectionLevel&PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) == 0) { - throw new SecurityException("Permission " + permissionName - + " is not a development permission"); + throw new IllegalArgumentException("Unknown permission: " + permissionName); } + + checkGrantRevokePermissions(pkg, bp); + final PackageSetting ps = (PackageSetting) pkg.mExtras; if (ps == null) { return; } - final GrantedPermissions gp = ps.sharedUser != null ? ps.sharedUser : ps; + final GrantedPermissions gp = (ps.sharedUser != null) ? ps.sharedUser : ps; if (gp.grantedPermissions.add(permissionName)) { if (ps.haveGids) { gp.gids = appendInts(gp.gids, bp.gids); @@ -2282,21 +2311,16 @@ public class PackageManagerService extends IPackageManager.Stub { } final BasePermission bp = mSettings.mPermissions.get(permissionName); if (bp == null) { - throw new IllegalArgumentException("Unknown permission: " + packageName); - } - if (!pkg.requestedPermissions.contains(permissionName)) { - throw new SecurityException("Package " + packageName - + " has not requested permission " + permissionName); - } - if ((bp.protectionLevel&PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) == 0) { - throw new SecurityException("Permission " + permissionName - + " is not a development permission"); + throw new IllegalArgumentException("Unknown permission: " + permissionName); } + + checkGrantRevokePermissions(pkg, bp); + final PackageSetting ps = (PackageSetting) pkg.mExtras; if (ps == null) { return; } - final GrantedPermissions gp = ps.sharedUser != null ? ps.sharedUser : ps; + final GrantedPermissions gp = (ps.sharedUser != null) ? ps.sharedUser : ps; if (gp.grantedPermissions.remove(permissionName)) { gp.grantedPermissions.remove(permissionName); if (ps.haveGids) { @@ -3676,9 +3700,9 @@ public class PackageManagerService extends IPackageManager.Stub { } } - private int createDataDirsLI(String packageName, int uid) { + private int createDataDirsLI(String packageName, int uid, String seinfo) { int[] users = sUserManager.getUserIds(); - int res = mInstaller.install(packageName, uid, uid); + int res = mInstaller.install(packageName, uid, uid, seinfo); if (res < 0) { return res; } @@ -4020,6 +4044,10 @@ public class PackageManagerService extends IPackageManager.Stub { pkg.applicationInfo.flags |= ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; } + if (mFoundPolicyFile) { + SELinuxMMAC.assignSeinfoValue(pkg); + } + pkg.applicationInfo.uid = pkgSetting.appId; pkg.mExtras = pkgSetting; @@ -4158,7 +4186,8 @@ public class PackageManagerService extends IPackageManager.Stub { recovered = true; // And now re-install the app. - ret = createDataDirsLI(pkgName, pkg.applicationInfo.uid); + ret = createDataDirsLI(pkgName, pkg.applicationInfo.uid, + pkg.applicationInfo.seinfo); if (ret == -1) { // Ack should not happen! msg = prefix + pkg.packageName @@ -4204,7 +4233,8 @@ public class PackageManagerService extends IPackageManager.Stub { Log.v(TAG, "Want this data dir: " + dataPath); } //invoke installer to do the actual installation - int ret = createDataDirsLI(pkgName, pkg.applicationInfo.uid); + int ret = createDataDirsLI(pkgName, pkg.applicationInfo.uid, + pkg.applicationInfo.seinfo); if (ret < 0) { // Error from installer mLastScanError = PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; @@ -5915,7 +5945,7 @@ public class PackageManagerService extends IPackageManager.Stub { null); final int uid = Binder.getCallingUid(); - if (!isUserAllowed(UserHandle.getUserId(uid), UserManager.ALLOW_INSTALL_APPS)) { + if (isUserRestricted(UserHandle.getUserId(uid), UserManager.DISALLOW_INSTALL_APPS)) { try { observer.packageInstalled("", PackageManager.INSTALL_FAILED_USER_RESTRICTED); } catch (RemoteException re) { @@ -5963,7 +5993,7 @@ public class PackageManagerService extends IPackageManager.Stub { android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, "installExistingPackage for user " + userId); } - if (!isUserAllowed(userId, UserManager.ALLOW_INSTALL_APPS)) { + if (isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) { return PackageManager.INSTALL_FAILED_USER_RESTRICTED; } @@ -5997,13 +6027,13 @@ public class PackageManagerService extends IPackageManager.Stub { return PackageManager.INSTALL_SUCCEEDED; } - private boolean isUserAllowed(int userId, String restrictionKey) { + private boolean isUserRestricted(int userId, String restrictionKey) { Bundle restrictions = sUserManager.getUserRestrictions(userId); - if (!restrictions.getBoolean(UserManager.ALLOW_INSTALL_APPS)) { - Log.w(TAG, "User does not have permission to: " + restrictionKey); - return false; + if (restrictions.getBoolean(restrictionKey, false)) { + Log.w(TAG, "User is restricted: " + restrictionKey); + return true; } - return true; + return false; } @Override @@ -8400,7 +8430,7 @@ public class PackageManagerService extends IPackageManager.Stub { android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, "deletePackage for user " + userId); } - if (!isUserAllowed(userId, UserManager.ALLOW_UNINSTALL_APPS)) { + if (isUserRestricted(userId, UserManager.DISALLOW_UNINSTALL_APPS)) { try { observer.packageDeleted(packageName, PackageManager.DELETE_FAILED_USER_RESTRICTED); } catch (RemoteException re) { @@ -8446,7 +8476,8 @@ public class PackageManagerService extends IPackageManager.Stub { IDevicePolicyManager dpm = IDevicePolicyManager.Stub.asInterface( ServiceManager.getService(Context.DEVICE_POLICY_SERVICE)); try { - if (dpm != null && dpm.packageHasActiveAdmins(packageName, userId)) { + if (dpm != null && (dpm.packageHasActiveAdmins(packageName, userId) + || dpm.isDeviceOwner(packageName))) { Slog.w(TAG, "Not removing package " + packageName + ": has active device admin"); return PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER; } diff --git a/services/java/com/android/server/pm/SELinuxMMAC.java b/services/java/com/android/server/pm/SELinuxMMAC.java new file mode 100644 index 0000000..15d2a5a --- /dev/null +++ b/services/java/com/android/server/pm/SELinuxMMAC.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageParser; +import android.content.pm.Signature; +import android.os.Environment; +import android.util.Slog; +import android.util.Xml; + +import com.android.internal.util.XmlUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; + +import java.util.HashMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Centralized access to SELinux MMAC (middleware MAC) implementation. + * {@hide} + */ +public final class SELinuxMMAC { + + private static final String TAG = "SELinuxMMAC"; + + private static final boolean DEBUG_POLICY = false; + private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false; + + // Signature seinfo values read from policy. + private static final HashMap<Signature, String> sSigSeinfo = + new HashMap<Signature, String>(); + + // Package name seinfo values read from policy. + private static final HashMap<String, String> sPackageSeinfo = + new HashMap<String, String>(); + + // Locations of potential install policy files. + private static final File[] INSTALL_POLICY_FILE = { + new File(Environment.getDataDirectory(), "system/mac_permissions.xml"), + new File(Environment.getRootDirectory(), "etc/security/mac_permissions.xml"), + null}; + + private static void flushInstallPolicy() { + sSigSeinfo.clear(); + sPackageSeinfo.clear(); + } + + /** + * Parses an MMAC install policy from a predefined list of locations. + * @param none + * @return boolean indicating whether an install policy was correctly parsed. + */ + public static boolean readInstallPolicy() { + + return readInstallPolicy(INSTALL_POLICY_FILE); + } + + /** + * Parses an MMAC install policy given as an argument. + * @param File object representing the path of the policy. + * @return boolean indicating whether the install policy was correctly parsed. + */ + public static boolean readInstallPolicy(File policyFile) { + + return readInstallPolicy(new File[]{policyFile,null}); + } + + private static boolean readInstallPolicy(File[] policyFiles) { + + FileReader policyFile = null; + int i = 0; + while (policyFile == null && policyFiles != null && policyFiles[i] != null) { + try { + policyFile = new FileReader(policyFiles[i]); + break; + } catch (FileNotFoundException e) { + Slog.d(TAG,"Couldn't find install policy " + policyFiles[i].getPath()); + } + i++; + } + + if (policyFile == null) { + Slog.d(TAG, "No policy file found. All seinfo values will be null."); + return false; + } + + Slog.d(TAG, "Using install policy file " + policyFiles[i].getPath()); + + flushInstallPolicy(); + + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(policyFile); + + XmlUtils.beginDocument(parser, "policy"); + while (true) { + XmlUtils.nextElement(parser); + if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { + break; + } + + String tagName = parser.getName(); + if ("signer".equals(tagName)) { + String cert = parser.getAttributeValue(null, "signature"); + if (cert == null) { + Slog.w(TAG, "<signer> without signature at " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } + Signature signature; + try { + signature = new Signature(cert); + } catch (IllegalArgumentException e) { + Slog.w(TAG, "<signer> with bad signature at " + + parser.getPositionDescription(), e); + XmlUtils.skipCurrentTag(parser); + continue; + } + String seinfo = readSeinfoTag(parser); + if (seinfo != null) { + if (DEBUG_POLICY_INSTALL) + Slog.i(TAG, "<signer> tag: (" + cert + ") assigned seinfo=" + + seinfo); + + sSigSeinfo.put(signature, seinfo); + } + } else if ("default".equals(tagName)) { + String seinfo = readSeinfoTag(parser); + if (seinfo != null) { + if (DEBUG_POLICY_INSTALL) + Slog.i(TAG, "<default> tag assigned seinfo=" + seinfo); + + // The 'null' signature is the default seinfo value + sSigSeinfo.put(null, seinfo); + } + } else if ("package".equals(tagName)) { + String pkgName = parser.getAttributeValue(null, "name"); + if (pkgName == null) { + Slog.w(TAG, "<package> without name at " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } + String seinfo = readSeinfoTag(parser); + if (seinfo != null) { + if (DEBUG_POLICY_INSTALL) + Slog.i(TAG, "<package> tag: (" + pkgName + + ") assigned seinfo=" + seinfo); + + sPackageSeinfo.put(pkgName, seinfo); + } + } else { + XmlUtils.skipCurrentTag(parser); + continue; + } + } + } catch (XmlPullParserException e) { + Slog.w(TAG, "Got execption parsing ", e); + } catch (IOException e) { + Slog.w(TAG, "Got execption parsing ", e); + } + try { + policyFile.close(); + } catch (IOException e) { + //omit + } + return true; + } + + private static String readSeinfoTag(XmlPullParser parser) throws + IOException, XmlPullParserException { + + int type; + int outerDepth = parser.getDepth(); + String seinfo = null; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG + || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if ("seinfo".equals(tagName)) { + String seinfoValue = parser.getAttributeValue(null, "value"); + if (seinfoValue != null) { + seinfo = seinfoValue; + } else { + Slog.w(TAG, "<seinfo> without value at " + + parser.getPositionDescription()); + } + } + XmlUtils.skipCurrentTag(parser); + } + return seinfo; + } + + /** + * Labels a package based on an seinfo tag from install policy. + * The label is attached to the ApplicationInfo instance of the package. + * @param PackageParser.Package object representing the package + * to labeled. + * @return String holding the value of the seinfo label that was assigned. + * Value may be null which indicates no seinfo label was assigned. + */ + public static void assignSeinfoValue(PackageParser.Package pkg) { + + /* + * Non system installed apps should be treated the same. This + * means that any post-loaded apk will be assigned the default + * tag, if one exists in the policy, else null, without respect + * to the signing key. + */ + if (((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) || + ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0)) { + + // We just want one of the signatures to match. + for (Signature s : pkg.mSignatures) { + if (s == null) + continue; + + if (sSigSeinfo.containsKey(s)) { + String seinfo = pkg.applicationInfo.seinfo = sSigSeinfo.get(s); + if (DEBUG_POLICY_INSTALL) + Slog.i(TAG, "package (" + pkg.packageName + + ") labeled with seinfo=" + seinfo); + + return; + } + } + + // Check for seinfo labeled by package. + if (sPackageSeinfo.containsKey(pkg.packageName)) { + String seinfo = pkg.applicationInfo.seinfo = sPackageSeinfo.get(pkg.packageName); + if (DEBUG_POLICY_INSTALL) + Slog.i(TAG, "package (" + pkg.packageName + + ") labeled with seinfo=" + seinfo); + return; + } + } + + // If we have a default seinfo value then great, otherwise + // we set a null object and that is what we started with. + String seinfo = pkg.applicationInfo.seinfo = sSigSeinfo.get(null); + if (DEBUG_POLICY_INSTALL) + Slog.i(TAG, "package (" + pkg.packageName + + ") labeled with seinfo=" + (seinfo == null ? "null" : seinfo)); + } +} diff --git a/services/java/com/android/server/pm/UserManagerService.java b/services/java/com/android/server/pm/UserManagerService.java index 636b0e5..fecc2df 100644 --- a/services/java/com/android/server/pm/UserManagerService.java +++ b/services/java/com/android/server/pm/UserManagerService.java @@ -226,6 +226,13 @@ public class UserManagerService extends IUserManager.Stub { } } + @Override + public boolean isRestricted() { + synchronized (mPackagesLock) { + return getUserInfoLocked(UserHandle.getCallingUserId()).isRestricted(); + } + } + /* * Should be locked on mUsers before calling this. */ @@ -558,7 +565,6 @@ public class UserManagerService extends IUserManager.Stub { mNextSerialNumber = MIN_USER_ID; Bundle restrictions = new Bundle(); - initRestrictionsToDefaults(restrictions); mUserRestrictions.append(UserHandle.USER_OWNER, restrictions); updateUserIdsLocked(); @@ -608,11 +614,11 @@ public class UserManagerService extends IUserManager.Stub { Bundle restrictions = mUserRestrictions.get(userInfo.id); if (restrictions != null) { serializer.startTag(null, TAG_RESTRICTIONS); - writeBoolean(serializer, restrictions, UserManager.ALLOW_CONFIG_WIFI); - writeBoolean(serializer, restrictions, UserManager.ALLOW_MODIFY_ACCOUNTS); - writeBoolean(serializer, restrictions, UserManager.ALLOW_INSTALL_APPS); - writeBoolean(serializer, restrictions, UserManager.ALLOW_UNINSTALL_APPS); - writeBoolean(serializer, restrictions, UserManager.ALLOW_CONFIG_LOCATION_ACCESS); + writeBoolean(serializer, restrictions, UserManager.DISALLOW_CONFIG_WIFI); + writeBoolean(serializer, restrictions, UserManager.DISALLOW_MODIFY_ACCOUNTS); + writeBoolean(serializer, restrictions, UserManager.DISALLOW_INSTALL_APPS); + writeBoolean(serializer, restrictions, UserManager.DISALLOW_UNINSTALL_APPS); + writeBoolean(serializer, restrictions, UserManager.DISALLOW_SHARE_LOCATION); serializer.endTag(null, TAG_RESTRICTIONS); } serializer.endTag(null, TAG_USER); @@ -676,7 +682,6 @@ public class UserManagerService extends IUserManager.Stub { long lastLoggedInTime = 0L; boolean partial = false; Bundle restrictions = new Bundle(); - initRestrictionsToDefaults(restrictions); FileInputStream fis = null; try { @@ -725,11 +730,11 @@ public class UserManagerService extends IUserManager.Stub { name = parser.getText(); } } else if (TAG_RESTRICTIONS.equals(tag)) { - readBoolean(parser, restrictions, UserManager.ALLOW_CONFIG_WIFI); - readBoolean(parser, restrictions, UserManager.ALLOW_MODIFY_ACCOUNTS); - readBoolean(parser, restrictions, UserManager.ALLOW_INSTALL_APPS); - readBoolean(parser, restrictions, UserManager.ALLOW_UNINSTALL_APPS); - readBoolean(parser, restrictions, UserManager.ALLOW_CONFIG_LOCATION_ACCESS); + readBoolean(parser, restrictions, UserManager.DISALLOW_CONFIG_WIFI); + readBoolean(parser, restrictions, UserManager.DISALLOW_MODIFY_ACCOUNTS); + readBoolean(parser, restrictions, UserManager.DISALLOW_INSTALL_APPS); + readBoolean(parser, restrictions, UserManager.DISALLOW_UNINSTALL_APPS); + readBoolean(parser, restrictions, UserManager.DISALLOW_SHARE_LOCATION); } } } @@ -758,7 +763,9 @@ public class UserManagerService extends IUserManager.Stub { private void readBoolean(XmlPullParser parser, Bundle restrictions, String restrictionKey) { String value = parser.getAttributeValue(null, restrictionKey); - restrictions.putBoolean(restrictionKey, value == null ? true : Boolean.parseBoolean(value)); + if (value != null) { + restrictions.putBoolean(restrictionKey, Boolean.parseBoolean(value)); + } } private void writeBoolean(XmlSerializer xml, Bundle restrictions, String restrictionKey) @@ -769,14 +776,6 @@ public class UserManagerService extends IUserManager.Stub { } } - private void initRestrictionsToDefaults(Bundle restrictions) { - restrictions.putBoolean(UserManager.ALLOW_CONFIG_WIFI, true); - restrictions.putBoolean(UserManager.ALLOW_MODIFY_ACCOUNTS, true); - restrictions.putBoolean(UserManager.ALLOW_INSTALL_APPS, true); - restrictions.putBoolean(UserManager.ALLOW_UNINSTALL_APPS, true); - restrictions.putBoolean(UserManager.ALLOW_CONFIG_LOCATION_ACCESS, true); - } - private int readIntAttribute(XmlPullParser parser, String attr, int defaultValue) { String valueString = parser.getAttributeValue(null, attr); if (valueString == null) return defaultValue; @@ -823,7 +822,6 @@ public class UserManagerService extends IUserManager.Stub { writeUserLocked(userInfo); updateUserIdsLocked(); Bundle restrictions = new Bundle(); - initRestrictionsToDefaults(restrictions); mUserRestrictions.append(userId, restrictions); } } diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index 7ef1485..2e309be 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -66,8 +66,8 @@ public class UserManagerTest extends AndroidTestCase { && !user.isPrimary()) { found = true; Bundle restrictions = mUserManager.getUserRestrictions(user.getUserHandle()); - assertTrue("New user should have ALLOW_CONFIG_WIFI =true by default", - restrictions.getBoolean(UserManager.ALLOW_CONFIG_WIFI)); + assertFalse("New user should have DISALLOW_CONFIG_WIFI =false by default", + restrictions.getBoolean(UserManager.DISALLOW_CONFIG_WIFI)); } } assertTrue(found); @@ -147,13 +147,13 @@ public class UserManagerTest extends AndroidTestCase { List<UserInfo> users = mUserManager.getUsers(); if (users.size() > 1) { Bundle restrictions = new Bundle(); - restrictions.putBoolean(UserManager.ALLOW_INSTALL_APPS, false); - restrictions.putBoolean(UserManager.ALLOW_CONFIG_WIFI, true); + restrictions.putBoolean(UserManager.DISALLOW_INSTALL_APPS, true); + restrictions.putBoolean(UserManager.DISALLOW_CONFIG_WIFI, false); mUserManager.setUserRestrictions(restrictions, new UserHandle(users.get(1).id)); Bundle stored = mUserManager.getUserRestrictions(new UserHandle(users.get(1).id)); - assertEquals(stored.getBoolean(UserManager.ALLOW_CONFIG_WIFI), true); - assertEquals(stored.getBoolean(UserManager.ALLOW_UNINSTALL_APPS), true); - assertEquals(stored.getBoolean(UserManager.ALLOW_INSTALL_APPS), false); + assertEquals(stored.getBoolean(UserManager.DISALLOW_CONFIG_WIFI), false); + assertEquals(stored.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS), false); + assertEquals(stored.getBoolean(UserManager.DISALLOW_INSTALL_APPS), true); } } diff --git a/wifi/java/android/net/wifi/WifiStateMachine.java b/wifi/java/android/net/wifi/WifiStateMachine.java index b57910f..c0a3bc1 100644 --- a/wifi/java/android/net/wifi/WifiStateMachine.java +++ b/wifi/java/android/net/wifi/WifiStateMachine.java @@ -1677,12 +1677,12 @@ public class WifiStateMachine extends StateMachine { setNetworkDetailedState(DetailedState.DISCONNECTED); mWifiConfigStore.updateStatus(mLastNetworkId, DetailedState.DISCONNECTED); - /* send event to CM & network change broadcast */ - sendNetworkStateChangeBroadcast(mLastBssid); - /* Clear network properties */ mLinkProperties.clear(); + /* send event to CM & network change broadcast */ + sendNetworkStateChangeBroadcast(mLastBssid); + /* Clear IP settings if the network used DHCP */ if (!mWifiConfigStore.isUsingStaticIp(mLastNetworkId)) { mWifiConfigStore.clearLinkProperties(mLastNetworkId); |